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 binascii
36import datetime
37import enum
38import logging
39import os
40import statistics
41import struct
42import time
43import traceback
44from collections import deque
45from dataclasses import dataclass, field
46from typing import Any, Callable, Dict, Optional, Protocol, Type, TypeVar, Union
47
48import pylibsrtp
49from cryptography import x509
50from cryptography.hazmat.backends import default_backend
51from cryptography.hazmat.primitives import hashes
52from cryptography.hazmat.primitives.asymmetric import ec
53from OpenSSL import SSL
54from pyee.asyncio import AsyncIOEventEmitter
55from pylibsrtp import Policy, Session
56
57from . import clock, rtp
58from .pacer import (
59 BRAKE_LOSS_FRACTION,
60 BRAKE_LOSS_RUNS,
61 CLASS_DC,
62 CLASS_RTCP,
63 CLASS_VIDEO,
64 FRAME_ENDS_MAX,
65 MIN_GOODPUT_SAMPLE_BYTES,
66 RtpPacer,
67)
68from .rtcicetransport import RTCIceTransport
69from .rtcrtpparameters import RTCRtpReceiveParameters, RTCRtpSendParameters
70from .rtp import (
71 AnyRtcpPacket,
72 RtcpByePacket,
73 RtcpPacket,
74 RtcpPsfbPacket,
75 RtcpRrPacket,
76 RtcpRtpfbPacket,
77 pack_twcc_fci,
78 RtcpSrPacket,
79 RtcpXrPacket,
80 RtpPacket,
81 is_rtcp,
82)
83from .stats import RTCStatsReport, RTCTransportStats
84
85CERTIFICATE_T = TypeVar("CERTIFICATE_T", bound="RTCCertificate")
86K = TypeVar("K")
87V = TypeVar("V")
88
89# A gap between two arrivals this long is silence, not delivery.
90TWCC_IDLE_US = 250_000
91# How long a sent packet waits in the transport-cc history for its feedback.
92TWCC_HISTORY_S = 2.0
93# How long the transport-wide sequence numbers transport-cc reported received are kept
94# (`twcc_arrived`): a sender reads from them which frames its peer holds, and a peer on a
95# narrow link with a standing queue reports a packet seconds after it left.
96TWCC_ARRIVED_S = 10.0
97# How far back the least one-way delay is the path's own: short enough that
98# the drift between the two ends' clocks stays a few milliseconds inside it.
99TWCC_DELAY_FLOOR_S = 30.0
100# How fast the path's own delay is let rise by the clock alone: the two ends'
101# clocks drift apart by tens of microseconds a second, a few hundred where one
102# is being slewed, and a queue stands tens of milliseconds past it at once.
103TWCC_DRIFT_S_PER_S = 0.00025
104# What tells a longer path from a queue once one-way delay has stood past the
105# path's own: intervals in a row that lost nothing and sent under this share
106# of what the path delivered while the delay stood, with the least delay of
107# each within the band of the others. A queue the stream filled empties by a
108# tenth of a second every second at such a rate; delay that holds is the path's.
109TWCC_PATH_INTERVALS = 3
110TWCC_PATH_SENT = 0.9
111TWCC_PATH_BAND_MS = 5.0
112TWCC_PATH_LOSS = 0.02
113# How far past the path's own delay one-way delay stands before congestion
114# control reads a queue on it.
115TWCC_QUEUE_MS = 25.0
116# The fewest feedback packets a queue is read over, which a still screen's
117# trickle takes a second or more to send; twice as many read one over the
118# newest half of an interval.
119TWCC_STAND_FEEDBACKS = 4
120# Packets sent within this of the one before belong to one burst, and a burst
121# of fewer packets measures nothing.
122TWCC_BURST_GAP_S = 0.001
123TWCC_BURST_MIN_PACKETS = 4
124# The resolution of transport-cc arrival times.
125TWCC_TICK_US = 250.0
126
127logger = logging.getLogger(__name__)
128
129# Mapping of supported `RTCDtlsFingerprint` algorithms to the
130# corresponding argument for `x509.Certificate.fingerprint`.
131X509_DIGEST_ALGORITHMS = {
132 "sha-256": hashes.SHA256(),
133 "sha-384": hashes.SHA384(),
134 "sha-512": hashes.SHA512(),
135}
136
137
138def burst_delivery_bps(packets: list, probe_bytes: float) -> Optional[float]:
139 """The median rate the wire delivered bursts' leading bytes at.
140
141 A frame's packets are sent back to back, and the pacer lets the first
142 `probe_bytes` of one out at once whenever it holds nothing back, so their
143 arrivals are spread by the slowest hop alone: the bytes after the first
144 over the time from the first arrival to the last is that hop's rate. The
145 rest of a burst may leave at the pace and would measure the pace instead.
146 Arrivals sit on the feedback's grid, so a burst arriving within one step
147 counts as arriving over one.
148
149 Args:
150 packets: `(send instant in seconds, arrival in microseconds, size)`
151 per packet a feedback acknowledged, in send order.
152 probe_bytes: How many leading bytes of a burst leave unpaced.
153
154 Returns:
155 The median of the bursts' rates in bits per second, or None when no
156 burst had `TWCC_BURST_MIN_PACKETS` packets.
157 """
158 rates = []
159 count = 0
160 last_sent = first_size = size_sum = lo = hi = 0.0
161 for sent_at, at_us, size in packets:
162 if not count or sent_at - last_sent > TWCC_BURST_GAP_S:
163 if count >= TWCC_BURST_MIN_PACKETS:
164 rates.append((size_sum - first_size) * 8e6 / max(hi - lo, TWCC_TICK_US))
165 count, first_size, size_sum, lo, hi = 1, size, size, at_us, at_us
166 elif size_sum + size <= probe_bytes:
167 count += 1
168 size_sum += size
169 lo, hi = min(lo, at_us), max(hi, at_us)
170 last_sent = sent_at
171 if count >= TWCC_BURST_MIN_PACKETS:
172 rates.append((size_sum - first_size) * 8e6 / max(hi - lo, TWCC_TICK_US))
173 return statistics.median(rates) if rates else None
174
175
176@dataclass(frozen=True)
177class SRTPProtectionProfile:
178 libsrtp_profile: int
179 openssl_profile: bytes
180 key_length: int
181 salt_length: int
182
183 def get_key_and_salt(self, src: bytes, idx: int) -> bytes:
184 key_start = idx * self.key_length
185 salt_start = 2 * self.key_length + idx * self.salt_length
186 return (
187 src[key_start : key_start + self.key_length]
188 + src[salt_start : salt_start + self.salt_length]
189 )
190
191
192SRTP_AEAD_AES_256_GCM = SRTPProtectionProfile(
193 libsrtp_profile=Policy.SRTP_PROFILE_AEAD_AES_256_GCM,
194 openssl_profile=b"SRTP_AEAD_AES_256_GCM",
195 key_length=32,
196 salt_length=12,
197)
198SRTP_AEAD_AES_128_GCM = SRTPProtectionProfile(
199 libsrtp_profile=Policy.SRTP_PROFILE_AEAD_AES_128_GCM,
200 openssl_profile=b"SRTP_AEAD_AES_128_GCM",
201 key_length=16,
202 salt_length=12,
203)
204SRTP_AES128_CM_SHA1_80 = SRTPProtectionProfile(
205 libsrtp_profile=Policy.SRTP_PROFILE_AES128_CM_SHA1_80,
206 openssl_profile=b"SRTP_AES128_CM_SHA1_80",
207 key_length=16,
208 salt_length=14,
209)
210
211# AES-GCM may not be available depending on how libsrtp2 was built.
212SRTP_PROFILES: list[SRTPProtectionProfile] = []
213for srtp_profile in [
214 SRTP_AEAD_AES_256_GCM,
215 SRTP_AEAD_AES_128_GCM,
216 SRTP_AES128_CM_SHA1_80,
217]:
218 try:
219 Policy(srtp_profile=srtp_profile.libsrtp_profile)
220 except pylibsrtp.Error: # pragma: no cover
221 pass
222 else:
223 SRTP_PROFILES.append(srtp_profile)
224
225
226def certificate_digest(certificate: x509.Certificate, algorithm: str) -> str:
227 hexstring = certificate.fingerprint(X509_DIGEST_ALGORITHMS[algorithm]).hex().upper()
228 return ":".join(hexstring[x : x + 2] for x in range(0, len(hexstring), 2))
229
230
231def generate_certificate(key: ec.EllipticCurvePrivateKey) -> x509.Certificate:
232 name = x509.Name(
233 [
234 x509.NameAttribute(
235 x509.NameOID.COMMON_NAME,
236 binascii.hexlify(os.urandom(16)).decode("ascii"),
237 )
238 ]
239 )
240 now = datetime.datetime.now(tz=datetime.timezone.utc)
241 builder = (
242 x509.CertificateBuilder()
243 .subject_name(name)
244 .issuer_name(name)
245 .public_key(key.public_key())
246 .serial_number(x509.random_serial_number())
247 .not_valid_before(now - datetime.timedelta(days=1))
248 .not_valid_after(now + datetime.timedelta(days=30))
249 )
250 return builder.sign(key, hashes.SHA256(), default_backend())
251
252
253class State(enum.Enum):
254 NEW = 0
255 CONNECTING = 1
256 CONNECTED = 2
257 CLOSED = 3
258 FAILED = 4
259
260
261@dataclass
262class RTCDtlsFingerprint:
263 """
264 The :class:`RTCDtlsFingerprint` dictionary includes the hash function
265 algorithm and certificate fingerprint.
266 """
267
268 algorithm: str
269 "The hash function name, for instance `'sha-256'`."
270
271 value: str
272 "The fingerprint value."
273
274
275class RTCCertificate:
276 """
277 The :class:`RTCCertificate` interface enables the certificates used by an
278 :class:`RTCDtlsTransport`.
279
280 To generate a certificate and the corresponding private key use
281 :func:`generateCertificate`.
282 """
283
284 def __init__(self, key: ec.EllipticCurvePrivateKey, cert: x509.Certificate) -> None:
285 self._key = key
286 self._cert = cert
287
288 @property
289 def expires(self) -> datetime.datetime:
290 """
291 The date and time after which the certificate will be considered invalid.
292 """
293 return self._cert.not_valid_after_utc
294
295 def getFingerprints(self) -> list[RTCDtlsFingerprint]:
296 """
297 Returns the list of certificate fingerprints, one of which is computed
298 with the digest algorithm used in the certificate signature.
299 """
300 return [
301 RTCDtlsFingerprint(
302 algorithm=algorithm,
303 value=certificate_digest(self._cert, algorithm),
304 )
305 for algorithm in X509_DIGEST_ALGORITHMS.keys()
306 ]
307
308 @classmethod
309 def generateCertificate(cls: Type[CERTIFICATE_T]) -> CERTIFICATE_T:
310 """
311 Create and return an X.509 certificate and corresponding private key.
312
313 :rtype: RTCCertificate
314 """
315 key = ec.generate_private_key(ec.SECP256R1(), default_backend())
316 cert = generate_certificate(key)
317 return cls(key=key, cert=cert)
318
319 def _create_ssl_context(
320 self, srtp_profiles: list[SRTPProtectionProfile]
321 ) -> SSL.Context:
322 ctx = SSL.Context(SSL.DTLS_METHOD)
323 ctx.set_verify(
324 SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT, lambda *args: True
325 )
326 ctx.use_certificate(self._cert)
327 ctx.use_privatekey(self._key)
328 ctx.set_cipher_list(
329 b"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA"
330 )
331 ctx.set_tlsext_use_srtp(b":".join(x.openssl_profile for x in srtp_profiles))
332
333 return ctx
334
335
336@dataclass
337class RTCDtlsParameters:
338 """
339 The :class:`RTCDtlsParameters` dictionary includes information relating to
340 DTLS configuration.
341 """
342
343 fingerprints: list[RTCDtlsFingerprint] = field(default_factory=list)
344 "List of :class:`RTCDtlsFingerprint`, one fingerprint for each certificate."
345
346 role: str = "auto"
347 "The DTLS role, with a default of auto."
348
349
350class DataReceiver(Protocol):
351 async def _handle_data(self, data: bytes) -> None: ...
352
353
354class RtpReceiver(Protocol):
355 def _handle_disconnect(self) -> None: ...
356 async def _handle_rtcp_packet(self, packet: AnyRtcpPacket) -> None: ...
357 async def _handle_rtp_packet(
358 self, packet: RtpPacket, arrival_time_ms: int
359 ) -> None: ...
360
361
362class RtpSender(Protocol):
363 _ssrc: int
364
365 @property
366 def kind(self) -> str: ...
367
368 async def _handle_rtcp_packet(self, packet: AnyRtcpPacket) -> None: ...
369
370
371class RtpRouter:
372 """
373 Router to associate RTP/RTCP packets with streams.
374
375 https://tools.ietf.org/html/draft-ietf-mmusic-sdp-bundle-negotiation-53
376 """
377
378 def __init__(self) -> None:
379 self.receivers: set[RtpReceiver] = set()
380 self.senders: dict[int, RtpSender] = {}
381 self.mid_table: dict[str, RtpReceiver] = {}
382 self.ssrc_table: dict[int, RtpReceiver] = {}
383 self.payload_type_table: dict[int, set[RtpReceiver]] = {}
384
385 def register_receiver(
386 self,
387 receiver: RtpReceiver,
388 ssrcs: list[int],
389 payload_types: list[int],
390 mid: Optional[str] = None,
391 ) -> None:
392 self.receivers.add(receiver)
393 if mid is not None:
394 self.mid_table[mid] = receiver
395 for ssrc in ssrcs:
396 self.ssrc_table[ssrc] = receiver
397 for payload_type in payload_types:
398 if payload_type not in self.payload_type_table:
399 self.payload_type_table[payload_type] = set()
400 self.payload_type_table[payload_type].add(receiver)
401
402 def register_sender(self, sender: RtpSender, ssrc: int) -> None:
403 self.senders[ssrc] = sender
404
405 def route_rtcp(self, packet: AnyRtcpPacket) -> set[Union[RtpReceiver, RtpSender]]:
406 recipients: set[Union[RtpReceiver, RtpSender]] = set()
407
408 def add_recipient(recipient: Optional[Union[RtpReceiver, RtpSender]]) -> None:
409 if recipient is not None:
410 recipients.add(recipient)
411
412 # route to RTP receiver
413 if isinstance(packet, RtcpSrPacket):
414 add_recipient(self.ssrc_table.get(packet.ssrc))
415 elif isinstance(packet, RtcpByePacket):
416 for source in packet.sources:
417 add_recipient(self.ssrc_table.get(source))
418
419 # route to RTP sender
420 if isinstance(packet, (RtcpRrPacket, RtcpSrPacket)):
421 for report in packet.reports:
422 add_recipient(self.senders.get(report.ssrc))
423 elif isinstance(packet, (RtcpPsfbPacket, RtcpRtpfbPacket)):
424 # Transport-cc feedback is association-wide (media_ssrc=0), so keying
425 # by media_ssrc alone discards it entirely. Route it to ONE sender
426 # (video first: this is where congestion control input matters).
427 if isinstance(packet, RtcpRtpfbPacket) and packet.fmt == rtp.RTCP_RTPFB_TWCC:
428 if self.senders:
429 add_recipient(min(self.senders.values(),
430 key=lambda s: s.kind != "video"))
431 add_recipient(self.senders.get(packet.media_ssrc))
432
433 # for REMB packets, media_ssrc is always 0, we need to look into the FCI
434 if isinstance(packet, RtcpPsfbPacket) and packet.fmt == rtp.RTCP_PSFB_APP:
435 try:
436 for ssrc in rtp.unpack_remb_fci(packet.fci)[1]:
437 add_recipient(self.senders.get(ssrc))
438 except ValueError:
439 pass
440 elif isinstance(packet, RtcpXrPacket) and packet.rrtr is not None:
441 # A reference time names no media source: every sender answers it.
442 for sender in self.senders.values():
443 add_recipient(sender)
444
445 return recipients
446
447 def route_rtp(self, packet: RtpPacket) -> Optional[RtpReceiver]:
448 ssrc_receiver = self.ssrc_table.get(packet.ssrc)
449 pt_receivers = self.payload_type_table.get(packet.payload_type, set())
450
451 # the SSRC and payload type are known and match
452 if ssrc_receiver is not None and ssrc_receiver in pt_receivers:
453 return ssrc_receiver
454
455 # the SSRC is unknown but the payload type matches, update the SSRC table
456 if ssrc_receiver is None and len(pt_receivers) == 1:
457 pt_receiver = list(pt_receivers)[0]
458 self.ssrc_table[packet.ssrc] = pt_receiver
459 return pt_receiver
460
461 # discard the packet
462 return None
463
464 def unregister_receiver(self, receiver: RtpReceiver) -> None:
465 self.receivers.discard(receiver)
466 self.__discard(self.mid_table, receiver)
467 self.__discard(self.ssrc_table, receiver)
468 for pt, receivers in self.payload_type_table.items():
469 receivers.discard(receiver)
470
471 def unregister_sender(self, sender: RtpSender) -> None:
472 self.__discard(self.senders, sender)
473
474 def __discard(self, d: dict[K, V], value: V) -> None:
475 for k, v in list(d.items()):
476 if v == value:
477 d.pop(k)
478
479
480class RTCDtlsTransport(AsyncIOEventEmitter):
481 """
482 The :class:`RTCDtlsTransport` object includes information relating to
483 Datagram Transport Layer Security (DTLS) transport.
484
485 :param transport: An :class:`RTCIceTransport`.
486 :param certificates: A list of :class:`RTCCertificate` (only one is allowed
487 currently).
488 """
489
490 def __init__(
491 self, transport: RTCIceTransport, certificates: list[RTCCertificate]
492 ) -> None:
493 assert len(certificates) == 1
494 certificate = certificates[0]
495
496 super().__init__()
497 self.encrypted = False
498 self._data_receiver: Optional[DataReceiver] = None
499 self._role = "auto"
500 self._rtp_header_extensions_map = rtp.HeaderExtensionsMap()
501 self._rtp_router = RtpRouter()
502 self._state = State.NEW
503 self._stats_id = "transport_" + str(id(self))
504 self._task: Optional[asyncio.Future[None]] = None
505 self._transport = transport
506
507 # transport-wide congestion control: one sequence-number space shared by
508 # every sender on this transport, a bounded send history for matching the
509 # receiver's feedback, and the latest loss/delay estimate derived from it.
510 self._twcc_seq = 0
511 self._twcc_history: dict[int, tuple[int, float]] = {}
512 self._twcc_missing: dict[int, int] = {}
513 self._twcc_arrived: dict[int, float] = {}
514 self._twcc_window_id = 0
515 self._twcc_pruned_at = 0.0
516 self.twcc_estimate: Optional[dict] = None
517 self._twcc_window = self._twcc_window_zero()
518 self._twcc_reference: Optional[int] = None
519 self._twcc_delay_floor: deque = deque()
520 self._twcc_delay_base: Optional[tuple] = None
521 # The one-way delay of the newest arrival the newest feedback reported, and the
522 # least any reported, on the feedback's clock as `_twcc_delay_base` is; and when
523 # a feedback last reported a packet the wire lost.
524 self._twcc_delay_newest: Optional[float] = None
525 self._twcc_delay_least: Optional[float] = None
526 self._twcc_lost_at = 0.0
527 self._twcc_received_at = 0.0
528 self._twcc_sent_at = 0.0
529 self._twcc_stand: Optional[dict] = None
530 # Receive side of transport-wide congestion control: a sender that
531 # negotiates transport-cc runs its bandwidth estimation on this
532 # feedback alone and starves at its floor bitrate without it.
533 self._twcc_rx_arrivals: dict[int, float] = {}
534 self._twcc_rx_last_ext: Optional[int] = None
535 self._twcc_rx_next: Optional[int] = None
536 self._twcc_rx_last_fb = 0.0
537 self._twcc_rx_fb_count = 0
538
539 # Optional strict-priority packet pacer (SELKIES_WEBRTC_PACER): queues
540 # video behind audio/RTCP/data instead of blasting frames to the wire,
541 # with an IDR-aware budget and GOP-reset recovery. Off unless enabled by
542 # the application with enable_pacer().
543 self._pacer: Optional[RtpPacer] = None
544 # `frame_end` without a pacer: the packets it waits on, by sequence number.
545 self._frame_ends: Dict[int, tuple] = {}
546
547 # counters
548 self.__rx_bytes = 0
549 self.__rx_packets = 0
550 self.__tx_bytes = 0
551 self.__tx_packets = 0
552
553 # SRTP
554 self._rx_srtp: Session = None
555 self._tx_srtp: Session = None
556
557 # SSL
558 self._srtp_profiles = SRTP_PROFILES
559 self._ssl: Optional[SSL.Connection] = None
560 self.__local_certificate = certificate
561
562 @property
563 def state(self) -> str:
564 """
565 The current state of the DTLS transport.
566
567 One of `'new'`, `'connecting'`, `'connected'`, `'closed'`, or `'failed'`.
568 """
569 return str(self._state)[6:].lower()
570
571 @property
572 def transport(self) -> RTCIceTransport:
573 """
574 The associated :class:`RTCIceTransport` instance.
575 """
576 return self._transport
577
578 def getLocalParameters(self) -> RTCDtlsParameters:
579 """
580 Get the local parameters of the DTLS transport.
581
582 :rtype: :class:`RTCDtlsParameters`
583 """
584 return RTCDtlsParameters(
585 fingerprints=self.__local_certificate.getFingerprints()
586 )
587
588 async def _do_handshake(self) -> None:
589 """
590 Attempt to complete the DTLS handshake.
591 """
592 try:
593 while not self.encrypted:
594 try:
595 self._ssl.do_handshake()
596 except SSL.WantReadError:
597 await self._write_ssl()
598 await self._recv_next()
599 except SSL.Error as exc:
600 self.__log_debug("x DTLS handshake failed (error %s)", exc)
601 self._set_state(State.FAILED)
602 return
603 else:
604 self.encrypted = True
605 except ConnectionError:
606 self.__log_debug("x DTLS handshake failed (connection error)")
607 self._set_state(State.FAILED)
608 return
609
610 def _validate_peer_identity(self, remoteParameters: RTCDtlsParameters) -> None:
611 """
612 Check remote fingerprints. There must be at least one fingerprint
613 with a supported algorithm, and all supported fingerprints must
614 match.
615 """
616 certificate = self._ssl.get_peer_certificate(as_cryptography=True)
617 fingerprint_supported = 0
618 fingerprint_valid = 0
619 for f in remoteParameters.fingerprints:
620 algorithm = f.algorithm.lower()
621 if algorithm in X509_DIGEST_ALGORITHMS:
622 fingerprint_supported += 1
623 if f.value.upper() == certificate_digest(certificate, algorithm):
624 fingerprint_valid += 1
625 if not fingerprint_supported or fingerprint_valid != fingerprint_supported:
626 self.__log_debug("x DTLS handshake failed (fingerprint mismatch)")
627 self._set_state(State.FAILED)
628 return
629
630 def _setup_srtp(self) -> None:
631 """
632 Extract the SRTP keying material and setup the SRTP sessions.
633 """
634 openssl_profile = self._ssl.get_selected_srtp_profile()
635 for srtp_profile in self._srtp_profiles:
636 if srtp_profile.openssl_profile == openssl_profile:
637 self.__log_debug(
638 "x DTLS handshake negotiated %s",
639 srtp_profile.openssl_profile.decode(),
640 )
641 break
642 else:
643 self.__log_debug("x DTLS handshake failed (no SRTP profile negotiated)")
644 self._set_state(State.FAILED)
645 return
646 view = self._ssl.export_keying_material(
647 b"EXTRACTOR-dtls_srtp",
648 2 * (srtp_profile.key_length + srtp_profile.salt_length),
649 )
650 if self._role == "server":
651 srtp_tx_key = srtp_profile.get_key_and_salt(view, 1)
652 srtp_rx_key = srtp_profile.get_key_and_salt(view, 0)
653 else:
654 srtp_tx_key = srtp_profile.get_key_and_salt(view, 0)
655 srtp_rx_key = srtp_profile.get_key_and_salt(view, 1)
656
657 rx_policy = Policy(
658 key=srtp_rx_key,
659 ssrc_type=Policy.SSRC_ANY_INBOUND,
660 srtp_profile=srtp_profile.libsrtp_profile,
661 )
662 rx_policy.allow_repeat_tx = True
663 rx_policy.window_size = 1024
664 self._rx_srtp = Session(rx_policy)
665
666 tx_policy = Policy(
667 key=srtp_tx_key,
668 ssrc_type=Policy.SSRC_ANY_OUTBOUND,
669 srtp_profile=srtp_profile.libsrtp_profile,
670 )
671 tx_policy.allow_repeat_tx = True
672 tx_policy.window_size = 1024
673 self._tx_srtp = Session(tx_policy)
674
675 async def start(self, remoteParameters: RTCDtlsParameters) -> None:
676 """
677 Start DTLS transport negotiation with the parameters of the remote
678 DTLS transport.
679
680 :param remoteParameters: An :class:`RTCDtlsParameters`.
681 """
682 assert self._state == State.NEW
683 assert len(remoteParameters.fingerprints)
684
685 # For WebRTC, the DTLS role is explicitly determined as part of the
686 # offer / answer exchange.
687 #
688 # For ORTC however, we determine the DTLS role based on the ICE role.
689 if self._role == "auto":
690 if self.transport.role == "controlling":
691 self._set_role("server")
692 else:
693 self._set_role("client")
694
695 # Initialise SSL.
696 self._ssl = SSL.Connection(
697 self.__local_certificate._create_ssl_context(
698 srtp_profiles=self._srtp_profiles
699 )
700 )
701 if self._role == "server":
702 self._ssl.set_accept_state()
703 else:
704 self._ssl.set_connect_state()
705
706 # Start the DTLS handshake.
707 self._set_state(State.CONNECTING)
708 await self._do_handshake()
709 if self._state == State.FAILED:
710 return
711
712 # Validate the peer identity.
713 self._validate_peer_identity(remoteParameters)
714 if self._state == State.FAILED:
715 return
716
717 # Generate keying material.
718 self._setup_srtp()
719 if self._state == State.FAILED:
720 return
721
722 # start data pump
723 self.__log_debug("- DTLS handshake complete")
724 self._set_state(State.CONNECTED)
725 self._task = asyncio.ensure_future(self.__run())
726
727 async def stop(self) -> None:
728 """
729 Stop and close the DTLS transport.
730 """
731 if self._task is not None:
732 self._task.cancel()
733 self._task = None
734
735 if self._pacer is not None:
736 try:
737 await self._pacer.close()
738 finally:
739 self._pacer = None
740
741 if self._ssl and self._state in [State.CONNECTING, State.CONNECTED]:
742 try:
743 self._ssl.shutdown()
744 except SSL.Error:
745 pass
746 try:
747 await self._write_ssl()
748 except ConnectionError:
749 pass
750 self.__log_debug("- DTLS shutdown complete")
751
752 async def __run(self) -> None:
753 try:
754 while True:
755 await self._recv_next()
756 except ConnectionError:
757 for receiver in self._rtp_router.receivers:
758 receiver._handle_disconnect()
759 except Exception as exc:
760 if not isinstance(exc, asyncio.CancelledError):
761 self.__log_warning(traceback.format_exc())
762 raise exc
763 finally:
764 self._set_state(State.CLOSED)
765
766 def _get_stats(self) -> RTCStatsReport:
767 report = RTCStatsReport()
768 report.add(
769 RTCTransportStats(
770 # RTCStats
771 timestamp=clock.current_datetime(),
772 type="transport",
773 id=self._stats_id,
774 # RTCTransportStats,
775 packetsSent=self.__tx_packets,
776 packetsReceived=self.__rx_packets,
777 bytesSent=self.__tx_bytes,
778 bytesReceived=self.__rx_bytes,
779 iceRole=self.transport.role,
780 dtlsState=self.state,
781 )
782 )
783 return report
784
785 async def _handle_rtcp_data(self, data: bytes) -> None:
786 try:
787 packets = RtcpPacket.parse(data)
788 except (ValueError, struct.error) as exc:
789 self.__log_debug("x RTCP parsing failed: %s", exc)
790 return
791
792 for packet in packets:
793 # route RTCP packet
794 for recipient in self._rtp_router.route_rtcp(packet):
795 await recipient._handle_rtcp_packet(packet)
796
797 async def _handle_rtp_data(self, data: bytes, arrival_time_ms: int) -> None:
798 try:
799 packet = RtpPacket.parse(data, self._rtp_header_extensions_map)
800 except (ValueError, struct.error) as exc:
801 self.__log_debug("x RTP parsing failed: %s", exc)
802 return
803
804 # route RTP packet
805 receiver = self._rtp_router.route_rtp(packet)
806 if receiver is not None:
807 await receiver._handle_rtp_packet(packet, arrival_time_ms=arrival_time_ms)
808
809 async def _recv_next(self) -> None:
810 """Receive a datagram and drain its complete DTLS application records.
811
812 OpenSSL can retain unprocessed records while pending() reports zero.
813 Read until it needs input so a complete SCTP packet never waits for
814 an unrelated later datagram.
815 """
816 # get timeout
817 timeout = None
818 if not self.encrypted:
819 timeout = self._ssl.DTLSv1_get_timeout()
820
821 # receive next datagram
822 if timeout is not None:
823 try:
824 data = await asyncio.wait_for(self.transport._recv(), timeout=timeout)
825 except asyncio.TimeoutError:
826 self.__log_debug("x DTLS handling timeout")
827 self._ssl.DTLSv1_handle_timeout()
828 await self._write_ssl()
829 return
830 else:
831 data = await self.transport._recv()
832
833 self.__rx_bytes += len(data)
834 self.__rx_packets += 1
835
836 first_byte = data[0]
837 if first_byte > 19 and first_byte < 64:
838 # DTLS
839 self._ssl.bio_write(data)
840 while True:
841 try:
842 data = self._ssl.recv(1500)
843 except SSL.ZeroReturnError:
844 data = None
845 except SSL.Error:
846 data = b""
847 await self._write_ssl()
848 if data is None:
849 self.__log_debug("- DTLS shutdown by remote party")
850 raise ConnectionError
851 elif not data:
852 break
853 elif self._data_receiver:
854 await self._data_receiver._handle_data(data)
855 elif first_byte > 127 and first_byte < 192 and self._rx_srtp:
856 # SRTP / SRTCP
857 arrival_time_ms = clock.current_ms()
858 try:
859 if is_rtcp(data):
860 data = self._rx_srtp.unprotect_rtcp(data)
861 await self._handle_rtcp_data(data)
862 else:
863 data = self._rx_srtp.unprotect(data)
864 await self._handle_rtp_data(data, arrival_time_ms=arrival_time_ms)
865 except pylibsrtp.Error as exc:
866 self.__log_debug("x SRTP unprotect failed: %s", exc)
867
868 def _register_data_receiver(self, receiver: DataReceiver) -> None:
869 assert self._data_receiver is None
870 self._data_receiver = receiver
871
872 def _register_rtp_receiver(
873 self, receiver: RtpReceiver, parameters: RTCRtpReceiveParameters
874 ) -> None:
875 ssrcs = set()
876 for encoding in parameters.encodings:
877 ssrcs.add(encoding.ssrc)
878
879 self._rtp_header_extensions_map.configure(parameters)
880 self._rtp_router.register_receiver(
881 receiver,
882 ssrcs=list(ssrcs),
883 payload_types=[codec.payloadType for codec in parameters.codecs],
884 mid=parameters.muxId,
885 )
886
887 def _register_rtp_sender(
888 self, sender: RtpSender, parameters: RTCRtpSendParameters
889 ) -> None:
890 self._rtp_header_extensions_map.configure(parameters)
891 self._rtp_router.register_sender(sender, ssrc=sender._ssrc)
892
893 async def _send_data(self, data: bytes, rtc_class: Optional[int] = None) -> None:
894 if self._state != State.CONNECTED:
895 raise ConnectionError("Cannot send encrypted data, not connected")
896
897 # Data-channel traffic rides the pacer as class DC unless the caller
898 # names one: ahead of queued video, behind audio/RTCP.
899 if self._pacer is not None:
900 await self._pacer.send(data, rtc_class if rtc_class is not None else CLASS_DC)
901 return
902 self._ssl.send(data)
903 await self._write_ssl()
904
905 def pacer_enabled(self) -> bool:
906 return self._pacer is not None
907
908 def set_video_resync(self, resync: Optional[Callable[[Optional[int]], bool]]) -> None:
909 """The video sender's hook for a pacer GOP reset (`RtpPacer._resync`), kept for a
910 pacer enabled later."""
911 self._video_resync = resync
912 if self._pacer is not None:
913 self._pacer._resync = resync
914
915 def enable_pacer(
916 self,
917 encoder_bps: int,
918 request_keyframe: Optional[Callable[[], None]] = None,
919 ) -> None:
920 """Attach the packet pacer to this transport (idempotent)."""
921 if self._pacer is None:
922 async def _send_now_data(payload: bytes) -> None:
923 self._ssl.send(payload)
924 await self._write_ssl()
925 self._pacer = RtpPacer(
926 encoder_bps=encoder_bps,
927 # Bound coroutine fn directly: the RTP hot path cannot afford
928 # a wrapper frame around every packet.
929 send_now=self.transport._send,
930 send_now_data=_send_now_data,
931 request_keyframe=request_keyframe,
932 on_dropped=self._twcc_dropped,
933 resync=getattr(self, "_video_resync", None),
934 )
935 elif request_keyframe is not None:
936 self._pacer._request_keyframe = request_keyframe
937
938 def set_pacer_encoder_bps(self, bps: int) -> None:
939 if self._pacer is not None:
940 self._pacer.set_encoder_bps(bps)
941
942 def note_video_keyframe(self, total_payload_bytes: int, natural: bool = True) -> None:
943 """Video senders report keyframe emissions here so the pacer can keep
944 its queue budget above the (content-variable) IDR size and resurrect
945 the video class after a GOP reset. `natural=False` marks forced
946 recovery keyframes: they resurrect the stream but must not shrink
947 the IDR budget floor."""
948 if self._pacer is not None:
949 self._pacer.note_keyframe(total_payload_bytes, natural)
950
951 def _peer_heard_at(self) -> float:
952 """Monotonic time the peer was last heard from on the ICE path: media, DTLS,
953 SCTP, or one of its consent checks."""
954 return self.transport._connection.last_received
955
956 def _send_delay(self) -> float:
957 """Seconds until a packet handed over now is on the wire."""
958 return self._pacer.drain_s() if self._pacer is not None else 0.0
959
960 def video_backlog(self) -> tuple[int, float, float]:
961 """The frames whose last packet waits in the pacer; how long a packet handed
962 over now waits to reach the peer: what the pacer holds takes to leave, and the
963 queue the newest transport-cc feedback found standing on the path, its newest
964 arrival's delay over the path's own (`_twcc_path_delay`; the least delay
965 reported until the first control interval dates that); and the seconds since
966 a feedback last reported a packet the wire lost."""
967 frames, wait = (0, 0.0) if self._pacer is None else (
968 len(self._pacer.frame_ends), self._pacer.drain_s())
969 base = self._twcc_delay_base[1] if self._twcc_delay_base is not None else self._twcc_delay_least
970 if self._twcc_delay_newest is not None and base is not None:
971 wait += max(0.0, self._twcc_delay_newest - base)
972 return frames, wait, time.monotonic() - self._twcc_lost_at
973
974 def video_room(self) -> Optional[int]:
975 """Bytes of video the pacer queues before it abandons the GOP; None without a pacer."""
976 return None if self._pacer is None else self._pacer.video_room()
977
978 def pacer_snapshot(self) -> Optional[dict]:
979 return self._pacer.snapshot() if self._pacer is not None else None
980
981 def pacer_room_bps(self) -> Optional[float]:
982 """The rate a still screen's cleanup may take through the pacer (`RtpPacer.room_bps`)."""
983 return self._pacer.room_bps() if self._pacer is not None else None
984
985 def set_pacer_frame_floor(self, size: int) -> None:
986 """Let the pacer's video queue take a cleanup frame of `size` bytes whole."""
987 if self._pacer is not None:
988 self._pacer.set_frame_floor(size)
989
990 async def _send_rtp(self, data: bytes, rtc_class: Optional[int] = None,
991 twcc_seq: Optional[int] = None) -> bool:
992 """Protect and send one RTP or RTCP packet; False when the pacer dropped
993 it. `twcc_seq` names the packet in the transport-cc history, so a drop
994 is accounted as one."""
995 if self._state != State.CONNECTED:
996 raise ConnectionError("Cannot send encrypted RTP, not connected")
997
998 if is_rtcp(data):
999 data = self._tx_srtp.protect_rtcp(data)
1000 cls = CLASS_RTCP
1001 else:
1002 data = self._tx_srtp.protect(data)
1003 cls = rtc_class if rtc_class is not None else CLASS_VIDEO
1004 sent = True
1005 if self._pacer is not None:
1006 sent = await self._pacer.send(data, cls, twcc_seq)
1007 else:
1008 await self.transport._send(data)
1009 end = self._frame_ends.pop(twcc_seq, None) if self._frame_ends else None
1010 if end is not None:
1011 end[0](*end[1])
1012 self.__tx_bytes += len(data)
1013 self.__tx_packets += 1
1014 return sent
1015
1016 def frame_end(self, twcc_seq: int, sink: Callable[..., None], *args: Any) -> None:
1017 """Call `sink(*args)` once the RTP packet with transport-wide sequence number
1018 `twcc_seq`, the last of its frame, is on the wire: past the pacer where one
1019 paces, else as it is written to the socket."""
1020 if self._pacer is not None:
1021 self._pacer.frame_end(twcc_seq, sink, *args)
1022 return
1023 ends = self._frame_ends
1024 ends[twcc_seq] = (sink, args)
1025 if len(ends) > FRAME_ENDS_MAX:
1026 del ends[next(iter(ends))]
1027
1028 def forget_frame_end(self, twcc_seq: int) -> None:
1029 """Forget the frame whose last packet, `twcc_seq`, will not be sent."""
1030 (self._pacer.frame_ends if self._pacer is not None else self._frame_ends).pop(twcc_seq, None)
1031
1032 async def _twcc_received(self, seq: int, arrival_ms: float, media_ssrc: int,
1033 rtcp_ssrc: Optional[int]) -> None:
1034 """Record one received packet's transport-wide sequence and answer with
1035 feedback at most every 100 ms, built inline on arrival: while packets
1036 flow the estimator is fed at the cadence browsers use, and when they
1037 stop there is nothing left to report."""
1038 last = self._twcc_rx_last_ext
1039 if last is None:
1040 ext = seq
1041 else:
1042 delta = (seq - last) & 0xFFFF
1043 ext = last + delta - (0x10000 if delta >= 0x8000 else 0)
1044 if ext < 0:
1045 return
1046 if last is None or ext > last:
1047 self._twcc_rx_last_ext = ext
1048 if self._twcc_rx_next is not None and ext < self._twcc_rx_next:
1049 return
1050 self._twcc_rx_arrivals[ext] = arrival_ms
1051 now = time.time()
1052 if now - self._twcc_rx_last_fb < 0.1:
1053 return
1054 base = self._twcc_rx_next
1055 if base is None:
1056 base = min(self._twcc_rx_arrivals)
1057 end = self._twcc_rx_last_ext
1058 if end - base + 1 > 0x7FFF:
1059 base = end - 1024
1060 arrivals = [self._twcc_rx_arrivals.get(i) for i in range(base, end + 1)]
1061 fci = pack_twcc_fci(base, arrivals, self._twcc_rx_fb_count)
1062 if not fci:
1063 return
1064 self._twcc_rx_last_fb = now
1065 self._twcc_rx_fb_count = (self._twcc_rx_fb_count + 1) & 0xFF
1066 for key in [k for k in self._twcc_rx_arrivals if k <= end]:
1067 del self._twcc_rx_arrivals[key]
1068 self._twcc_rx_next = end + 1
1069 packet = RtcpRtpfbPacket(
1070 fmt=rtp.RTCP_RTPFB_TWCC,
1071 ssrc=rtcp_ssrc if rtcp_ssrc is not None else 1,
1072 media_ssrc=media_ssrc,
1073 fci=fci,
1074 )
1075 try:
1076 await self._send_rtp(bytes(packet))
1077 except ConnectionError:
1078 pass
1079
1080 def _twcc_next(self, size: int) -> int:
1081 """Allocate the next transport-wide sequence number and record the packet's
1082 size and send time for matching against the receiver's transport-cc feedback."""
1083 seq = self._twcc_seq
1084 self._twcc_seq = (self._twcc_seq + 1) & 0xFFFF
1085 now = self._twcc_sent_at = time.monotonic()
1086 self._twcc_history.pop(seq, None)
1087 self._twcc_missing.pop(seq, None)
1088 self._twcc_history[seq] = (size, now)
1089 self._twcc_window["bytes_sent"] += size
1090 # Bounded by age, not count: a retransmission storm allocates thousands
1091 # of numbers a second, and one let go before its feedback arrives would
1092 # read as wire loss. Insertion order is allocation order.
1093 if len(self._twcc_history) > 2048 and now - self._twcc_pruned_at > 0.25:
1094 self._twcc_pruned_at = now
1095 for old, (_size, at) in list(self._twcc_history.items()):
1096 if now - at < TWCC_HISTORY_S:
1097 break
1098 del self._twcc_history[old]
1099 self._twcc_missing.pop(old, None)
1100 return seq
1101
1102 def twcc_arrived(self, seq: int) -> bool:
1103 """Whether transport-cc feedback reported the packet sent under `seq` received
1104 within TWCC_ARRIVED_S."""
1105 return seq in self._twcc_arrived
1106
1107 def _twcc_acked(self, seq: int) -> bool:
1108 """Whether transport-cc feedback reported the packet sent under `seq`, sent
1109 within TWCC_HISTORY_S, received."""
1110 return seq not in self._twcc_history
1111
1112 def _twcc_unmark_missing(self, seq: int) -> None:
1113 """Retract a provisional loss only while its control interval is open.
1114
1115 A positive acknowledgment or a local discard resolves the negative.
1116 Intervals already consumed by the controller remain unchanged.
1117 """
1118 if self._twcc_missing.pop(seq, None) == self._twcc_window_id:
1119 self._twcc_window["lost"] -= 1
1120
1121 def _twcc_dropped(self, seq: int) -> None:
1122 """Record that the pacer dropped the packet sent under `seq`, so the
1123 receiver reporting it missing counts as nothing the wire lost."""
1124 if seq in self._twcc_history:
1125 self._twcc_history[seq] = (0, self._twcc_history[seq][1])
1126 self._twcc_unmark_missing(seq)
1127
1128 def _twcc_process_feedback(self, fci: bytes) -> None:
1129 """Decode a transport-cc feedback FCI (draft-holmer-rmcat-transport-wide-cc):
1130 walk the packet-status chunks and receive deltas, join them against the send
1131 history, and publish a loss / throughput / delay estimate.
1132
1133 A missing status is provisional: the packet may still be in transit.
1134 Retain its history until a positive acknowledgment or normal expiry,
1135 and count the negative once. A late positive contributes its bytes
1136 once and retracts a negative only from the current control interval.
1137 Missing markers carry interval numbers rather than retaining drained
1138 windows, and expire or reset with their send-history entries. Unknown
1139 packets and local discards contribute neither loss nor delivery;
1140 feedback containing no new observations cannot steer the pacer.
1141
1142 The deltas chain arrival times: the first is the first arrival's offset
1143 from the feedback's reference time, each later one the gap from the
1144 arrival before, so the interval the bytes were delivered over runs from
1145 the earliest arrival to the latest, and the earliest packet's bytes were
1146 not delivered inside it. The reference time, 64 ms units on the
1147 receiver's clock that wrap at 24 bits, puts every feedback's arrivals on
1148 that one clock, so an arrival less its send time is the packet's one-way
1149 delay plus a constant offset between the two clocks, comparable across
1150 feedback packets; the least of those in a control interval is what
1151 `take_twcc_window` measures a standing queue from. Only arrivals still in the send history take
1152 part: a receiver that moved its window back over a late packet reports
1153 packets it already reported, which would stretch the interval and add
1154 nothing to it. A silence of TWCC_IDLE_US or more between two arrivals
1155 is left out of that interval, with the packet ending it: it is an
1156 outage or an idle sender, and a rate measured across it would say the
1157 wire carries almost nothing. A feedback whose chunks or deltas run
1158 short, or that carries the reserved status symbol, is dropped whole,
1159 so a malformed one consumes no history. Each feedback also tells the
1160 pacer how fast the wire delivered frames' leading bursts
1161 (`burst_delivery_bps`), or that it lost a packet, which sizes the
1162 pacer's burst budget."""
1163 if len(fci) < 8:
1164 return
1165 base_seq, status_count = struct.unpack("!HH", fci[0:4])
1166 reference = int.from_bytes(fci[4:7], "big")
1167 if self._twcc_reference is not None:
1168 step = (reference - self._twcc_reference) & 0xFFFFFF
1169 reference = self._twcc_reference + (step - 0x1000000 if step & 0x800000 else step)
1170 pos = 8
1171 statuses: list[int] = []
1172 while len(statuses) < status_count and pos + 2 <= len(fci):
1173 chunk = struct.unpack("!H", fci[pos : pos + 2])[0]
1174 pos += 2
1175 if chunk & 0x8000 == 0:
1176 symbol = (chunk >> 13) & 0x3
1177 run = chunk & 0x1FFF
1178 statuses.extend([symbol] * run)
1179 elif chunk & 0x4000 == 0:
1180 for i in range(14):
1181 statuses.append((chunk >> (13 - i)) & 0x1)
1182 else:
1183 for i in range(7):
1184 statuses.append((chunk >> (12 - 2 * i)) & 0x3)
1185 statuses = statuses[:status_count]
1186 if len(statuses) < status_count or 3 in statuses:
1187 return
1188
1189 arrivals: list[tuple[int, float]] = []
1190 missing: list[int] = []
1191 at_us = reference * 64_000.0
1192 for i, symbol in enumerate(statuses):
1193 if symbol == 1:
1194 if pos >= len(fci):
1195 return
1196 at_us += fci[pos] * 250
1197 pos += 1
1198 elif symbol == 2:
1199 if pos + 2 > len(fci):
1200 return
1201 at_us += struct.unpack("!h", fci[pos : pos + 2])[0] * 250
1202 pos += 2
1203 else:
1204 missing.append((base_seq + i) & 0xFFFF)
1205 continue
1206 arrivals.append(((base_seq + i) & 0xFFFF, at_us))
1207 if not statuses:
1208 return
1209 self._twcc_reference = reference
1210 received = 0
1211 # A packet the pacer dropped was never on the wire to lose. Loss the
1212 # wire spread through the window says it is short of room; one run of
1213 # it is an outage, which says nothing about the room there is.
1214 lost = runs = 0
1215 previous = None
1216 for seq in missing:
1217 sent = self._twcc_history.get(seq)
1218 if sent is None or sent[0] == 0 or seq in self._twcc_missing:
1219 continue
1220 self._twcc_missing[seq] = self._twcc_window_id
1221 lost += 1
1222 if previous is None or (seq - previous) & 0xFFFF != 1:
1223 runs += 1
1224 previous = seq
1225 # The delivery interval is the time between arrivals of packets in the
1226 # history, less any silence long enough to be an outage or an idle
1227 # sender rather than a rate; the packet opening each stretch was not
1228 # delivered inside it.
1229 bytes_acked = spanned = 0
1230 matched: list[tuple[float, int]] = []
1231 in_order: list[tuple[float, float, int]] = []
1232 delay_min = delay_last = None
1233 now = time.monotonic()
1234 arrived = self._twcc_arrived
1235 for seq, _at in arrivals:
1236 arrived.pop(seq, None)
1237 arrived[seq] = now
1238 while arrived and now - next(iter(arrived.values())) > TWCC_ARRIVED_S:
1239 del arrived[next(iter(arrived))]
1240 for seq, at in arrivals:
1241 sent = self._twcc_history.pop(seq, None)
1242 self._twcc_unmark_missing(seq)
1243 if sent is not None and sent[0] > 0:
1244 received += 1
1245 bytes_acked += sent[0]
1246 window = self._twcc_window
1247 key = (at, seq)
1248 if window["first_arrival"] is None or key < window["first_arrival"]:
1249 window["first_arrival"] = key
1250 window["first_bytes"] = sent[0]
1251 last = window["last_arrival_us"]
1252 window["last_arrival_us"] = at if last is None else max(last, at)
1253 matched.append((at, sent[0]))
1254 in_order.append((sent[1], at, sent[0]))
1255 delay_last = at / 1e6 - sent[1]
1256 if delay_min is None or delay_last < delay_min:
1257 delay_min = delay_last
1258 if not received and not lost:
1259 return
1260 matched.sort(key=lambda m: m[0])
1261 span_us = 0.0
1262 for (t0, _size), (t1, size) in zip(matched, matched[1:]):
1263 if t1 - t0 <= TWCC_IDLE_US:
1264 span_us += t1 - t0
1265 spanned += size
1266 span_s = span_us / 1e6
1267 goodput = int(spanned * 8 / span_s) if span_s > 0 else 0
1268 self.twcc_estimate = {
1269 "received": received,
1270 "lost": lost,
1271 "loss_fraction": lost / max(1, received + lost),
1272 "bytes_acked": bytes_acked,
1273 "recv_span_s": span_s,
1274 "goodput_bps": goodput,
1275 }
1276 if lost:
1277 self._twcc_lost_at = time.monotonic()
1278 if received:
1279 self._twcc_received_at = time.monotonic()
1280 window = self._twcc_window
1281 window["received"] += received
1282 window["lost"] += lost
1283 window["bytes_acked"] += bytes_acked
1284 if delay_min is not None:
1285 if window["delay_min"] is None or delay_min < window["delay_min"]:
1286 window["delay_min"] = delay_min
1287 window["delay_last"] = delay_last
1288 self._twcc_delay_newest = delay_last
1289 if self._twcc_delay_least is None or delay_min < self._twcc_delay_least:
1290 self._twcc_delay_least = delay_min
1291 window["feedback_mins"].append(delay_min)
1292 if self._pacer is not None:
1293 # A brake sizes itself from a rate the wire limited. A window the wire
1294 # delivered whole, or one carrying almost no data, measured the pacer's
1295 # own output instead, which a braked pacer can only ever read back.
1296 if (goodput and spanned >= MIN_GOODPUT_SAMPLE_BYTES
1297 and runs >= BRAKE_LOSS_RUNS and lost >= BRAKE_LOSS_FRACTION * (received + lost)):
1298 self._pacer.set_goodput_bps(goodput)
1299 self._pacer.set_link_bps(
1300 0.0 if lost else burst_delivery_bps(in_order, self._pacer.burst_probe_bytes))
1301 logger.debug(
1302 "TWCC feedback: recv=%d lost=%d goodput=%s bps",
1303 received,
1304 lost,
1305 goodput,
1306 )
1307
1308 @staticmethod
1309 def _twcc_window_zero() -> dict:
1310 return {"received": 0, "lost": 0, "bytes_acked": 0,
1311 "first_arrival": None, "first_bytes": 0, "last_arrival_us": None,
1312 "delay_min": None, "delay_last": None, "feedback_mins": [], "bytes_sent": 0,
1313 "opened": time.monotonic()}
1314
1315 def take_twcc_window(self) -> Optional[dict]:
1316 """Drain the transport-cc feedback accumulated since the last call.
1317
1318 One feedback packet covers a few tens of RTP packets over a few tens of
1319 milliseconds, which is too little to measure either quantity: five lost
1320 out of twenty-two reads as 22.7% loss, and a 17 ms receive span turns a
1321 static screen's trickle into tens of Mbps. A control interval's worth of
1322 feedback is the measurement. Draining also means an interval that
1323 carried no feedback yields nothing to steer from, rather than the last
1324 window to apply again.
1325
1326 Aggregate goodput spans the earliest through the latest unique known
1327 arrival, excluding only the earliest packet's bytes. Gaps between
1328 feedback reports and idle periods count as elapsed time: this is
1329 delivered throughput, not a measure of available capacity. Summing the
1330 spans inside individual reports omits their intervening gaps and makes
1331 batching inflate the rate. Equal-time arrivals use the transport
1332 sequence as a deterministic tie-breaker; late positives can extend the
1333 envelope, while duplicates add nothing. The three envelope fields are
1334 constant-size and reset on drain. Per-report pacer rates keep their
1335 separate idle-exclusion semantics.
1336
1337 The interval's least one-way delay over the path's own
1338 (`_twcc_path_delay`) is the queue that stood through all of it: a key
1339 frame's burst delays the packets behind it for a moment, while a queue
1340 the rate has outgrown delays every one of them. A queue still building
1341 shows before it stands a whole interval as the least delay of the
1342 newest half of its feedback packets standing past the path's own, the
1343 least of a few hundred packets: jitter that spreads one-way delay over
1344 tens of milliseconds, as a Wi-Fi hop does, never lifts all of those,
1345 where a row of rising feedback packets comes from it once in 24
1346 intervals, and the packets behind a burst arrive ever sooner, so a
1347 burst shorter than half an interval never reads that way either. An
1348 interval of fewer than `TWCC_STAND_FEEDBACKS` feedback packets, a still
1349 screen's trickle, reads no queue: jitter lifts the least of one or two
1350 of them past any allowance, and a verdict there would take the
1351 trickle's goodput for the path's capacity. The newest arrival's delay
1352 over that floor is how deep the queue is by the interval's end, which a
1353 growing one has grown into.
1354
1355 Returns:
1356 Loss, goodput and the rate sent over the drained interval, with the
1357 standing queue, the queue still building (None unless the newest
1358 half of the interval stood past `TWCC_QUEUE_MS`), and the depth, in
1359 milliseconds (None without a delay measured or with too few
1360 feedback packets to read one), or None when no feedback arrived in
1361 it.
1362 """
1363 window = self._twcc_window
1364 packets = window["received"] + window["lost"]
1365 if not packets:
1366 return None
1367 self._twcc_window = self._twcc_window_zero()
1368 self._twcc_window_id += 1
1369 queue_ms = rising_ms = depth_ms = None
1370 now = time.monotonic()
1371 first = window["first_arrival"]
1372 span_s = (window["last_arrival_us"] - first[0]) / 1e6 if first is not None else 0.0
1373 goodput_bps = (int((window["bytes_acked"] - window["first_bytes"]) * 8 / span_s)
1374 if span_s > 0 else 0)
1375 sent_bps = (int(window["bytes_sent"] * 8 / (now - window["opened"]))
1376 if now > window["opened"] else 0)
1377 if window["delay_min"] is not None:
1378 mins = window["feedback_mins"]
1379 least = self._twcc_path_delay(
1380 now, window["delay_min"], window["lost"] / packets, sent_bps, goodput_bps,
1381 len(mins) >= TWCC_STAND_FEEDBACKS)
1382 if len(mins) >= TWCC_STAND_FEEDBACKS:
1383 queue_ms = (window["delay_min"] - least) * 1000.0
1384 depth_ms = (window["delay_last"] - least) * 1000.0
1385 if len(mins) >= 2 * TWCC_STAND_FEEDBACKS:
1386 newest_ms = (min(mins[len(mins) // 2:]) - least) * 1000.0
1387 if newest_ms > TWCC_QUEUE_MS:
1388 rising_ms = newest_ms
1389 return {
1390 "received": window["received"],
1391 "lost": window["lost"],
1392 "loss_fraction": window["lost"] / packets,
1393 "bytes_acked": window["bytes_acked"],
1394 "goodput_bps": goodput_bps,
1395 "queue_ms": queue_ms,
1396 "queue_rising_ms": rising_ms,
1397 "queue_depth_ms": depth_ms,
1398 "sent_bps": sent_bps,
1399 }
1400
1401 def _twcc_path_delay(self, now: float, delay: float, loss: float,
1402 sent_bps: int, goodput_bps: int, measured: bool) -> float:
1403 """The path's own one-way delay, in seconds on the feedback's clock,
1404 after an interval whose least delay was `delay`.
1405
1406 It is the least delay seen, let rise only as fast as the two ends'
1407 clocks drift apart (`TWCC_DRIFT_S_PER_S`): the least of the last
1408 `TWCC_DELAY_FLOOR_S` alone takes a queue that stands that long for the
1409 path, and reads nothing of it from then on, while the loss it overflows
1410 into goes on. Delay standing `TWCC_QUEUE_MS` past it is then a queue or
1411 a longer path, and what the stream does tells them apart: over
1412 `TWCC_PATH_INTERVALS` intervals in a row that lost nothing and sent
1413 under `TWCC_PATH_SENT` of the most the path delivered while the delay
1414 stood, a queue the stream filled drains by a tenth of a second each,
1415 so delay that held within `TWCC_PATH_BAND_MS` through them is the
1416 path's and becomes its own from there. A rate cut answering the first
1417 reading provides those intervals, so a route change costs the few
1418 cuts until they have passed. Where nothing cuts the rate, or the
1419 delay jitters past the band, delay that has stood with nothing lost for
1420 `TWCC_DELAY_FLOOR_S` gives way to the least of that time, as before. An interval of too few feedback packets to
1421 read a queue from (`measured` unset) can only lower it.
1422 """
1423 floor = self._twcc_delay_floor
1424 floor.append((now, delay))
1425 while floor[0][0] < now - TWCC_DELAY_FLOOR_S:
1426 floor.popleft()
1427 if self._twcc_delay_base is None:
1428 base = delay
1429 else:
1430 at, base = self._twcc_delay_base
1431 base = min(delay, base + TWCC_DRIFT_S_PER_S * max(now - at, 0.0))
1432 band = TWCC_PATH_BAND_MS / 1000.0
1433 if (delay - base) * 1000.0 <= TWCC_QUEUE_MS:
1434 self._twcc_stand = None
1435 elif measured:
1436 stand = self._twcc_stand
1437 if stand is None:
1438 stand = self._twcc_stand = {"delivered": 0, "quiet": [], "lossless": now}
1439 stand["delivered"] = max(stand["delivered"], goodput_bps)
1440 lossless = loss <= TWCC_PATH_LOSS
1441 if lossless and sent_bps <= stand["delivered"] * TWCC_PATH_SENT:
1442 stand["quiet"].append(delay)
1443 else:
1444 stand["quiet"].clear()
1445 if not lossless:
1446 stand["lossless"] = now
1447 quiet = stand["quiet"][-TWCC_PATH_INTERVALS:]
1448 if len(quiet) == TWCC_PATH_INTERVALS and max(quiet) - min(quiet) <= band:
1449 base = min(quiet)
1450 self._twcc_stand = None
1451 elif now - stand["lossless"] >= TWCC_DELAY_FLOOR_S:
1452 base = min(d for _, d in floor)
1453 self._twcc_stand = None
1454 self._twcc_delay_base = (now, base)
1455 return base
1456
1457 def _set_role(self, role: str) -> None:
1458 self._role = role
1459
1460 def _set_state(self, state: State) -> None:
1461 if state != self._state:
1462 self.__log_debug("- %s -> %s", self._state, state)
1463 self._state = state
1464 self.emit("statechange")
1465
1466 def _unregister_data_receiver(self, receiver: DataReceiver) -> None:
1467 if self._data_receiver == receiver:
1468 self._data_receiver = None
1469
1470 def _unregister_rtp_receiver(self, receiver: RtpReceiver) -> None:
1471 self._rtp_router.unregister_receiver(receiver)
1472
1473 def _unregister_rtp_sender(self, sender: RtpSender) -> None:
1474 self._rtp_router.unregister_sender(sender)
1475
1476 async def _write_ssl(self) -> None:
1477 """
1478 Flush outgoing data which OpenSSL put in our BIO to the transport.
1479 """
1480 try:
1481 data = self._ssl.bio_read(1500)
1482 except SSL.Error:
1483 data = b""
1484 if data:
1485 await self.transport._send(data)
1486 self.__tx_bytes += len(data)
1487 self.__tx_packets += 1
1488
1489 def __log_debug(self, msg: str, *args: object) -> None:
1490 logger.debug(f"RTCDtlsTransport(%s) {msg}", self._role, *args)
1491
1492 def __log_warning(self, msg: str, *args: object) -> None:
1493 logger.warning(f"RTCDtlsTransport(%s) {msg}", self._role, *args)