Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/selkies/webrtc/rtp.py: 54%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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.
34import math
35import os
36import struct
37from collections import deque
38from dataclasses import dataclass, field
39from struct import pack, unpack, unpack_from
40from typing import Any, Iterable, Optional, Union
42from .rtcrtpparameters import RTCRtpParameters
44# Receive side: how far back a NACK reaches.
45RTP_HISTORY_SIZE = 512
46# Send side: packets are kept for retransmission for at least this long. A
47# NACK reaches the sender about a round trip after the loss, and a count of
48# packets covers a shrinking slice of the stream as the bitrate rises. The
49# packet cap bounds what a burst can hold.
50RTP_HISTORY_S = 1.0
51RTP_HISTORY_MAX_PACKETS = 9600
53# reserved to avoid confusion with RTCP
54FORBIDDEN_PAYLOAD_TYPES = range(72, 77)
55DYNAMIC_PAYLOAD_TYPES = range(96, 128)
56# Unassigned by RFC 3551 and taken as dynamic by browsers once 96-127 run out (Chromium's red is 63).
57UNASSIGNED_PAYLOAD_TYPES = range(35, 64)
59RTP_HEADER_LENGTH = 12
60RTCP_HEADER_LENGTH = 4
62PACKETS_LOST_MIN = -(1 << 23)
63PACKETS_LOST_MAX = (1 << 23) - 1
65RTCP_SR = 200
66RTCP_RR = 201
67RTCP_SDES = 202
68RTCP_BYE = 203
69RTCP_RTPFB = 205
70RTCP_PSFB = 206
71RTCP_XR = 207
73RTCP_RTPFB_NACK = 1
74RTCP_RTPFB_TWCC = 15
76RTCP_PSFB_PLI = 1
77RTCP_PSFB_SLI = 2
78RTCP_PSFB_RPSI = 3
79RTCP_PSFB_FIR = 4
80RTCP_PSFB_APP = 15
82RTCP_XR_RRTR = 4
83RTCP_XR_DLRR = 5
85# The RTP Dependency Descriptor of the AV1 RTP specification, written on every packet of a
86# video stream whose encoder tracks its references: the frame the packet belongs to, its
87# edges, and the frame that one predicts from. A receiver reads a frame's dependency from it
88# instead of chaining every frame to its predecessor, so a frame predicting past a lost one
89# decodes as soon as it arrives.
90DEPENDENCY_DESCRIPTOR_URI = "https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension"
92# When a frame was captured, in the NTP clock the sender reports: 64-bit UQ32.32, the
93# estimated capture clock offset left out, as the sender is the capture system.
94ABS_CAPTURE_TIME_URI = "http://www.webrtc.org/experiments/rtp-hdrext/abs-capture-time"
97@dataclass
98class HeaderExtensions:
99 abs_send_time: Optional[int] = None
100 audio_level: Any = None
101 mid: Any = None
102 repaired_rtp_stream_id: Any = None
103 rtp_stream_id: Any = None
104 transmission_offset: Optional[int] = None
105 transport_sequence_number: Optional[int] = None
106 playout_delay: Any = None
107 # (flags, encode_start, encode_finish, packetization_complete, pacer_exit,
108 # network_ts, network2_ts) — u8 + 6x u16 ms deltas from the capture timestamp.
109 video_timing: Any = None
110 # (primaries, transfer, matrix, range) as the ITU-T H.273 codes; range is
111 # 1 limited, 2 full. Chroma siting is left unspecified.
112 color_space: Any = None
113 # The packet's dependency descriptor, packed as `dependency_descriptor` builds it.
114 dependency_descriptor: Any = None
115 # The capture instant of the packet's frame as a 64-bit NTP timestamp.
116 abs_capture_time: Optional[int] = None
119class HeaderExtensionsMap:
120 def __init__(self) -> None:
121 self.__ids = HeaderExtensions()
122 self.__mutable: dict[int, int] = {}
124 def configure(self, parameters: RTCRtpParameters) -> None:
125 for ext in parameters.headerExtensions:
126 if ext.uri == "urn:ietf:params:rtp-hdrext:sdes:mid":
127 self.__ids.mid = ext.id
128 elif ext.uri == "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id":
129 self.__ids.repaired_rtp_stream_id = ext.id
130 elif ext.uri == "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id":
131 self.__ids.rtp_stream_id = ext.id
132 elif (
133 ext.uri == "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time"
134 ):
135 self.__ids.abs_send_time = ext.id
136 elif ext.uri == "urn:ietf:params:rtp-hdrext:toffset":
137 self.__ids.transmission_offset = ext.id
138 elif ext.uri == "urn:ietf:params:rtp-hdrext:ssrc-audio-level":
139 self.__ids.audio_level = ext.id
140 elif (
141 ext.uri
142 == "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01"
143 ):
144 self.__ids.transport_sequence_number = ext.id
145 elif ext.uri == "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay":
146 self.__ids.playout_delay = ext.id
147 elif ext.uri == "http://www.webrtc.org/experiments/rtp-hdrext/video-timing":
148 self.__ids.video_timing = ext.id
149 elif ext.uri == "http://www.webrtc.org/experiments/rtp-hdrext/color-space":
150 self.__ids.color_space = ext.id
151 elif ext.uri == DEPENDENCY_DESCRIPTOR_URI:
152 self.__ids.dependency_descriptor = ext.id
153 elif ext.uri == ABS_CAPTURE_TIME_URI:
154 self.__ids.abs_capture_time = ext.id
155 # What `for_fec` zeroes: each mutable extension's ID and the byte its mutable part starts at.
156 self.__mutable = {
157 ext_id: first
158 for ext_id, first in (
159 (self.__ids.abs_send_time, 0),
160 (self.__ids.transmission_offset, 0),
161 (self.__ids.transport_sequence_number, 0),
162 (self.__ids.video_timing, 7),
163 )
164 if ext_id
165 }
167 def has_dependency_descriptor(self) -> bool:
168 """Whether the peer negotiated the dependency descriptor."""
169 return bool(self.__ids.dependency_descriptor)
171 def get(self, extension_profile: int, extension_value: bytes) -> HeaderExtensions:
172 values = HeaderExtensions()
173 for x_id, x_value in unpack_header_extensions(
174 extension_profile, extension_value
175 ):
176 if x_id == self.__ids.mid:
177 values.mid = x_value.decode("utf8")
178 elif x_id == self.__ids.repaired_rtp_stream_id:
179 values.repaired_rtp_stream_id = x_value.decode("ascii")
180 elif x_id == self.__ids.rtp_stream_id:
181 values.rtp_stream_id = x_value.decode("ascii")
182 elif x_id == self.__ids.abs_send_time:
183 if len(x_value) < 3:
184 # Malformed length: skip rather than raise struct.error.
185 continue
186 values.abs_send_time = unpack("!L", b"\00" + x_value[:3])[0]
187 elif x_id == self.__ids.transmission_offset:
188 if len(x_value) < 3:
189 # Malformed length: skip rather than raise struct.error.
190 continue
191 values.transmission_offset = unpack("!l", x_value[:3] + b"\00")[0] >> 8
192 elif x_id == self.__ids.audio_level:
193 if len(x_value) < 1:
194 # Malformed length: skip rather than raise struct.error.
195 continue
196 vad_level = unpack("!B", x_value[:1])[0]
197 values.audio_level = (vad_level & 0x80 == 0x80, vad_level & 0x7F)
198 elif x_id == self.__ids.transport_sequence_number:
199 values.transport_sequence_number = unpack("!H", x_value)[0]
200 elif x_id == self.__ids.playout_delay:
201 if len(x_value) < 3:
202 # Malformed length: skip rather than raise struct.error.
203 continue
204 byte1, byte2, byte3 = struct.unpack('!BBB', x_value[:3])
205 min_value = (byte1 << 4 | byte2 >> 4)
206 max_value = ((byte2 & 0x0F) << 8 | byte3)
207 values.playout_delay = (min_value, max_value)
208 elif x_id == self.__ids.video_timing:
209 if len(x_value) < 13:
210 # Malformed length: skip rather than raise struct.error.
211 continue
212 values.video_timing = unpack("!BHHHHHH", x_value[:13])
213 elif x_id == self.__ids.color_space:
214 if len(x_value) < 4:
215 # Malformed length: skip rather than raise struct.error.
216 continue
217 primaries, transfer, matrix, siting = unpack("!BBBB", x_value[:4])
218 values.color_space = (primaries, transfer, matrix, (siting >> 4) & 0x03)
219 elif x_id == self.__ids.dependency_descriptor:
220 values.dependency_descriptor = bytes(x_value)
221 elif x_id == self.__ids.abs_capture_time:
222 if len(x_value) < 8:
223 # Malformed length: skip rather than raise struct.error.
224 continue
225 values.abs_capture_time = unpack("!Q", x_value[:8])[0]
226 return values
228 def for_fec(self, packet: bytes) -> bytes:
229 """Copy an RTP packet for FlexFEC with mutable extension values zeroed.
231 libwebrtc zeroes these values on received media before XOR recovery
232 (`RtpPacket::ZeroMutableExtensions`). With unequal header lengths, as
233 video timing on a frame's last packet makes them, an unnormalized value
234 XORs into another packet's payload. Transport-wide sequence, absolute
235 send time, and transmission offset are zeroed whole, and video timing
236 from its pacer-exit leg (byte 7) on; the packet sent and kept for
237 retransmission is left as it is. Both RFC 8285 forms are walked as
238 libwebrtc parses them, and the walk ends where its parser does: at a
239 one-byte ID of 15, or of 0 with a length, and at an element running
240 past the block. A packet whose header does not parse comes back as it
241 is, as the receiver drops it, so nothing raises into the RTP task.
242 """
243 if len(packet) < 12 or not packet[0] & 0x10:
244 return packet
245 start = 12 + 4 * (packet[0] & 15)
246 if start + 4 > len(packet):
247 return packet
248 profile, words = unpack_from("!HH", packet, start)
249 pos, end = start + 4, start + 4 + 4 * words
250 if end > len(packet):
251 return packet
252 if profile != 0xBEDE and profile & 0xFFF0 != 0x1000:
253 return packet
254 mutable = self.__mutable
255 result = bytearray(packet)
256 while pos < end:
257 head = packet[pos]
258 pos += 1
259 if head == 0:
260 continue
261 if profile == 0xBEDE:
262 ext_id, length = head >> 4, (head & 15) + 1
263 if ext_id in (0, 15):
264 break
265 else:
266 ext_id = head
267 if pos >= end:
268 break
269 length = packet[pos]
270 pos += 1
271 if pos + length > end:
272 break
273 skip = mutable.get(ext_id, length)
274 if skip < length:
275 result[pos + skip:pos + length] = bytes(length - skip)
276 pos += length
277 return bytes(result)
279 def set(self, values: HeaderExtensions) -> tuple[int, bytes]:
280 extensions = []
281 if values.mid is not None and self.__ids.mid:
282 extensions.append((self.__ids.mid, values.mid.encode("utf8")))
283 if (
284 values.repaired_rtp_stream_id is not None
285 and self.__ids.repaired_rtp_stream_id
286 ):
287 extensions.append(
288 (
289 self.__ids.repaired_rtp_stream_id,
290 values.repaired_rtp_stream_id.encode("ascii"),
291 )
292 )
293 if values.rtp_stream_id is not None and self.__ids.rtp_stream_id:
294 extensions.append(
295 (self.__ids.rtp_stream_id, values.rtp_stream_id.encode("ascii"))
296 )
297 if values.abs_send_time is not None and self.__ids.abs_send_time:
298 extensions.append(
299 (self.__ids.abs_send_time, pack("!L", values.abs_send_time)[1:])
300 )
301 if values.transmission_offset is not None and self.__ids.transmission_offset:
302 extensions.append(
303 (
304 self.__ids.transmission_offset,
305 pack("!l", values.transmission_offset << 8)[0:2],
306 )
307 )
308 if values.audio_level is not None and self.__ids.audio_level:
309 extensions.append(
310 (
311 self.__ids.audio_level,
312 pack(
313 "!B",
314 (0x80 if values.audio_level[0] else 0)
315 | (values.audio_level[1] & 0x7F),
316 ),
317 )
318 )
319 if (
320 values.transport_sequence_number is not None
321 and self.__ids.transport_sequence_number
322 ):
323 extensions.append(
324 (
325 self.__ids.transport_sequence_number,
326 pack("!H", values.transport_sequence_number),
327 )
328 )
329 if values.playout_delay is not None and self.__ids.playout_delay:
330 extensions.append(
331 (
332 self.__ids.playout_delay,
333 pack(
334 "!BBB",
335 values.playout_delay[0] >> 4,
336 (((values.playout_delay[0] & 0x0F) << 4) | values.playout_delay[1] >> 8),
337 values.playout_delay[1] & 0xFF
338 ),
339 )
340 )
341 if values.video_timing is not None and self.__ids.video_timing:
342 extensions.append(
343 (self.__ids.video_timing, pack("!BHHHHHH", *values.video_timing))
344 )
345 if values.color_space is not None and self.__ids.color_space:
346 primaries, transfer, matrix, color_range = values.color_space
347 extensions.append(
348 (
349 self.__ids.color_space,
350 pack("!BBBB", primaries, transfer, matrix, (color_range & 0x03) << 4),
351 )
352 )
353 if values.dependency_descriptor is not None and self.__ids.dependency_descriptor:
354 extensions.append((self.__ids.dependency_descriptor, values.dependency_descriptor))
355 if values.abs_capture_time is not None and self.__ids.abs_capture_time:
356 extensions.append((self.__ids.abs_capture_time, pack("!Q", values.abs_capture_time)))
357 return pack_header_extensions(extensions)
360# The dependency structure every stream declares, as the specification's
361# template_dependency_structure: one decode target and two frame templates, a key frame that
362# predicts from nothing and a frame predicting from the one before it, both switch points.
363# 28 bits: structure id 0, one decode target, the second template on the same layer and no
364# more templates, both templates' indications, the templates' frame diffs (none, then one),
365# no chains, and no resolutions.
366DEPENDENCY_STRUCTURE = int("000000" "00000" "00" "11" "10" "10" "0" "1" "0000" "0" "0" "0", 2)
369def dependency_descriptor(first: bool, last: bool, frame_number: int,
370 fdiff: Optional[int], keyframe: bool) -> bytes:
371 """The dependency descriptor of one packet: whether it opens and closes its frame, the
372 frame's number, and how many frames back the frame predicts from -- None for a frame that
373 predicts from nothing. A frame one back rides the predicted-frame template alone and a
374 key frame the other, so most packets carry the three mandatory bytes; a frame further
375 back writes its own diff, and a key frame's first packet the structure the receiver
376 reads the stream by."""
377 bits = (first << 23) | (last << 22) | ((0 if fdiff is None else 1) << 16) | (frame_number & 0xFFFF)
378 count = 24
379 structure = keyframe and first
380 custom = fdiff is not None and fdiff != 1
381 if not structure and not custom:
382 return bits.to_bytes(3, "big")
383 bits = (bits << 5) | (structure << 4) | (custom << 1)
384 count += 5
385 if structure:
386 bits = (bits << 28) | DEPENDENCY_STRUCTURE
387 count += 28
388 if custom:
389 size = 1 if fdiff <= 16 else 2 if fdiff <= 256 else 3
390 bits = (((bits << 2) | size) << (4 * size) | (fdiff - 1)) << 2
391 count += 4 + 4 * size
392 padding = -count % 8
393 return (bits << padding).to_bytes((count + padding) // 8, "big")
396def clamp_packets_lost(count: int) -> int:
397 return max(PACKETS_LOST_MIN, min(count, PACKETS_LOST_MAX))
400def pack_packets_lost(count: int) -> bytes:
401 return pack("!l", count)[1:]
404def unpack_packets_lost(d: bytes) -> int:
405 if d[0] & 0x80:
406 d = b"\xff" + d
407 else:
408 d = b"\x00" + d
409 return unpack("!l", d)[0]
412def pack_rtcp_packet(packet_type: int, count: int, payload: bytes) -> bytes:
413 assert len(payload) % 4 == 0
414 return pack("!BBH", (2 << 6) | count, packet_type, len(payload) // 4) + payload
417def pack_remb_fci(bitrate: int, ssrcs: list[int]) -> bytes:
418 """
419 Pack the FCI for a Receiver Estimated Maximum Bitrate report.
421 https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03
422 """
423 data = b"REMB"
424 exponent = 0
425 mantissa = bitrate
426 while mantissa > 0x3FFFF:
427 mantissa >>= 1
428 exponent += 1
429 data += pack(
430 "!BBH", len(ssrcs), (exponent << 2) | (mantissa >> 16), (mantissa & 0xFFFF)
431 )
432 for ssrc in ssrcs:
433 data += pack("!L", ssrc)
434 return data
437def pack_twcc_fci(base_seq: int, arrivals: list, fb_pkt_count: int) -> bytes:
438 """Encode one transport-wide congestion control feedback FCI
439 (draft-holmer-rmcat-transport-wide-cc-extensions-01).
441 `arrivals[i]` is the arrival time in milliseconds of transport-wide
442 sequence `base_seq + i`, or None for one not received. Statuses are all
443 two-bit status-vector chunks (one encoding, correct by inspection);
444 deltas accumulate in quantized 0.25 ms steps so rounding never drifts.
446 Returns:
447 The FCI padded to a 32-bit boundary.
448 """
449 first = next((t for t in arrivals if t is not None), None)
450 if first is None:
451 return b""
452 # Mask only the wire field: masked-epoch deltas overflow into "not received".
453 ref_units = int(first // 64)
454 prev = ref_units * 64.0
455 symbols = []
456 deltas = b""
457 for t in arrivals:
458 if t is None:
459 symbols.append(0)
460 continue
461 du = round((t - prev) * 4)
462 if 0 <= du <= 0xFF:
463 symbols.append(1)
464 deltas += pack("!B", du)
465 elif -0x8000 <= du <= 0x7FFF:
466 symbols.append(2)
467 deltas += pack("!h", du)
468 else:
469 symbols.append(0)
470 continue
471 prev += du / 4.0
472 chunks = b""
473 for i in range(0, len(symbols), 7):
474 group = symbols[i : i + 7]
475 group += [0] * (7 - len(group))
476 value = 0xC000
477 for j, symbol in enumerate(group):
478 value |= symbol << (12 - 2 * j)
479 chunks += pack("!H", value)
480 fci = (
481 pack("!HH", base_seq & 0xFFFF, len(arrivals))
482 + pack("!I", ((ref_units & 0xFFFFFF) << 8) | (fb_pkt_count & 0xFF))
483 + chunks
484 + deltas
485 )
486 while len(fci) % 4:
487 fci += b"\x00"
488 return fci
491def unpack_remb_fci(data: bytes) -> tuple[int, list[int]]:
492 """
493 Unpack the FCI for a Receiver Estimated Maximum Bitrate report.
495 https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03
496 """
497 if len(data) < 8 or data[0:4] != b"REMB":
498 raise ValueError("Invalid REMB prefix")
499 if len(data) < 8 + 4 * data[4]:
500 # The declared SSRC count exceeds the buffer.
501 raise ValueError("Invalid REMB length")
503 exponent = (data[5] & 0xFC) >> 2
504 mantissa = ((data[5] & 0x03) << 16) | (data[6] << 8) | data[7]
505 bitrate = mantissa << exponent
507 pos = 8
508 ssrcs = []
509 for r in range(data[4]):
510 ssrcs.append(unpack_from("!L", data, pos)[0])
511 pos += 4
513 return (bitrate, ssrcs)
516def is_rtcp(msg: bytes) -> bool:
517 return len(msg) >= 2 and msg[1] >= 192 and msg[1] <= 208
520def padl(length: int) -> int:
521 """
522 Return amount of padding needed for a 4-byte multiple.
523 """
524 return 4 * ((length + 3) // 4) - length
527def unpack_header_extensions(
528 extension_profile: int, extension_value: bytes
529) -> list[tuple[int, bytes]]:
530 """
531 Parse header extensions according to RFC 5285.
532 """
533 extensions = []
534 pos = 0
536 if extension_profile == 0xBEDE:
537 # One-Byte Header
538 while pos < len(extension_value):
539 # skip padding byte
540 if extension_value[pos] == 0:
541 pos += 1
542 continue
544 x_id = (extension_value[pos] & 0xF0) >> 4
545 x_length = (extension_value[pos] & 0x0F) + 1
546 pos += 1
548 if len(extension_value) < pos + x_length:
549 raise ValueError("RTP one-byte header extension value is truncated")
550 x_value = extension_value[pos : pos + x_length]
551 extensions.append((x_id, x_value))
552 pos += x_length
553 elif extension_profile == 0x1000:
554 # Two-Byte Header
555 while pos < len(extension_value):
556 # skip padding byte
557 if extension_value[pos] == 0:
558 pos += 1
559 continue
561 if len(extension_value) < pos + 2:
562 raise ValueError("RTP two-byte header extension is truncated")
563 x_id, x_length = unpack_from("!BB", extension_value, pos)
564 pos += 2
566 if len(extension_value) < pos + x_length:
567 raise ValueError("RTP two-byte header extension value is truncated")
568 x_value = extension_value[pos : pos + x_length]
569 extensions.append((x_id, x_value))
570 pos += x_length
572 return extensions
575def pack_header_extensions(extensions: list[tuple[int, bytes]]) -> tuple[int, bytes]:
576 """
577 Serialize header extensions according to RFC 5285.
578 """
579 extension_profile = 0
580 extension_value = b""
582 if not extensions:
583 return extension_profile, extension_value
585 one_byte = True
586 for x_id, x_value in extensions:
587 x_length = len(x_value)
588 assert x_id > 0 and x_id < 256
589 assert x_length >= 0 and x_length < 256
590 if x_id > 14 or x_length == 0 or x_length > 16:
591 one_byte = False
593 if one_byte:
594 # One-Byte Header
595 extension_profile = 0xBEDE
596 extension_value = b""
597 for x_id, x_value in extensions:
598 x_length = len(x_value)
599 extension_value += pack("!B", (x_id << 4) | (x_length - 1))
600 extension_value += x_value
601 else:
602 # Two-Byte Header
603 extension_profile = 0x1000
604 extension_value = b""
605 for x_id, x_value in extensions:
606 x_length = len(x_value)
607 extension_value += pack("!BB", x_id, x_length)
608 extension_value += x_value
610 extension_value += b"\x00" * padl(len(extension_value))
611 return extension_profile, extension_value
614def compute_audio_level_dbov(frame: Any) -> int:
615 """
616 Compute the energy level as spelled out in RFC 6465, Appendix A.
617 """
618 MAX_SAMPLE_VALUE = 32767
619 MAX_AUDIO_LEVEL = 0
620 MIN_AUDIO_LEVEL = -127
621 rms = 0.0
622 buf = bytes(frame.planes[0])
623 s = struct.Struct("h")
624 for unpacked in s.iter_unpack(buf):
625 sample = unpacked[0]
626 rms += sample * sample
627 rms = math.sqrt(rms / (frame.samples * MAX_SAMPLE_VALUE * MAX_SAMPLE_VALUE))
628 if rms > 0:
629 db = 20 * math.log10(rms)
630 db = max(db, MIN_AUDIO_LEVEL)
631 db = min(db, MAX_AUDIO_LEVEL)
632 else:
633 db = MIN_AUDIO_LEVEL
634 return round(db)
637@dataclass
638class RtcpReceiverInfo:
639 ssrc: int
640 fraction_lost: int
641 packets_lost: int
642 highest_sequence: int
643 jitter: int
644 lsr: int
645 dlsr: int
647 def __bytes__(self) -> bytes:
648 data = pack("!LB", self.ssrc, self.fraction_lost)
649 data += pack_packets_lost(self.packets_lost)
650 data += pack("!LLLL", self.highest_sequence, self.jitter, self.lsr, self.dlsr)
651 return data
653 @classmethod
654 def parse(cls, data: bytes) -> "RtcpReceiverInfo":
655 ssrc, fraction_lost = unpack("!LB", data[0:5])
656 packets_lost = unpack_packets_lost(data[5:8])
657 highest_sequence, jitter, lsr, dlsr = unpack("!LLLL", data[8:])
658 return cls(
659 ssrc=ssrc,
660 fraction_lost=fraction_lost,
661 packets_lost=packets_lost,
662 highest_sequence=highest_sequence,
663 jitter=jitter,
664 lsr=lsr,
665 dlsr=dlsr,
666 )
669@dataclass
670class RtcpSenderInfo:
671 ntp_timestamp: int
672 rtp_timestamp: int
673 packet_count: int
674 octet_count: int
676 def __bytes__(self) -> bytes:
677 return pack(
678 "!QLLL",
679 self.ntp_timestamp,
680 self.rtp_timestamp,
681 self.packet_count,
682 self.octet_count,
683 )
685 @classmethod
686 def parse(cls, data: bytes) -> "RtcpSenderInfo":
687 ntp_timestamp, rtp_timestamp, packet_count, octet_count = unpack("!QLLL", data)
688 return cls(
689 ntp_timestamp=ntp_timestamp,
690 rtp_timestamp=rtp_timestamp,
691 packet_count=packet_count,
692 octet_count=octet_count,
693 )
696@dataclass
697class RtcpSourceInfo:
698 ssrc: int
699 items: list[tuple[Any, bytes]]
702@dataclass
703class RtcpByePacket:
704 sources: list[int]
706 def __bytes__(self) -> bytes:
707 payload = b"".join([pack("!L", ssrc) for ssrc in self.sources])
708 return pack_rtcp_packet(RTCP_BYE, len(self.sources), payload)
710 @classmethod
711 def parse(cls, data: bytes, count: int) -> "RtcpByePacket":
712 if len(data) < 4 * count:
713 raise ValueError("RTCP bye length is invalid")
714 if count > 0:
715 sources = list(unpack_from("!" + ("L" * count), data, 0))
716 else:
717 sources = []
718 return cls(sources=sources)
721@dataclass
722class RtcpPsfbPacket:
723 """
724 Payload-Specific Feedback Message (RFC 4585).
725 """
727 fmt: int
728 ssrc: int
729 media_ssrc: int
730 fci: bytes = b""
732 def __bytes__(self) -> bytes:
733 payload = pack("!LL", self.ssrc, self.media_ssrc) + self.fci
734 return pack_rtcp_packet(RTCP_PSFB, self.fmt, payload)
736 @classmethod
737 def parse(cls, data: bytes, fmt: int) -> "RtcpPsfbPacket":
738 if len(data) < 8:
739 raise ValueError("RTCP payload-specific feedback length is invalid")
741 ssrc, media_ssrc = unpack("!LL", data[0:8])
742 fci = data[8:]
743 return cls(fmt=fmt, ssrc=ssrc, media_ssrc=media_ssrc, fci=fci)
746@dataclass
747class RtcpRrPacket:
748 ssrc: int
749 reports: list[RtcpReceiverInfo] = field(default_factory=list)
751 def __bytes__(self) -> bytes:
752 payload = bytearray(pack("!L", self.ssrc))
753 for report in self.reports:
754 payload += bytes(report)
755 return pack_rtcp_packet(RTCP_RR, len(self.reports), bytes(payload))
757 @classmethod
758 def parse(cls, data: bytes, count: int) -> "RtcpRrPacket":
759 if len(data) != 4 + 24 * count:
760 raise ValueError("RTCP receiver report length is invalid")
762 ssrc = unpack("!L", data[0:4])[0]
763 pos = 4
764 reports = []
765 for r in range(count):
766 reports.append(RtcpReceiverInfo.parse(data[pos : pos + 24]))
767 pos += 24
768 return cls(ssrc=ssrc, reports=reports)
771@dataclass
772class RtcpRtpfbPacket:
773 """
774 Generic RTP Feedback Message (RFC 4585).
775 """
777 fmt: int
778 ssrc: int
779 media_ssrc: int
781 # generick NACK
782 lost: list[int] = field(default_factory=list)
784 # raw FCI bytes for formats with structured payloads (e.g. transport-cc)
785 fci: bytes = b""
787 def __bytes__(self) -> bytes:
788 payload = pack("!LL", self.ssrc, self.media_ssrc)
789 if self.lost:
790 pid = self.lost[0]
791 blp = 0
792 for p in self.lost[1:]:
793 d = p - pid - 1
794 if d < 16:
795 blp |= 1 << d
796 else:
797 payload += pack("!HH", pid, blp)
798 pid = p
799 blp = 0
800 payload += pack("!HH", pid, blp)
801 payload += self.fci
802 return pack_rtcp_packet(RTCP_RTPFB, self.fmt, payload)
804 @classmethod
805 def parse(cls, data: bytes, fmt: int) -> "RtcpRtpfbPacket":
806 # Only a NACK's FCI comes in whole words: a transport-cc feedback runs
807 # to any length and libwebrtc pads it with the RTCP padding bit, which
808 # the caller has already stripped.
809 if len(data) < 8 or (fmt == RTCP_RTPFB_NACK and len(data) % 4):
810 raise ValueError("RTCP RTP feedback length is invalid")
812 ssrc, media_ssrc = unpack("!LL", data[0:8])
813 lost = []
814 if fmt == RTCP_RTPFB_NACK:
815 for pos in range(8, len(data), 4):
816 pid, blp = unpack("!HH", data[pos : pos + 4])
817 lost.append(pid)
818 for d in range(0, 16):
819 if (blp >> d) & 1:
820 lost.append(pid + d + 1)
821 return cls(fmt=fmt, ssrc=ssrc, media_ssrc=media_ssrc, lost=lost, fci=data[8:])
824@dataclass
825class RtcpSdesPacket:
826 chunks: list[RtcpSourceInfo] = field(default_factory=list)
828 def __bytes__(self) -> bytes:
829 payload = b""
830 for chunk in self.chunks:
831 payload += pack("!L", chunk.ssrc)
832 for d_type, d_value in chunk.items:
833 payload += pack("!BB", d_type, len(d_value)) + d_value
834 payload += b"\x00\x00"
835 while len(payload) % 4:
836 payload += b"\x00"
837 return pack_rtcp_packet(RTCP_SDES, len(self.chunks), payload)
839 @classmethod
840 def parse(cls, data: bytes, count: int) -> "RtcpSdesPacket":
841 pos = 0
842 chunks = []
843 for r in range(count):
844 if len(data) < pos + 4:
845 raise ValueError("RTCP SDES source is truncated")
846 ssrc = unpack_from("!L", data, pos)[0]
847 pos += 4
849 items = []
850 while pos < len(data) - 1:
851 d_type, d_length = unpack_from("!BB", data, pos)
852 pos += 2
854 if len(data) < pos + d_length:
855 raise ValueError("RTCP SDES item is truncated")
856 d_value = data[pos : pos + d_length]
857 pos += d_length
858 if d_type == 0:
859 break
860 else:
861 items.append((d_type, d_value))
862 chunks.append(RtcpSourceInfo(ssrc=ssrc, items=items))
863 return cls(chunks=chunks)
866@dataclass
867class RtcpSrPacket:
868 ssrc: int
869 sender_info: RtcpSenderInfo
870 reports: list[RtcpReceiverInfo] = field(default_factory=list)
872 def __bytes__(self) -> bytes:
873 payload = pack("!L", self.ssrc)
874 payload += bytes(self.sender_info)
875 for report in self.reports:
876 payload += bytes(report)
877 return pack_rtcp_packet(RTCP_SR, len(self.reports), payload)
879 @classmethod
880 def parse(cls, data: bytes, count: int) -> "RtcpSrPacket":
881 if len(data) != 24 + 24 * count:
882 raise ValueError("RTCP sender report length is invalid")
884 ssrc = unpack_from("!L", data)[0]
885 sender_info = RtcpSenderInfo.parse(data[4:24])
886 pos = 24
887 reports = []
888 for r in range(count):
889 reports.append(RtcpReceiverInfo.parse(data[pos : pos + 24]))
890 pos += 24
891 return RtcpSrPacket(ssrc=ssrc, sender_info=sender_info, reports=reports)
894@dataclass
895class RtcpXrPacket:
896 """
897 Extended Report (RFC 3611) carrying the round-trip blocks: a receive-only peer sends
898 a Receiver Reference Time Report (`rrtr`, its NTP time), and the sender answers with
899 DLRR sub-blocks (`dlrr`: the reporter's SSRC, the middle 32 bits of that NTP time,
900 and the delay since, in 1/65536 s), from which the receiver computes the round trip
901 as a sender does from a receiver report. Other block types are skipped.
902 """
904 ssrc: int
905 rrtr: Optional[int] = None
906 dlrr: list[tuple[int, int, int]] = field(default_factory=list)
908 def __bytes__(self) -> bytes:
909 payload = pack("!L", self.ssrc)
910 if self.rrtr is not None:
911 payload += pack("!BBHQ", RTCP_XR_RRTR, 0, 2, self.rrtr)
912 if self.dlrr:
913 payload += pack("!BBH", RTCP_XR_DLRR, 0, 3 * len(self.dlrr))
914 for item in self.dlrr:
915 payload += pack("!LLL", *item)
916 return pack_rtcp_packet(RTCP_XR, 0, payload)
918 @classmethod
919 def parse(cls, data: bytes) -> "RtcpXrPacket":
920 if len(data) < 4:
921 raise ValueError("RTCP extended report length is invalid")
923 packet = cls(ssrc=unpack_from("!L", data)[0])
924 pos = 4
925 while pos + 4 <= len(data):
926 block_type, _, length = unpack_from("!BBH", data, pos)
927 pos += 4
928 end = pos + 4 * length
929 if len(data) < end:
930 raise ValueError("RTCP extended report block is truncated")
931 if block_type == RTCP_XR_RRTR and length == 2:
932 packet.rrtr = unpack_from("!Q", data, pos)[0]
933 elif block_type == RTCP_XR_DLRR and length % 3 == 0:
934 packet.dlrr += [unpack_from("!LLL", data, sub) for sub in range(pos, end, 12)]
935 pos = end
936 return packet
939AnyRtcpPacket = Union[
940 RtcpByePacket,
941 RtcpPsfbPacket,
942 RtcpRrPacket,
943 RtcpRtpfbPacket,
944 RtcpSdesPacket,
945 RtcpSrPacket,
946 RtcpXrPacket,
947]
950class RtcpPacket:
951 @classmethod
952 def parse(cls, data: bytes) -> list[AnyRtcpPacket]:
953 pos = 0
954 packets: list[AnyRtcpPacket] = []
956 while pos < len(data):
957 if len(data) < pos + RTCP_HEADER_LENGTH:
958 raise ValueError(
959 f"RTCP packet length is less than {RTCP_HEADER_LENGTH} bytes"
960 )
962 v_p_count, packet_type, length = unpack("!BBH", data[pos : pos + 4])
963 version = v_p_count >> 6
964 padding = (v_p_count >> 5) & 1
965 count = v_p_count & 0x1F
966 if version != 2:
967 raise ValueError("RTCP packet has invalid version")
968 pos += 4
970 end = pos + length * 4
971 if len(data) < end:
972 raise ValueError("RTCP packet is truncated")
973 payload = data[pos:end]
974 pos = end
976 if padding:
977 if not payload or not payload[-1] or payload[-1] > len(payload):
978 raise ValueError("RTCP packet padding length is invalid")
979 payload = payload[0 : -payload[-1]]
981 if packet_type == RTCP_BYE:
982 packets.append(RtcpByePacket.parse(payload, count))
983 elif packet_type == RTCP_SDES:
984 packets.append(RtcpSdesPacket.parse(payload, count))
985 elif packet_type == RTCP_SR:
986 packets.append(RtcpSrPacket.parse(payload, count))
987 elif packet_type == RTCP_RR:
988 packets.append(RtcpRrPacket.parse(payload, count))
989 elif packet_type == RTCP_RTPFB:
990 packets.append(RtcpRtpfbPacket.parse(payload, count))
991 elif packet_type == RTCP_PSFB:
992 packets.append(RtcpPsfbPacket.parse(payload, count))
993 elif packet_type == RTCP_XR:
994 packets.append(RtcpXrPacket.parse(payload))
996 return packets
999class RtpPacket:
1000 def __init__(
1001 self,
1002 payload_type: int = 0,
1003 marker: int = 0,
1004 sequence_number: int = 0,
1005 timestamp: int = 0,
1006 ssrc: int = 0,
1007 payload: bytes = b"",
1008 ) -> None:
1009 self.version = 2
1010 self.marker = marker
1011 self.payload_type = payload_type
1012 self.sequence_number = sequence_number
1013 self.timestamp = timestamp
1014 self.ssrc = ssrc
1015 self.csrc: list[int] = []
1016 self.extensions = HeaderExtensions()
1017 self.payload = payload
1018 self.padding_size = 0
1020 def __repr__(self) -> str:
1021 return (
1022 f"RtpPacket(seq={self.sequence_number}, ts={self.timestamp}, "
1023 f"marker={self.marker}, payload={self.payload_type}, "
1024 f"{len(self.payload)} bytes)"
1025 )
1027 @classmethod
1028 def parse(
1029 cls, data: bytes, extensions_map: HeaderExtensionsMap = HeaderExtensionsMap()
1030 ) -> "RtpPacket":
1031 if len(data) < RTP_HEADER_LENGTH:
1032 raise ValueError(
1033 f"RTP packet length is less than {RTP_HEADER_LENGTH} bytes"
1034 )
1036 v_p_x_cc, m_pt, sequence_number, timestamp, ssrc = unpack("!BBHLL", data[0:12])
1037 version = v_p_x_cc >> 6
1038 padding = (v_p_x_cc >> 5) & 1
1039 extension = (v_p_x_cc >> 4) & 1
1040 cc = v_p_x_cc & 0x0F
1041 if version != 2:
1042 raise ValueError("RTP packet has invalid version")
1043 if len(data) < RTP_HEADER_LENGTH + 4 * cc:
1044 raise ValueError("RTP packet has truncated CSRC")
1046 packet = cls(
1047 marker=(m_pt >> 7),
1048 payload_type=(m_pt & 0x7F),
1049 sequence_number=sequence_number,
1050 timestamp=timestamp,
1051 ssrc=ssrc,
1052 )
1054 pos = RTP_HEADER_LENGTH
1055 for i in range(0, cc):
1056 packet.csrc.append(unpack_from("!L", data, pos)[0])
1057 pos += 4
1059 if extension:
1060 if len(data) < pos + 4:
1061 raise ValueError("RTP packet has truncated extension profile / length")
1062 extension_profile, extension_length = unpack_from("!HH", data, pos)
1063 extension_length *= 4
1064 pos += 4
1066 if len(data) < pos + extension_length:
1067 raise ValueError("RTP packet has truncated extension value")
1068 extension_value = data[pos : pos + extension_length]
1069 pos += extension_length
1070 packet.extensions = extensions_map.get(extension_profile, extension_value)
1072 if padding:
1073 padding_len = data[-1]
1074 if not padding_len or padding_len > len(data) - pos:
1075 raise ValueError("RTP packet padding length is invalid")
1076 packet.padding_size = padding_len
1077 packet.payload = data[pos:-padding_len]
1078 else:
1079 packet.payload = data[pos:]
1081 return packet
1083 def serialize(
1084 self, extensions_map: HeaderExtensionsMap = HeaderExtensionsMap()
1085 ) -> bytes:
1086 extension_profile, extension_value = extensions_map.set(self.extensions)
1087 has_extension = bool(extension_value)
1089 padding = self.padding_size > 0
1090 data = pack(
1091 "!BBHLL",
1092 (self.version << 6)
1093 | (padding << 5)
1094 | (has_extension << 4)
1095 | len(self.csrc),
1096 (self.marker << 7) | self.payload_type,
1097 self.sequence_number,
1098 self.timestamp,
1099 self.ssrc,
1100 )
1101 for csrc in self.csrc:
1102 data += pack("!L", csrc)
1103 if has_extension:
1104 data += pack("!HH", extension_profile, len(extension_value) >> 2)
1105 data += extension_value
1106 data += self.payload
1107 if padding:
1108 data += os.urandom(self.padding_size - 1)
1109 data += bytes([self.padding_size])
1110 return data
1113class RtpHistory:
1114 """Packets sent on one stream, by sequence number, for retransmission, each with the
1115 frame it carried, how many NACKs have named it, when the last packet able to
1116 restore it went out, and whether its GOP was abandoned; and the frames the peer was
1117 reported to have lost.
1119 Bounded by RTP_HISTORY_S of sending and RTP_HISTORY_MAX_PACKETS; within
1120 those a sequence number cannot repeat, so a lookup is exact.
1121 """
1123 __slots__ = ("_packets", "_order", "_horizon", "_capacity", "_lost")
1125 def __init__(self, horizon: float = RTP_HISTORY_S,
1126 capacity: int = RTP_HISTORY_MAX_PACKETS) -> None:
1127 self._packets: dict[int, list] = {}
1128 self._order: deque = deque()
1129 self._horizon = horizon
1130 self._capacity = capacity
1131 self._lost: dict[int, float] = {}
1133 def add(self, packet: RtpPacket, now: float, frame: Optional[int] = None) -> None:
1134 """Record a sent packet and let go of those past the horizon."""
1135 self._packets[packet.sequence_number] = [packet, frame, 0, now, False]
1136 order = self._order
1137 order.append((now, packet.sequence_number))
1138 while order and (now - order[0][0] > self._horizon or len(order) > self._capacity):
1139 self._packets.pop(order.popleft()[1], None)
1141 def abandon(self) -> None:
1142 """Abandon every packet held, with the GOP the pacer dropped: its key frame is their
1143 repair. Each packet carries the mark, so none sent later is taken for one, however
1144 far the numbering has run since."""
1145 for entry in self._packets.values():
1146 entry[4] = True
1148 def abandoned(self, sequence_number: int) -> bool:
1149 """Whether the packet was held when its GOP was abandoned."""
1150 entry = self._packets.get(sequence_number)
1151 return entry is not None and entry[4]
1153 def get(self, sequence_number: int) -> Optional[RtpPacket]:
1154 entry = self._packets.get(sequence_number)
1155 return entry[0] if entry else None
1157 def nacked(self, sequence_number: int) -> tuple:
1158 """The packet a NACK names, the frame it carried, and how many NACKs have named it,
1159 this one counted; a packet let go reads as (None, None, 0)."""
1160 entry = self._packets.get(sequence_number)
1161 if entry is None:
1162 return None, None, 0
1163 entry[2] += 1
1164 return entry[0], entry[1], entry[2]
1166 def repaired(self, sequence_number: int, now: float) -> None:
1167 """Note that a packet able to restore this one (its retransmission, a FlexFEC
1168 repair covering it) reaches the wire at `now`, which counts what the pacer
1169 holds ahead of it: the peer measures the round trip over RTCP, which skips
1170 that queue, so it NACKs again before a repair waiting there can land."""
1171 entry = self._packets.get(sequence_number)
1172 if entry is not None:
1173 entry[3] = now
1175 def unrepaired(self, sequence_number: int, now: float, rtt: float) -> bool:
1176 """Whether a NACK arriving at `now` left the peer after every packet able to
1177 restore this one could have reached it, a round trip after the last went out.
1178 A peer that knows the round trip NACKs again within a few milliseconds, before
1179 a FlexFEC repair sent at the end of the packet's paced group can have landed."""
1180 entry = self._packets.get(sequence_number)
1181 return entry is not None and now - entry[3] >= rtt
1183 def newly_lost(self, frame: int, now: float) -> bool:
1184 """Whether `frame` is reported lost for the first time within the horizon. A peer
1185 that knows the round trip re-NACKs a packet it still misses every round trip
1186 until a later frame decodes, and another of the frame's packets can be NACKed
1187 twice too; neither is news, and a report reaching the encoder after the frame
1188 left its references is answered with a key frame."""
1189 lost = self._lost
1190 while lost and now - next(iter(lost.values())) > self._horizon:
1191 del lost[next(iter(lost))]
1192 if frame in lost:
1193 return False
1194 lost[frame] = now
1195 return True
1197 def __len__(self) -> int:
1198 return len(self._order)
1201def unwrap_rtx(rtx: RtpPacket, payload_type: int, ssrc: int) -> RtpPacket:
1202 """
1203 Recover initial packet from a retransmission packet.
1204 """
1205 packet = RtpPacket(
1206 payload_type=payload_type,
1207 marker=rtx.marker,
1208 sequence_number=unpack("!H", rtx.payload[0:2])[0],
1209 timestamp=rtx.timestamp,
1210 ssrc=ssrc,
1211 payload=rtx.payload[2:],
1212 )
1213 packet.csrc = rtx.csrc
1214 packet.extensions = rtx.extensions
1215 return packet
1218def wrap_rtx(
1219 packet: RtpPacket, payload_type: int, sequence_number: int, ssrc: int
1220) -> RtpPacket:
1221 """
1222 Create a retransmission packet from a lost packet.
1223 """
1224 rtx = RtpPacket(
1225 payload_type=payload_type,
1226 marker=packet.marker,
1227 sequence_number=sequence_number,
1228 timestamp=packet.timestamp,
1229 ssrc=ssrc,
1230 payload=pack("!H", packet.sequence_number) + packet.payload,
1231 )
1232 rtx.csrc = packet.csrc
1233 rtx.extensions = packet.extensions
1234 return rtx
1237def build_flexfec_03(
1238 media_packets: list[bytes],
1239 first_sequence_number: int,
1240 protected_ssrc: int,
1241 payload_type: int,
1242 sequence_number: int,
1243 timestamp: int,
1244 ssrc: int,
1245 protected: Optional[Iterable[int]] = None,
1246) -> bytes:
1247 """
1248 Build one FlexFEC (draft-03, R=0/F=0) repair packet XOR-protecting the given
1249 serialized media packets, whose sequence numbers start at
1250 `first_sequence_number` and are consecutive (single 15-bit mask block).
1251 `protected` names the offsets the repair covers, every packet by default:
1252 several repairs over one group with interleaved offsets let the receiver
1253 recover as many losses as there are repairs, one XOR at a time.
1255 Recovery fields follow the draft: byte 0 covers P/X/CC, byte 1 covers M/PT,
1256 plus 16-bit length recovery (packet length minus the fixed RTP header) and
1257 32-bit timestamp recovery; the repair payload XORs everything after the
1258 fixed header, zero-padded to the longest packet.
1259 """
1260 assert media_packets and len(media_packets) <= 15
1261 covered = [(offset, media_packets[offset])
1262 for offset in (range(len(media_packets)) if protected is None else protected)]
1263 recovery = bytearray(2)
1264 length_recovery = 0
1265 ts_recovery = 0
1266 longest = max(len(p) for _, p in covered) - 12
1267 # The payloads are XORed as big integers: a shorter one is shifted up so
1268 # its first byte lines up with the others', which pads it with zeros at
1269 # the end as the draft requires.
1270 payload_xor = 0
1271 mask = 0
1272 for offset, media in covered:
1273 # Byte 0 folds in P, X, and CC (the version bits stay out); byte 1,
1274 # M and PT.
1275 recovery[0] ^= media[0] & 0x3F
1276 recovery[1] ^= media[1]
1277 length_recovery ^= len(media) - 12
1278 ts_recovery ^= unpack("!L", media[4:8])[0]
1279 payload_xor ^= int.from_bytes(media[12:], "big") << (8 * (longest - len(media) + 12))
1280 mask |= 1 << (14 - offset)
1281 payload_recovery = payload_xor.to_bytes(longest, "big")
1283 header = bytearray(12)
1284 # V=2, P=0, X=0, CC=0.
1285 header[0] = 0x80
1286 header[1] = payload_type & 0x7F
1287 header[2:4] = pack("!H", sequence_number)
1288 header[4:8] = pack("!L", timestamp)
1289 header[8:12] = pack("!L", ssrc)
1291 fec = bytearray()
1292 # Masking the top recovery bits sets R=0, F=0.
1293 fec += bytes([recovery[0] & 0x3F, recovery[1]])
1294 fec += pack("!H", length_recovery)
1295 fec += pack("!L", ts_recovery)
1296 # SSRCCount=1 plus three reserved bytes.
1297 fec += bytes([1, 0, 0, 0])
1298 fec += pack("!L", protected_ssrc)
1299 fec += pack("!H", first_sequence_number)
1300 # K=1: single mask block.
1301 fec += pack("!H", 0x8000 | mask)
1302 return bytes(header) + bytes(fec) + payload_recovery