1# This Source Code Form is subject to the terms of the Mozilla Public
2# License, v. 2.0. If a copy of the MPL was not distributed with this
3# file, You can obtain one at https://mozilla.org/MPL/2.0/.
4#
5# This file incorporates work covered by the following copyright and
6# permission notice:
7#
8# Copyright (c) Jeremy Lainé.
9# All rights reserved.
10#
11# Redistribution and use in source and binary forms, with or without
12# modification, are permitted provided that the following conditions are met:
13#
14# * Redistributions of source code must retain the above copyright notice,
15# this list of conditions and the following disclaimer.
16# * Redistributions in binary form must reproduce the above copyright notice,
17# this list of conditions and the following disclaimer in the documentation
18# and/or other materials provided with the distribution.
19# * Neither the name of aiortc nor the names of its contributors may
20# be used to endorse or promote products derived from this software without
21# specific prior written permission.
22#
23# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
24# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
27# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
28# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
29# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
31# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
32# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33
34import asyncio
35import copy
36import logging
37import uuid
38from typing import Optional, Union
39
40from pyee.asyncio import AsyncIOEventEmitter
41
42from . import clock, rtp, sdp
43from .codecs import CODECS, HEADER_EXTENSIONS, is_red, is_rtx, red_block_payload_type
44from .events import RTCTrackEvent
45from .exceptions import (
46 InternalError,
47 InvalidAccessError,
48 InvalidStateError,
49 OperationError,
50)
51from .mediastreams import MediaStreamTrack
52from .rtcconfiguration import RTCBundlePolicy, RTCConfiguration
53from .rtcdatachannel import RTCDataChannel, RTCDataChannelParameters
54from .rtcdtlstransport import RTCCertificate, RTCDtlsParameters, RTCDtlsTransport
55from .rtcicetransport import (
56 RTCIceCandidate,
57 RTCIceGatherer,
58 RTCIceParameters,
59 RTCIceTransport,
60)
61from .rtcrtpparameters import (
62 RTCRtpCodecCapability,
63 RTCRtpCodecParameters,
64 RTCRtpDecodingParameters,
65 RTCRtpHeaderExtensionParameters,
66 RTCRtpParameters,
67 RTCRtpReceiveParameters,
68 RTCRtpRtxParameters,
69 RTCRtpSendParameters,
70)
71from .rtcrtpreceiver import RemoteStreamTrack, RTCRtpReceiver
72from .rtcrtpsender import RTCRtpSender
73from .rtcrtptransceiver import RTCRtpTransceiver
74from .rtcsctptransport import RTCSctpCapabilities, RTCSctpTransport
75from .rtcsessiondescription import RTCSessionDescription
76from .stats import RTCStatsReport
77
78DISCARD_HOST = "0.0.0.0"
79DISCARD_PORT = 9
80MEDIA_KINDS = ["audio", "video"]
81
82logger = logging.getLogger(__name__)
83
84
85def filter_preferred_codecs(
86 codecs: list[RTCRtpCodecParameters], preferred: list[RTCRtpCodecCapability]
87) -> list[RTCRtpCodecParameters]:
88 if not preferred:
89 return codecs
90
91 rtx_codecs = list(filter(is_rtx, codecs))
92 rtx_enabled = next(filter(is_rtx, preferred), None) is not None
93
94 filtered = []
95 for pref in filter(lambda x: not is_rtx(x), preferred):
96 for codec in codecs:
97 if (
98 codec.mimeType.lower() == pref.mimeType.lower()
99 and codec.parameters == pref.parameters
100 ):
101 filtered.append(codec)
102
103 # add corresponding RTX
104 if rtx_enabled:
105 for rtx in rtx_codecs:
106 if rtx.parameters["apt"] == codec.payloadType:
107 filtered.append(rtx)
108 break
109
110 break
111
112 # Drop audio/red whose inner opus payload type was not negotiated, so we
113 # never keep a redundancy codec without the codec it wraps (mirrors rtx/apt).
114 present_pts = {c.payloadType for c in filtered if not is_red(c)}
115 filtered = [
116 c
117 for c in filtered
118 if not is_red(c) or red_block_payload_type(c.parameters) in present_pts
119 ]
120
121 return filtered
122
123
124def receive_only_payload_types() -> dict[str, dict[int, int]]:
125 """The payload types a section only the browser encodes for (the webcam, the microphone)
126 takes in place of the shared ones, by kind: payload types no codec of either kind holds,
127 the dynamic range first for video and the unassigned one (35-63) from the top for audio,
128 static ones (G.722, PCMU, PCMA) too. Such a section's codecs differ in parameters from the
129 display's and the playback's (profiles, sps-pps-idr-in-keyframe, the client's stereo, the
130 playback's ptime, which Chromium folds into every codec of its section), and one payload
131 type with two sets of parameters in a BUNDLE group is a collision Chromium logs (and may
132 one day refuse)."""
133 used = {c.payloadType for kind in CODECS for c in CODECS[kind]}
134 mapping: dict[str, dict[int, int]] = {"video": {}, "audio": {}}
135 free_video = [pt for pt in (*rtp.DYNAMIC_PAYLOAD_TYPES, *rtp.UNASSIGNED_PAYLOAD_TYPES) if pt not in used]
136 for c in CODECS["video"]:
137 if c.payloadType not in mapping["video"]:
138 mapping["video"][c.payloadType] = free_video.pop(0)
139 taken = used | set(mapping["video"].values())
140 free_audio = [pt for pt in reversed(rtp.UNASSIGNED_PAYLOAD_TYPES) if pt not in taken]
141 for c in CODECS["audio"]:
142 if c.payloadType not in mapping["audio"]:
143 mapping["audio"][c.payloadType] = free_audio.pop(0)
144 return mapping
145
146
147def with_payload_types(
148 codec: RTCRtpCodecParameters, mapping: dict[int, int]
149) -> RTCRtpCodecParameters:
150 """`codec` under the payload types `mapping` gives, the one an RTX or RED codec names
151 with it."""
152 codec = copy.deepcopy(codec)
153 codec.payloadType = mapping.get(codec.payloadType, codec.payloadType)
154 if is_rtx(codec) and isinstance(codec.parameters.get("apt"), int):
155 codec.parameters["apt"] = mapping.get(codec.parameters["apt"], codec.parameters["apt"])
156 elif is_red(codec):
157 codec.parameters = {
158 "/".join(str(mapping.get(int(pt), int(pt))) if pt.isdigit() else pt
159 for pt in str(key).split("/")): value
160 for key, value in codec.parameters.items()
161 }
162 return codec
163
164
165def find_common_codecs(
166 local_codecs: list[RTCRtpCodecParameters],
167 remote_codecs: list[RTCRtpCodecParameters],
168) -> list[RTCRtpCodecParameters]:
169 common = []
170 common_base: dict[int, RTCRtpCodecParameters] = {}
171 for c in remote_codecs:
172 # for RTX, check we accepted the base codec
173 if is_rtx(c):
174 apt = c.parameters.get("apt")
175 if isinstance(apt, int) and apt in common_base:
176 base = common_base[apt]
177 if c.clockRate == base.clockRate:
178 common.append(copy.deepcopy(c))
179 continue
180
181 # handle other codecs
182 for codec in local_codecs:
183 if is_codec_compatible(codec, c):
184 codec = copy.deepcopy(codec)
185 if c.payloadType in rtp.DYNAMIC_PAYLOAD_TYPES or c.payloadType in rtp.UNASSIGNED_PAYLOAD_TYPES:
186 codec.payloadType = c.payloadType
187 if is_red(codec):
188 # The payload type of the codec it wraps, as negotiated.
189 codec.parameters = dict(c.parameters)
190 codec.rtcpFeedback = list(
191 filter(lambda x: x in c.rtcpFeedback, codec.rtcpFeedback)
192 )
193 common.append(codec)
194 common_base[codec.payloadType] = codec
195 break
196 return common
197
198
199def find_common_header_extensions(
200 local_extensions: list[RTCRtpHeaderExtensionParameters],
201 remote_extensions: list[RTCRtpHeaderExtensionParameters],
202) -> list[RTCRtpHeaderExtensionParameters]:
203 common = []
204 for rx in remote_extensions:
205 for lx in local_extensions:
206 if lx.uri == rx.uri:
207 common.append(rx)
208 return common
209
210
211def is_codec_compatible(a: RTCRtpCodecParameters, b: RTCRtpCodecParameters) -> bool:
212 if a.mimeType.lower() != b.mimeType.lower() or a.clockRate != b.clockRate:
213 return False
214
215 if a.mimeType.lower() == "video/h264":
216
217 def packetization(c: RTCRtpCodecParameters) -> int:
218 return int(c.parameters.get("packetization-mode", "0"))
219
220 def profile(c: RTCRtpCodecParameters) -> sdp.H264Profile:
221 # for backwards compatibility with older versions of WebRTC,
222 # consider the absence of a profile-level-id parameter to mean
223 # "constrained baseline level 3.1"
224 return sdp.parse_h264_profile_level_id(
225 str(c.parameters.get("profile-level-id", "42E01F"))
226 )[0]
227
228 if packetization(a) != packetization(b):
229 return False
230 try:
231 return profile(a) == profile(b)
232 except ValueError:
233 # Profiles the parser doesn't model (e.g. High 4:4:4 f4001f used by
234 # full-color streams): the sender payloads the encoder's bitstream
235 # as-is, so byte-equal raw ids are compatible.
236 def raw_id(c: RTCRtpCodecParameters) -> str:
237 return str(c.parameters.get("profile-level-id", "42E01F")).lower()
238
239 return raw_id(a) == raw_id(b)
240
241 return True
242
243
244def add_transport_description(
245 media: sdp.MediaDescription, dtlsTransport: RTCDtlsTransport
246) -> None:
247 # ice
248 iceTransport = dtlsTransport.transport
249 iceGatherer = iceTransport.iceGatherer
250 media.ice_candidates = iceGatherer.getLocalCandidates()
251 media.ice_candidates_complete = iceGatherer.state == "completed"
252 media.ice = iceGatherer.getLocalParameters()
253 if media.ice_candidates:
254 media.host = media.ice_candidates[0].ip
255 media.port = media.ice_candidates[0].port
256 else:
257 media.host = DISCARD_HOST
258 media.port = DISCARD_PORT
259
260 # dtls
261 if media.dtls is None:
262 media.dtls = dtlsTransport.getLocalParameters()
263 else:
264 media.dtls.fingerprints = dtlsTransport.getLocalParameters().fingerprints
265
266
267async def add_remote_candidates(
268 iceTransport: RTCIceTransport, media: sdp.MediaDescription
269) -> None:
270 coros = map(iceTransport.addRemoteCandidate, media.ice_candidates)
271 await asyncio.gather(*coros)
272
273 if media.ice_candidates_complete:
274 await iceTransport.addRemoteCandidate(None)
275
276
277def allocate_mid(mids: set[str]) -> str:
278 """
279 Allocate a MID which has not been used yet.
280 """
281 i = 0
282 while True:
283 mid = str(i)
284 if mid not in mids:
285 mids.add(mid)
286 return mid
287 i += 1
288
289
290def create_media_description_for_sctp(
291 sctp: RTCSctpTransport, legacy: bool, mid: str
292) -> sdp.MediaDescription:
293 if legacy:
294 media = sdp.MediaDescription(
295 kind="application", port=DISCARD_PORT, profile="DTLS/SCTP", fmt=[sctp.port]
296 )
297 media.sctpmap[sctp.port] = f"webrtc-datachannel {sctp._outbound_streams_count}"
298 else:
299 media = sdp.MediaDescription(
300 kind="application",
301 port=DISCARD_PORT,
302 profile="UDP/DTLS/SCTP",
303 fmt=["webrtc-datachannel"],
304 )
305 media.sctp_port = sctp.port
306
307 media.rtp.muxId = mid
308 media.sctpCapabilities = sctp.getCapabilities()
309 add_transport_description(media, sctp.transport)
310
311 return media
312
313
314def create_media_description_for_transceiver(
315 transceiver: RTCRtpTransceiver, cname: str, direction: str, mid: str
316) -> sdp.MediaDescription:
317 media = sdp.MediaDescription(
318 kind=transceiver.kind,
319 port=DISCARD_PORT,
320 profile="UDP/TLS/RTP/SAVPF",
321 fmt=[c.payloadType for c in transceiver._codecs],
322 )
323 media.direction = direction
324 media.msid = f"{transceiver.sender._stream_id} {transceiver.sender._track_id}"
325
326 media.rtp = RTCRtpParameters(
327 codecs=transceiver._codecs,
328 headerExtensions=transceiver._headerExtensions,
329 muxId=mid,
330 )
331 media.rtcp_host = DISCARD_HOST
332 media.rtcp_port = DISCARD_PORT
333 media.rtcp_mux = True
334 media.ssrc = [sdp.SsrcDescription(ssrc=transceiver.sender._ssrc, cname=cname)]
335
336 # if RTX is enabled, add corresponding SSRC
337 if next(filter(is_rtx, media.rtp.codecs), None):
338 media.ssrc.append(
339 sdp.SsrcDescription(ssrc=transceiver.sender._rtx_ssrc, cname=cname)
340 )
341 media.ssrc_group.append(
342 sdp.GroupDescription(
343 semantic="FID",
344 items=[transceiver.sender._ssrc, transceiver.sender._rtx_ssrc],
345 )
346 )
347
348 # if FlexFEC is on the table, announce the repair stream (FEC-FR group) and
349 # declare our send resolution envelope (RFC 6236) for the video line.
350 if next(
351 (c for c in media.rtp.codecs if c.mimeType.lower() == "video/flexfec-03"), None
352 ):
353 media.ssrc.append(
354 sdp.SsrcDescription(ssrc=transceiver.sender._fec_ssrc, cname=cname)
355 )
356 media.ssrc_group.append(
357 sdp.GroupDescription(
358 semantic="FEC-FR",
359 items=[transceiver.sender._ssrc, transceiver.sender._fec_ssrc],
360 )
361 )
362 if transceiver.kind == "video":
363 media.imageattr = ["* send [x=[16:8192],y=[16:8192]] recv *"]
364
365 add_transport_description(media, transceiver.receiver.transport)
366
367 return media
368
369
370def and_direction(a: str, b: str) -> str:
371 return sdp.DIRECTIONS[sdp.DIRECTIONS.index(a) & sdp.DIRECTIONS.index(b)]
372
373
374def or_direction(a: str, b: str) -> str:
375 return sdp.DIRECTIONS[sdp.DIRECTIONS.index(a) | sdp.DIRECTIONS.index(b)]
376
377
378def reverse_direction(direction: str) -> str:
379 if direction == "sendonly":
380 return "recvonly"
381 elif direction == "recvonly":
382 return "sendonly"
383 return direction
384
385
386def wrap_session_description(
387 session_description: Optional[sdp.SessionDescription],
388) -> Optional[RTCSessionDescription]:
389 if session_description is not None:
390 return RTCSessionDescription(
391 sdp=str(session_description), type=session_description.type
392 )
393 return None
394
395
396class RTCPeerConnection(AsyncIOEventEmitter):
397 """
398 The :class:`RTCPeerConnection` interface represents a WebRTC connection
399 between the local computer and a remote peer.
400
401 :param configuration: An optional :class:`RTCConfiguration`.
402 """
403
404 def __init__(self, configuration: Optional[RTCConfiguration] = None) -> None:
405 super().__init__()
406 self.__certificates = [RTCCertificate.generateCertificate()]
407 self.__cname = f"{uuid.uuid4()}"
408 self.__configuration = configuration or RTCConfiguration()
409 self.__dtlsTransports: set[RTCDtlsTransport] = set()
410 self.__iceTransports: set[RTCIceTransport] = set()
411 self.__remoteDtls: dict[
412 Union[RTCRtpTransceiver, RTCSctpTransport], RTCDtlsParameters
413 ] = {}
414 self.__remoteIce: dict[
415 Union[RTCRtpTransceiver, RTCSctpTransport], RTCIceParameters
416 ] = {}
417 self.__seenMids: set[str] = set()
418 self.__sctp: Optional[RTCSctpTransport] = None
419 self.__sctp_mline_index: Optional[int] = None
420 # Offer the standards SCTP SDP (UDP/DTLS/SCTP + a=sctp-port +
421 # a=max-message-size): Chromium ignores a=max-message-size on the legacy
422 # DTLS/SCTP form and caps data channel messages at 64 KiB. Answers still
423 # adapt to whichever form the peer offered.
424 self._sctpLegacySdp = False
425 self.__sctpRemotePort: Optional[int] = None
426 self.__sctpRemoteCaps: Optional[RTCSctpCapabilities] = None
427 self.__stream_id = str(uuid.uuid4())
428 self.__transceivers: list[RTCRtpTransceiver] = []
429
430 self.__closeTask: Optional[asyncio.Task] = None
431 self.__connectionState = "new"
432 self.__iceConnectionState = "new"
433 self.__iceGatheringState = "new"
434 self.__isClosed: Optional[asyncio.Future[bool]] = None
435 self.__signalingState = "stable"
436
437 self.__currentLocalDescription: Optional[sdp.SessionDescription] = None
438 self.__currentRemoteDescription: Optional[sdp.SessionDescription] = None
439 self.__pendingLocalDescription: Optional[sdp.SessionDescription] = None
440 self.__pendingRemoteDescription: Optional[sdp.SessionDescription] = None
441
442 @property
443 def connectionState(self) -> str:
444 """
445 The current connection state.
446
447 Possible values: `"connected"`, `"connecting"`, `"closed"`, `"failed"`, `"new`".
448
449 When the state changes, the `"connectionstatechange"` event is fired.
450 """
451 return self.__connectionState
452
453 @property
454 def iceConnectionState(self) -> str:
455 """
456 The current ICE connection state.
457
458 Possible values: `"checking"`, `"completed"`, `"closed"`, `"failed"`, `"new`".
459
460 When the state changes, the `"iceconnectionstatechange"` event is fired.
461 """
462 return self.__iceConnectionState
463
464 @property
465 def iceGatheringState(self) -> str:
466 """
467 The current ICE gathering state.
468
469 Possible values: `"complete"`, `"gathering"`, `"new`".
470
471 When the state changes, the `"icegatheringstatechange"` event is fired.
472 """
473 return self.__iceGatheringState
474
475 @property
476 def localDescription(self) -> RTCSessionDescription:
477 """
478 An :class:`RTCSessionDescription` describing the session for
479 the local end of the connection.
480 """
481 return wrap_session_description(self.__localDescription())
482
483 @property
484 def remoteDescription(self) -> RTCSessionDescription:
485 """
486 An :class:`RTCSessionDescription` describing the session for
487 the remote end of the connection.
488 """
489 return wrap_session_description(self.__remoteDescription())
490
491 @property
492 def sctp(self) -> Optional[RTCSctpTransport]:
493 """
494 An :class:`RTCSctpTransport` describing the SCTP transport being used
495 for datachannels or `None`.
496 """
497 return self.__sctp
498
499 @property
500 def signalingState(self) -> str:
501 """
502 The current signaling state.
503
504 Possible values: `"closed"`, `"have-local-offer"`, `"have-remote-offer`",
505 `"stable"`.
506
507 When the state changes, the `"signalingstatechange"` event is fired.
508 """
509 return self.__signalingState
510
511 async def addIceCandidate(self, candidate: Optional[RTCIceCandidate]) -> None:
512 """
513 Add a new :class:`RTCIceCandidate` received from the remote peer.
514
515 The specified candidate must have a value for either `sdpMid` or
516 `sdpMLineIndex`.
517
518 :param candidate: The new remote candidate or `None` to signal
519 end-of-candidates.
520 """
521 if (
522 candidate is not None
523 and candidate.sdpMid is None
524 and candidate.sdpMLineIndex is None
525 ):
526 raise ValueError("Candidate must have either sdpMid or sdpMLineIndex")
527
528 for transceiver in self.__transceivers:
529 if (
530 candidate is None
531 or (
532 candidate.sdpMid == transceiver.mid
533 or candidate.sdpMLineIndex == transceiver._get_mline_index()
534 )
535 and not transceiver._bundled
536 ):
537 iceTransport = transceiver.receiver.transport.transport
538 await iceTransport.addRemoteCandidate(candidate)
539
540 if self.__sctp and (
541 candidate is None
542 or (
543 candidate.sdpMid == self.__sctp.mid
544 or candidate.sdpMLineIndex == self.__sctp_mline_index
545 )
546 and not self.__sctp._bundled
547 ):
548 iceTransport = self.__sctp.transport.transport
549 await iceTransport.addRemoteCandidate(candidate)
550
551 # Update the remote description if it exists.
552 # The spec forbids adding candidates prior to setRemoteDescription
553 # but this used to work in previous versions.
554 if remote_description := self.__remoteDescription():
555 media = remote_description.media
556 for sdp_m_line_index in range(0, len(media)):
557 if candidate is None:
558 media[sdp_m_line_index].ice_candidates_complete = True
559 elif (
560 candidate.sdpMLineIndex == sdp_m_line_index
561 or candidate.sdpMid == media[sdp_m_line_index].rtp.muxId
562 ):
563 media[sdp_m_line_index].ice_candidates.append(candidate)
564 else:
565 logger.warning(
566 "RTCPeerConnection addIceCandidate called without remote description"
567 )
568
569 def addTrack(self, track: MediaStreamTrack) -> RTCRtpSender:
570 """
571 Add a :class:`MediaStreamTrack` to the set of media tracks which
572 will be transmitted to the remote peer.
573 """
574 # check state is valid
575 self.__assertNotClosed()
576 if track.kind not in ["audio", "video"]:
577 raise InternalError(f'Invalid track kind "{track.kind}"')
578
579 # don't add track twice
580 self.__assertTrackHasNoSender(track)
581
582 for transceiver in self.__transceivers:
583 if transceiver.kind == track.kind:
584 if transceiver.sender.track is None:
585 transceiver.sender.replaceTrack(track)
586 transceiver.direction = or_direction(
587 transceiver.direction, "sendonly"
588 )
589 return transceiver.sender
590
591 transceiver = self.__createTransceiver(
592 direction="sendrecv", kind=track.kind, sender_track=track
593 )
594 return transceiver.sender
595
596 def addTransceiver(
597 self, trackOrKind: Union[str, MediaStreamTrack], direction: str = "sendrecv"
598 ) -> RTCRtpTransceiver:
599 """
600 Add a new :class:`RTCRtpTransceiver`.
601 """
602 self.__assertNotClosed()
603
604 # determine track or kind
605 if isinstance(trackOrKind, MediaStreamTrack):
606 kind = trackOrKind.kind
607 track = trackOrKind
608 else:
609 kind = trackOrKind
610 track = None
611 if kind not in ["audio", "video"]:
612 raise InternalError(f'Invalid track kind "{kind}"')
613
614 # check direction
615 if direction not in sdp.DIRECTIONS:
616 raise InternalError(f'Invalid direction "{direction}"')
617
618 # don't add track twice
619 if track:
620 self.__assertTrackHasNoSender(track)
621
622 return self.__createTransceiver(
623 direction=direction, kind=kind, sender_track=track
624 )
625
626 async def close(self) -> None:
627 """
628 Terminate the ICE agent, ending ICE processing and streams.
629 """
630 if self.__isClosed:
631 await self.__isClosed
632 return
633 self.__isClosed = asyncio.Future()
634 self.__setSignalingState("closed")
635
636 # stop senders / receivers
637 for transceiver in self.__transceivers:
638 await transceiver.stop()
639 if self.__sctp:
640 await self.__sctp.stop()
641
642 # stop transports
643 for transceiver in self.__transceivers:
644 await transceiver.receiver.transport.stop()
645 await transceiver.receiver.transport.transport.stop()
646 if self.__sctp:
647 await self.__sctp.transport.stop()
648 await self.__sctp.transport.transport.stop()
649
650 # update states
651 self.__updateIceGatheringState()
652 self.__updateIceConnectionState()
653 self.__updateConnectionState()
654
655 # no more events will be emitted, so remove all event listeners
656 # to facilitate garbage collection.
657 self.remove_all_listeners()
658
659 self.__isClosed.set_result(True)
660
661 async def createAnswer(self) -> RTCSessionDescription:
662 """
663 Create an SDP answer to an offer received from a remote peer during
664 the offer/answer negotiation of a WebRTC connection.
665
666 :rtype: :class:`RTCSessionDescription`
667 """
668 # check state is valid
669 self.__assertNotClosed()
670 if self.signalingState not in ["have-remote-offer", "have-local-pranswer"]:
671 raise InvalidStateError(
672 f'Cannot create answer in signaling state "{self.signalingState}"'
673 )
674
675 # create description
676 ntp_seconds = clock.current_ntp_time() >> 32
677 description = sdp.SessionDescription()
678 description.origin = f"- {ntp_seconds} {ntp_seconds} IN IP4 0.0.0.0"
679 description.msid_semantic.append(
680 sdp.GroupDescription(semantic="WMS", items=["*"])
681 )
682 description.type = "answer"
683
684 for remote_m in self.__remoteDescription().media:
685 if remote_m.kind in ["audio", "video"]:
686 transceiver = self.__getTransceiverByMid(remote_m.rtp.muxId)
687 media = create_media_description_for_transceiver(
688 transceiver,
689 cname=self.__cname,
690 direction=and_direction(
691 transceiver.direction, transceiver._offerDirection
692 ),
693 mid=transceiver.mid,
694 )
695 dtlsTransport = transceiver.receiver.transport
696 else:
697 media = create_media_description_for_sctp(
698 self.__sctp, legacy=self._sctpLegacySdp, mid=self.__sctp.mid
699 )
700 dtlsTransport = self.__sctp.transport
701
702 # determine DTLS role, or preserve the currently configured role
703 if dtlsTransport._role == "auto":
704 media.dtls.role = "client"
705 else:
706 media.dtls.role = dtlsTransport._role
707
708 description.media.append(media)
709
710 bundle = sdp.GroupDescription(semantic="BUNDLE", items=[])
711 for media in description.media:
712 bundle.items.append(media.rtp.muxId)
713 description.group.append(bundle)
714
715 return wrap_session_description(description)
716
717 def createDataChannel(
718 self,
719 label: str,
720 maxPacketLifeTime: Optional[int] = None,
721 maxRetransmits: Optional[int] = None,
722 ordered: bool = True,
723 protocol: str = "",
724 negotiated: bool = False,
725 id: Optional[int] = None,
726 ) -> RTCDataChannel:
727 """
728 Create a data channel with the given label.
729
730 :rtype: :class:`RTCDataChannel`
731 """
732 if maxPacketLifeTime is not None and maxRetransmits is not None:
733 raise ValueError("Cannot specify both maxPacketLifeTime and maxRetransmits")
734
735 if not self.__sctp:
736 self.__createSctpTransport()
737
738 parameters = RTCDataChannelParameters(
739 id=id,
740 label=label,
741 maxPacketLifeTime=maxPacketLifeTime,
742 maxRetransmits=maxRetransmits,
743 negotiated=negotiated,
744 ordered=ordered,
745 protocol=protocol,
746 )
747 return RTCDataChannel(self.__sctp, parameters)
748
749 async def createOffer(self) -> RTCSessionDescription:
750 """
751 Create an SDP offer for the purpose of starting a new WebRTC
752 connection to a remote peer.
753
754 :rtype: :class:`RTCSessionDescription`
755 """
756 # check state is valid
757 self.__assertNotClosed()
758
759 # offer codecs
760 receive_only = receive_only_payload_types()
761 for transceiver in self.__transceivers:
762 transceiver._codecs = filter_preferred_codecs(
763 CODECS[transceiver.kind][:], transceiver._preferred_codecs
764 )
765 if transceiver.direction == "recvonly":
766 transceiver._codecs = [
767 with_payload_types(c, receive_only[transceiver.kind])
768 for c in transceiver._codecs
769 ]
770 transceiver._headerExtensions = HEADER_EXTENSIONS[transceiver.kind][:]
771
772 mids = self.__seenMids.copy()
773
774 # create description
775 ntp_seconds = clock.current_ntp_time() >> 32
776 description = sdp.SessionDescription()
777 description.origin = f"- {ntp_seconds} {ntp_seconds} IN IP4 0.0.0.0"
778 description.msid_semantic.append(
779 sdp.GroupDescription(semantic="WMS", items=["*"])
780 )
781 description.type = "offer"
782
783 def get_media(
784 description: sdp.SessionDescription,
785 ) -> list[sdp.MediaDescription]:
786 return description.media if description else []
787
788 def get_media_section(
789 media: list[sdp.MediaDescription], i: int
790 ) -> Optional[sdp.MediaDescription]:
791 return media[i] if i < len(media) else None
792
793 # handle existing transceivers / sctp
794 local_media = get_media(self.__localDescription())
795 remote_media = get_media(self.__remoteDescription())
796 for i in range(max(len(local_media), len(remote_media))):
797 local_m = get_media_section(local_media, i)
798 remote_m = get_media_section(remote_media, i)
799 media_kind = local_m.kind if local_m else remote_m.kind
800 mid = local_m.rtp.muxId if local_m else remote_m.rtp.muxId
801 if media_kind in ["audio", "video"]:
802 transceiver = self.__getTransceiverByMid(mid)
803 transceiver._set_mline_index(i)
804 description.media.append(
805 create_media_description_for_transceiver(
806 transceiver,
807 cname=self.__cname,
808 direction=transceiver.direction,
809 mid=mid,
810 )
811 )
812 elif media_kind == "application":
813 self.__sctp_mline_index = i
814 description.media.append(
815 create_media_description_for_sctp(
816 self.__sctp, legacy=self._sctpLegacySdp, mid=mid
817 )
818 )
819
820 # handle new transceivers / sctp
821 def next_mline_index() -> int:
822 return len(description.media)
823
824 def add_datachannel_section() -> None:
825 self.__sctp_mline_index = next_mline_index()
826 description.media.append(
827 create_media_description_for_sctp(
828 self.__sctp, legacy=self._sctpLegacySdp, mid=allocate_mid(mids)
829 )
830 )
831
832 # JSEP specifies data channels should only be added after audio/video
833 # m-sections, but the `alwaysNegotiateDataChannels` option overrides that:
834 # https://w3c.github.io/webrtc-extensions/#always-negotiating-datachannels
835 if not self.__sctp and self.__configuration.alwaysNegotiateDataChannels:
836 self.__createSctpTransport()
837 add_datachannel_section()
838
839 for transceiver in filter(
840 lambda x: x.mid is None and not x.stopped, self.__transceivers
841 ):
842 transceiver._set_mline_index(next_mline_index())
843 description.media.append(
844 create_media_description_for_transceiver(
845 transceiver,
846 cname=self.__cname,
847 direction=transceiver.direction,
848 mid=allocate_mid(mids),
849 )
850 )
851 if (
852 self.__sctp
853 and self.__sctp.mid is None
854 and not self.__configuration.alwaysNegotiateDataChannels
855 ):
856 add_datachannel_section()
857
858 bundle = sdp.GroupDescription(semantic="BUNDLE", items=[])
859 for media in description.media:
860 bundle.items.append(media.rtp.muxId)
861 description.group.append(bundle)
862
863 return wrap_session_description(description)
864
865 def getReceivers(self) -> list[RTCRtpReceiver]:
866 """
867 Returns the list of :class:`RTCRtpReceiver` objects that are currently
868 attached to the connection.
869 """
870 return list(map(lambda x: x.receiver, self.__transceivers))
871
872 def getSenders(self) -> list[RTCRtpSender]:
873 """
874 Returns the list of :class:`RTCRtpSender` objects that are currently
875 attached to the connection.
876 """
877 return list(map(lambda x: x.sender, self.__transceivers))
878
879 async def getStats(self) -> RTCStatsReport:
880 """
881 Returns statistics for the connection.
882
883 :rtype: :class:`RTCStatsReport`
884 """
885 merged = RTCStatsReport()
886 coros = [x.getStats() for x in self.getSenders()] + [
887 x.getStats() for x in self.getReceivers()
888 ]
889 for report in await asyncio.gather(*coros):
890 merged.update(report)
891 return merged
892
893 def getTransceivers(self) -> list[RTCRtpTransceiver]:
894 """
895 Returns the list of :class:`RTCRtpTransceiver` objects that are currently
896 attached to the connection.
897 """
898 return list(self.__transceivers)
899
900 async def setLocalDescription(
901 self, sessionDescription: Optional[RTCSessionDescription] = None
902 ) -> None:
903 """
904 Change the local description associated with the connection.
905
906 :param sessionDescription: An :class:`RTCSessionDescription` generated
907 by :meth:`createOffer` or :meth:`createAnswer()`
908 or `None` to implicitly create an offer or create
909 an answer, as needed.
910 """
911 # check state is valid
912 self.__assertNotClosed()
913
914 if sessionDescription is None:
915 # https://w3c.github.io/webrtc-pc/#dom-peerconnection-setlocaldescription
916 # If left out, then setLocalDescription will implicitly create an offer
917 # or create an answer, as needed.
918 if self.signalingState == "have-remote-offer":
919 sessionDescription = await self.createAnswer()
920 else:
921 sessionDescription = await self.createOffer()
922 self.__log_debug(
923 "setLocalDescription(%s, implicit)\n%s",
924 sessionDescription.type,
925 sessionDescription.sdp,
926 )
927 else:
928 self.__log_debug(
929 "setLocalDescription(%s)\n%s",
930 sessionDescription.type,
931 sessionDescription.sdp,
932 )
933
934 # parse and validate description
935 description = sdp.SessionDescription.parse(sessionDescription.sdp)
936 description.type = sessionDescription.type
937 self.__validate_description(description, is_local=True)
938
939 # update signaling state
940 if description.type == "offer":
941 self.__setSignalingState("have-local-offer")
942 elif description.type == "answer":
943 self.__setSignalingState("stable")
944
945 # assign MID
946 for i, media in enumerate(description.media):
947 mid = media.rtp.muxId
948 self.__seenMids.add(mid)
949 if media.kind in ["audio", "video"]:
950 transceiver = self.__getTransceiverByMLineIndex(i)
951 transceiver._set_mid(mid)
952 elif media.kind == "application":
953 self.__sctp.mid = mid
954
955 # set ICE role; an ICE-lite agent is controlled whichever side offers
956 if description.type == "offer":
957 for iceTransport in self.__iceTransports:
958 if not iceTransport._role_set:
959 iceTransport._connection.ice_controlling = (
960 not iceTransport._connection.ice_lite
961 )
962 iceTransport._role_set = True
963
964 # set DTLS role
965 if description.type == "answer":
966 for i, media in enumerate(description.media):
967 if media.kind in ["audio", "video"]:
968 transceiver = self.__getTransceiverByMLineIndex(i)
969 transceiver.receiver.transport._set_role(media.dtls.role)
970 elif media.kind == "application":
971 self.__sctp.transport._set_role(media.dtls.role)
972
973 # configure direction
974 for t in self.__transceivers:
975 if description.type in ["answer", "pranswer"]:
976 t._setCurrentDirection(and_direction(t.direction, t._offerDirection))
977
978 # gather candidates
979 await self.__gather()
980 for i, media in enumerate(description.media):
981 if media.kind in ["audio", "video"]:
982 transceiver = self.__getTransceiverByMLineIndex(i)
983 add_transport_description(media, transceiver.receiver.transport)
984 elif media.kind == "application":
985 add_transport_description(media, self.__sctp.transport)
986
987 # connect
988 asyncio.ensure_future(self.__connect())
989
990 # replace description
991 if description.type == "answer":
992 self.__currentLocalDescription = description
993 self.__pendingLocalDescription = None
994 else:
995 self.__pendingLocalDescription = description
996
997 async def setRemoteDescription(
998 self, sessionDescription: RTCSessionDescription
999 ) -> None:
1000 """
1001 Changes the remote description associated with the connection.
1002
1003 :param sessionDescription: An :class:`RTCSessionDescription` created from
1004 information received over the signaling channel.
1005 """
1006 self.__log_debug(
1007 "setRemoteDescription(%s)\n%s",
1008 sessionDescription.type,
1009 sessionDescription.sdp,
1010 )
1011
1012 # parse and validate description
1013 description = sdp.SessionDescription.parse(sessionDescription.sdp)
1014 description.type = sessionDescription.type
1015 self.__validate_description(description, is_local=False)
1016
1017 # apply description
1018 iceCandidates: dict[RTCIceTransport, sdp.MediaDescription] = {}
1019 trackEvents = []
1020 for i, media in enumerate(description.media):
1021 dtlsTransport: Optional[RTCDtlsTransport] = None
1022 self.__seenMids.add(media.rtp.muxId)
1023 if media.port == 0:
1024 # The peer rejected this m-line (RFC 3264: port zero); nothing
1025 # negotiates. Drop its never-started transports from the state
1026 # aggregation so the connection doesn't wait on them forever —
1027 # close() still reaches them through the transceiver list.
1028 if media.kind in ["audio", "video"]:
1029 for transceiver in self.__transceivers:
1030 if transceiver.kind == media.kind and transceiver.mid in [
1031 None,
1032 media.rtp.muxId,
1033 ]:
1034 if transceiver.mid is None:
1035 transceiver._set_mid(media.rtp.muxId)
1036 transceiver._set_mline_index(i)
1037 if description.type in ["answer", "pranswer"]:
1038 transceiver._setCurrentDirection("inactive")
1039 rejectedDtls = transceiver.receiver.transport
1040 if not any(
1041 t is not transceiver
1042 and t.receiver.transport is rejectedDtls
1043 for t in self.__transceivers
1044 ):
1045 self.__dtlsTransports.discard(rejectedDtls)
1046 self.__iceTransports.discard(rejectedDtls.transport)
1047 break
1048 elif media.kind == "application" and self.__sctp:
1049 self.__dtlsTransports.discard(self.__sctp.transport)
1050 self.__iceTransports.discard(self.__sctp.transport.transport)
1051 continue
1052 if media.kind in ["audio", "video"]:
1053 # find transceiver
1054 transceiver = None
1055 for t in self.__transceivers:
1056 if t.kind == media.kind and t.mid in [None, media.rtp.muxId]:
1057 transceiver = t
1058 break
1059 if transceiver is None:
1060 transceiver = self.__createTransceiver(
1061 direction="recvonly", kind=media.kind
1062 )
1063 if transceiver.mid is None:
1064 transceiver._set_mid(media.rtp.muxId)
1065 transceiver._set_mline_index(i)
1066
1067 # negotiate codecs
1068 common = filter_preferred_codecs(
1069 find_common_codecs(CODECS[media.kind], media.rtp.codecs),
1070 transceiver._preferred_codecs,
1071 )
1072
1073 if not len(common):
1074 raise OperationError(
1075 "Failed to set remote {} description send parameters".format(
1076 media.kind
1077 )
1078 )
1079
1080 transceiver._codecs = common
1081 transceiver._headerExtensions = find_common_header_extensions(
1082 HEADER_EXTENSIONS[media.kind], media.rtp.headerExtensions
1083 )
1084
1085 # configure direction
1086 direction = reverse_direction(media.direction)
1087 if description.type in ["answer", "pranswer"]:
1088 transceiver._setCurrentDirection(direction)
1089 else:
1090 transceiver._offerDirection = direction
1091
1092 # create remote stream track
1093 if (
1094 direction in ["recvonly", "sendrecv"]
1095 and not transceiver.receiver.track
1096 ):
1097 transceiver.receiver._track = RemoteStreamTrack(
1098 kind=media.kind, id=description.webrtc_track_id(media)
1099 )
1100 trackEvents.append(
1101 RTCTrackEvent(
1102 receiver=transceiver.receiver,
1103 track=transceiver.receiver.track,
1104 transceiver=transceiver,
1105 )
1106 )
1107
1108 # memorise transport parameters
1109 dtlsTransport = transceiver.receiver.transport
1110 self.__remoteDtls[transceiver] = media.dtls
1111 self.__remoteIce[transceiver] = media.ice
1112
1113 elif media.kind == "application":
1114 if not self.__sctp:
1115 self.__createSctpTransport()
1116 if self.__sctp.mid is None:
1117 self.__sctp.mid = media.rtp.muxId
1118 self.__sctp_mline_index = i
1119
1120 # configure sctp
1121 if media.profile == "DTLS/SCTP":
1122 self._sctpLegacySdp = True
1123 self.__sctpRemotePort = int(media.fmt[0])
1124 else:
1125 self._sctpLegacySdp = False
1126 self.__sctpRemotePort = media.sctp_port
1127 self.__sctpRemoteCaps = media.sctpCapabilities
1128
1129 # memorise transport parameters
1130 dtlsTransport = self.__sctp.transport
1131 self.__remoteDtls[self.__sctp] = media.dtls
1132 self.__remoteIce[self.__sctp] = media.ice
1133
1134 if dtlsTransport is not None:
1135 # add ICE candidates
1136 iceTransport = dtlsTransport.transport
1137 iceCandidates[iceTransport] = media
1138
1139 # set ICE role
1140 if description.type == "offer" and not iceTransport._role_set:
1141 iceTransport._connection.ice_controlling = (
1142 media.ice.iceLite and not iceTransport._connection.ice_lite
1143 )
1144 iceTransport._role_set = True
1145
1146 # set DTLS role
1147 if description.type == "offer" and media.dtls.role == "client":
1148 dtlsTransport._set_role(role="server")
1149 if description.type == "answer":
1150 dtlsTransport._set_role(
1151 role="server" if media.dtls.role == "client" else "client"
1152 )
1153
1154 # remove bundled transports
1155 bundle = next((x for x in description.group if x.semantic == "BUNDLE"), None)
1156 if bundle and bundle.items:
1157 # find main media stream
1158 primaryMid = bundle.items[0]
1159 primaryTransport = None
1160 for transceiver in self.__transceivers:
1161 if transceiver.mid == primaryMid:
1162 primaryTransport = transceiver.receiver.transport
1163 break
1164 if self.__sctp and self.__sctp.mid == primaryMid:
1165 primaryTransport = self.__sctp.transport
1166
1167 # replace transport for bundled media; a slave that already shares
1168 # the primary transport (same-kind transceivers are created on one
1169 # transport) must not retire it as an "old" transport — e.g. a
1170 # Firefox answer that rejected the first m-line re-anchors the
1171 # bundle on such a shared transport.
1172 oldTransports = set()
1173 slaveMids = bundle.items[1:]
1174 for transceiver in self.__transceivers:
1175 if transceiver.mid in slaveMids and not transceiver._bundled:
1176 if transceiver.receiver.transport is not primaryTransport:
1177 oldTransports.add(transceiver.receiver.transport)
1178 transceiver.receiver.setTransport(primaryTransport)
1179 transceiver.sender.setTransport(primaryTransport)
1180 transceiver._bundled = True
1181 if (
1182 self.__sctp
1183 and self.__sctp.mid in slaveMids
1184 and not self.__sctp._bundled
1185 ):
1186 if self.__sctp.transport is not primaryTransport:
1187 oldTransports.add(self.__sctp.transport)
1188 self.__sctp.setTransport(primaryTransport)
1189 self.__sctp._bundled = True
1190
1191 # stop and discard old ICE transports
1192 for dtlsTransport in oldTransports:
1193 await dtlsTransport.stop()
1194 await dtlsTransport.transport.stop()
1195 self.__dtlsTransports.discard(dtlsTransport)
1196 self.__iceTransports.discard(dtlsTransport.transport)
1197 iceCandidates.pop(dtlsTransport.transport, None)
1198 self.__updateIceGatheringState()
1199 self.__updateIceConnectionState()
1200 self.__updateConnectionState()
1201
1202 # add remote candidates
1203 coros = [
1204 add_remote_candidates(iceTransport, media)
1205 for iceTransport, media in iceCandidates.items()
1206 ]
1207 await asyncio.gather(*coros)
1208
1209 # Emits the bare track rather than a wrapper event object; the rtc.py
1210 # listeners rely on receiving the track object itself.
1211 for event in trackEvents:
1212 self.emit("track", event.track)
1213
1214 # connect
1215 asyncio.ensure_future(self.__connect())
1216
1217 # update signaling state
1218 if description.type == "offer":
1219 self.__setSignalingState("have-remote-offer")
1220 elif description.type == "answer":
1221 self.__setSignalingState("stable")
1222
1223 # replace description
1224 if description.type == "answer":
1225 self.__currentRemoteDescription = description
1226 self.__pendingRemoteDescription = None
1227 else:
1228 self.__pendingRemoteDescription = description
1229
1230 async def __connect(self) -> None:
1231 for transceiver in self.__transceivers:
1232 dtlsTransport = transceiver.receiver.transport
1233 iceTransport = dtlsTransport.transport
1234 if (
1235 iceTransport.iceGatherer.getLocalCandidates()
1236 and transceiver in self.__remoteIce
1237 ):
1238 await iceTransport.start(self.__remoteIce[transceiver])
1239 if dtlsTransport.state == "new":
1240 await dtlsTransport.start(self.__remoteDtls[transceiver])
1241 if dtlsTransport.state == "connected":
1242 if transceiver.currentDirection in ["sendonly", "sendrecv"]:
1243 await transceiver.sender.send(self.__localRtp(transceiver))
1244 if transceiver.currentDirection in ["recvonly", "sendrecv"]:
1245 await transceiver.receiver.receive(
1246 self.__remoteRtp(transceiver)
1247 )
1248 if self.__sctp:
1249 dtlsTransport = self.__sctp.transport
1250 iceTransport = dtlsTransport.transport
1251 if (
1252 iceTransport.iceGatherer.getLocalCandidates()
1253 and self.__sctp in self.__remoteIce
1254 ):
1255 await iceTransport.start(self.__remoteIce[self.__sctp])
1256 if dtlsTransport.state == "new":
1257 await dtlsTransport.start(self.__remoteDtls[self.__sctp])
1258 if dtlsTransport.state == "connected":
1259 await self.__sctp.start(
1260 self.__sctpRemoteCaps, self.__sctpRemotePort
1261 )
1262
1263 async def __gather(self) -> None:
1264 coros = map(lambda t: t.iceGatherer.gather(), self.__iceTransports)
1265 await asyncio.gather(*coros)
1266
1267 def __assertNotClosed(self) -> None:
1268 if self.__isClosed:
1269 raise InvalidStateError("RTCPeerConnection is closed")
1270
1271 def __assertTrackHasNoSender(self, track: MediaStreamTrack) -> None:
1272 for sender in self.getSenders():
1273 if sender.track == track:
1274 raise InvalidAccessError("Track already has a sender")
1275
1276 def __createDtlsTransport(self) -> RTCDtlsTransport:
1277 # create ICE transport
1278 if len(self.__transceivers) > 0 or self.__sctp:
1279 if len(self.__transceivers) > 0:
1280 parameters = self.__transceivers[
1281 0
1282 ].receiver.transport.transport.iceGatherer.getLocalParameters()
1283 else:
1284 parameters = (
1285 self.__sctp.transport.transport.iceGatherer.getLocalParameters()
1286 )
1287 iceGatherer = RTCIceGatherer(
1288 iceServers=self.__configuration.iceServers,
1289 local_username=parameters.usernameFragment,
1290 local_password=parameters.password,
1291 ice_host_public_ips=self.__configuration.iceHostPublicIps,
1292 ice_port_range=self.__configuration.icePortRange,
1293 ice_udp_mux=self.__configuration.iceUdpMux,
1294 ice_tcp_mux=self.__configuration.iceTcpMux,
1295 ice_lite=self.__configuration.iceLite,
1296 )
1297 else:
1298 iceGatherer = RTCIceGatherer(
1299 iceServers=self.__configuration.iceServers,
1300 ice_host_public_ips=self.__configuration.iceHostPublicIps,
1301 ice_port_range=self.__configuration.icePortRange,
1302 ice_udp_mux=self.__configuration.iceUdpMux,
1303 ice_tcp_mux=self.__configuration.iceTcpMux,
1304 ice_lite=self.__configuration.iceLite,
1305 )
1306
1307 iceGatherer.on("statechange", self.__updateIceGatheringState)
1308 iceTransport = RTCIceTransport(iceGatherer)
1309 iceTransport.on("statechange", self.__updateIceConnectionState)
1310 iceTransport.on("statechange", self.__updateConnectionState)
1311 self.__iceTransports.add(iceTransport)
1312
1313 # create DTLS transport
1314 dtlsTransport = RTCDtlsTransport(iceTransport, self.__certificates)
1315 dtlsTransport.on("statechange", self.__updateConnectionState)
1316 self.__dtlsTransports.add(dtlsTransport)
1317
1318 # update states
1319 self.__updateIceGatheringState()
1320 self.__updateIceConnectionState()
1321 self.__updateConnectionState()
1322
1323 return dtlsTransport
1324
1325 def __createSctpTransport(self) -> None:
1326 dtlsTransport = None
1327 bundled = (
1328 self.__configuration.bundlePolicy == RTCBundlePolicy.MAX_BUNDLE
1329 and len(self.__transceivers) > 0
1330 )
1331 if bundled:
1332 dtlsTransport = self.__transceivers[0].receiver.transport
1333 else:
1334 dtlsTransport = self.__createDtlsTransport()
1335 self.__sctp = RTCSctpTransport(dtlsTransport)
1336 self.__sctp._bundled = bundled
1337 self.__sctp.mid = None
1338
1339 @self.__sctp.on("datachannel")
1340 def on_datachannel(channel: RTCDataChannel) -> None:
1341 self.emit("datachannel", channel)
1342
1343 def __createTransceiver(
1344 self, direction: str, kind: str, sender_track: Optional[MediaStreamTrack] = None
1345 ) -> RTCRtpTransceiver:
1346 dtlsTransport = None
1347 bundled = False
1348 if self.__configuration.bundlePolicy == RTCBundlePolicy.MAX_BUNDLE:
1349 if len(self.__transceivers) > 0:
1350 dtlsTransport = self.__transceivers[0].receiver.transport
1351 bundled = True
1352 elif self.__sctp:
1353 dtlsTransport = self.__sctp.transport
1354 bundled = True
1355 elif self.__configuration.bundlePolicy == RTCBundlePolicy.BALANCED:
1356 transceiver = next(
1357 filter(lambda t: t.kind == kind, self.__transceivers), None
1358 )
1359 if transceiver:
1360 dtlsTransport = transceiver.receiver.transport
1361 bundled = True
1362
1363 if not dtlsTransport:
1364 dtlsTransport = self.__createDtlsTransport()
1365
1366 transceiver = RTCRtpTransceiver(
1367 direction=direction,
1368 kind=kind,
1369 sender=RTCRtpSender(sender_track or kind, dtlsTransport),
1370 receiver=RTCRtpReceiver(kind, dtlsTransport),
1371 )
1372 transceiver.receiver._set_rtcp_ssrc(transceiver.sender._ssrc)
1373 transceiver.sender._stream_id = self.__stream_id
1374 transceiver._bundled = bundled
1375 self.__transceivers.append(transceiver)
1376 return transceiver
1377
1378 def __getTransceiverByMid(self, mid: str) -> Optional[RTCRtpTransceiver]:
1379 return next(filter(lambda x: x.mid == mid, self.__transceivers), None)
1380
1381 def __getTransceiverByMLineIndex(self, index: int) -> Optional[RTCRtpTransceiver]:
1382 return next(
1383 filter(lambda x: x._get_mline_index() == index, self.__transceivers), None
1384 )
1385
1386 def __localDescription(self) -> Optional[sdp.SessionDescription]:
1387 return self.__pendingLocalDescription or self.__currentLocalDescription
1388
1389 def __localRtp(self, transceiver: RTCRtpTransceiver) -> RTCRtpSendParameters:
1390 rtp = RTCRtpSendParameters(
1391 codecs=transceiver._codecs,
1392 headerExtensions=transceiver._headerExtensions,
1393 muxId=transceiver.mid,
1394 )
1395 rtp.rtcp.cname = self.__cname
1396 rtp.rtcp.ssrc = transceiver.sender._ssrc
1397 rtp.rtcp.mux = True
1398 return rtp
1399
1400 def __log_debug(self, msg: str, *args: object) -> None:
1401 logger.debug(f"RTCPeerConnection() {msg}", *args)
1402
1403 def __remoteDescription(self) -> Optional[sdp.SessionDescription]:
1404 return self.__pendingRemoteDescription or self.__currentRemoteDescription
1405
1406 def __remoteRtp(self, transceiver: RTCRtpTransceiver) -> RTCRtpReceiveParameters:
1407 media = self.__remoteDescription().media[transceiver._get_mline_index()]
1408
1409 receiveParameters = RTCRtpReceiveParameters(
1410 codecs=transceiver._codecs,
1411 headerExtensions=transceiver._headerExtensions,
1412 muxId=media.rtp.muxId,
1413 rtcp=media.rtp.rtcp,
1414 )
1415 if len(media.ssrc):
1416 # The retransmission stream is the FID group's second member; a
1417 # remote that also sends FEC lists a third SSRC, so the count of
1418 # SSRCs alone names nothing.
1419 rtx_ssrc = next((group.items[1] for group in media.ssrc_group
1420 if group.semantic == "FID" and len(group.items) == 2
1421 and group.items[0] == media.ssrc[0].ssrc),
1422 media.ssrc[1].ssrc if len(media.ssrc) == 2 else None)
1423 encodings: dict[int, RTCRtpDecodingParameters] = {}
1424 for codec in transceiver._codecs:
1425 if is_rtx(codec):
1426 apt = codec.parameters.get("apt")
1427 if isinstance(apt, int) and apt in encodings and rtx_ssrc is not None:
1428 encodings[apt].rtx = RTCRtpRtxParameters(ssrc=rtx_ssrc)
1429 continue
1430
1431 encodings[codec.payloadType] = RTCRtpDecodingParameters(
1432 ssrc=media.ssrc[0].ssrc, payloadType=codec.payloadType
1433 )
1434 receiveParameters.encodings = list(encodings.values())
1435 return receiveParameters
1436
1437 def __setSignalingState(self, state: str) -> None:
1438 self.__signalingState = state
1439 self.emit("signalingstatechange")
1440
1441 def __updateConnectionState(self) -> None:
1442 # compute new state
1443 # NOTE: we do not have a "disconnected" state
1444 dtlsStates = set(map(lambda x: x.state, self.__dtlsTransports))
1445 iceStates = set(map(lambda x: x.state, self.__iceTransports))
1446 if self.__isClosed:
1447 state = "closed"
1448 elif "failed" in iceStates or "failed" in dtlsStates:
1449 state = "failed"
1450 elif not iceStates.difference(["new", "closed"]) and not dtlsStates.difference(
1451 ["new", "closed"]
1452 ):
1453 state = "new"
1454 elif "checking" in iceStates or "connecting" in dtlsStates:
1455 state = "connecting"
1456 elif "new" in dtlsStates:
1457 # this avoids a spurious connecting -> connected -> connecting
1458 # transition after ICE connects but before DTLS starts
1459 state = "connecting"
1460 else:
1461 state = "connected"
1462
1463 # update state
1464 if state != self.__connectionState:
1465 self.__log_debug("connectionState %s -> %s", self.__connectionState, state)
1466 self.__connectionState = state
1467 self.emit("connectionstatechange")
1468
1469 # if all DTLS connections are closed, initiate a shutdown
1470 if (
1471 not self.__isClosed
1472 and self.__closeTask is None
1473 and dtlsStates == set(["closed"])
1474 ):
1475 self.__closeTask = asyncio.ensure_future(self.close())
1476
1477 def __updateIceConnectionState(self) -> None:
1478 # compute new state
1479 # NOTE: we do not have "connected" or "disconnected" states
1480 states = set(map(lambda x: x.state, self.__iceTransports))
1481 if self.__isClosed:
1482 state = "closed"
1483 elif "failed" in states:
1484 state = "failed"
1485 elif states == set(["completed"]):
1486 state = "completed"
1487 elif "checking" in states:
1488 state = "checking"
1489 else:
1490 state = "new"
1491
1492 # update state
1493 if state != self.__iceConnectionState:
1494 self.__log_debug(
1495 "iceConnectionState %s -> %s", self.__iceConnectionState, state
1496 )
1497 self.__iceConnectionState = state
1498 self.emit("iceconnectionstatechange")
1499
1500 def __updateIceGatheringState(self) -> None:
1501 # compute new state
1502 states = set(map(lambda x: x.iceGatherer.state, self.__iceTransports))
1503 if states == set(["completed"]):
1504 state = "complete"
1505 elif "gathering" in states:
1506 state = "gathering"
1507 else:
1508 state = "new"
1509
1510 # update state
1511 if state != self.__iceGatheringState:
1512 self.__log_debug(
1513 "iceGatheringState %s -> %s", self.__iceGatheringState, state
1514 )
1515 self.__iceGatheringState = state
1516 self.emit("icegatheringstatechange")
1517
1518 def __validate_description(
1519 self, description: sdp.SessionDescription, is_local: bool
1520 ) -> None:
1521 # check description is compatible with signaling state
1522 if is_local:
1523 if description.type == "offer":
1524 if self.signalingState not in ["stable", "have-local-offer"]:
1525 raise InvalidStateError(
1526 "Cannot handle offer in signaling state "
1527 f'"{self.signalingState}"'
1528 )
1529 elif description.type == "answer":
1530 if self.signalingState not in [
1531 "have-remote-offer",
1532 "have-local-pranswer",
1533 ]:
1534 raise InvalidStateError(
1535 "Cannot handle answer in signaling state "
1536 f'"{self.signalingState}"'
1537 )
1538 else:
1539 if description.type == "offer":
1540 if self.signalingState not in ["stable", "have-remote-offer"]:
1541 raise InvalidStateError(
1542 "Cannot handle offer in signaling state "
1543 f'"{self.signalingState}"'
1544 )
1545 elif description.type == "answer":
1546 if self.signalingState not in [
1547 "have-local-offer",
1548 "have-remote-pranswer",
1549 ]:
1550 raise InvalidStateError(
1551 "Cannot handle answer in signaling state "
1552 f'"{self.signalingState}"'
1553 )
1554
1555 for media in description.media:
1556 # A rejected m-line (RFC 3264: port zero) negotiates nothing and
1557 # carries no ICE/DTLS parameters — e.g. Firefox answering a video
1558 # section it has no codec for — so none of these checks apply.
1559 if media.port == 0:
1560 continue
1561
1562 # check ICE credentials were provided
1563 if not media.ice.usernameFragment or not media.ice.password:
1564 raise ValueError("ICE username fragment or password is missing")
1565
1566 # check DTLS role is allowed
1567 if description.type in ["answer", "pranswer"] and media.dtls.role not in [
1568 "client",
1569 "server",
1570 ]:
1571 raise ValueError(
1572 "DTLS setup attribute must be 'active' or 'passive' for an answer"
1573 )
1574
1575 # check RTCP mux is used
1576 if media.kind in ["audio", "video"] and not media.rtcp_mux:
1577 raise ValueError("RTCP mux is not enabled")
1578
1579 # check the number of media section matches
1580 if description.type in ["answer", "pranswer"]:
1581 offer = (
1582 self.__remoteDescription() if is_local else self.__localDescription()
1583 )
1584 offer_media = [(media.kind, media.rtp.muxId) for media in offer.media]
1585 answer_media = [
1586 (media.kind, media.rtp.muxId) for media in description.media
1587 ]
1588 if answer_media != offer_media:
1589 raise ValueError("Media sections in answer do not match offer")