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

769 statements  

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 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 

41 

42from .rtcrtpparameters import RTCRtpParameters 

43 

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 

52 

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) 

58 

59RTP_HEADER_LENGTH = 12 

60RTCP_HEADER_LENGTH = 4 

61 

62PACKETS_LOST_MIN = -(1 << 23) 

63PACKETS_LOST_MAX = (1 << 23) - 1 

64 

65RTCP_SR = 200 

66RTCP_RR = 201 

67RTCP_SDES = 202 

68RTCP_BYE = 203 

69RTCP_RTPFB = 205 

70RTCP_PSFB = 206 

71RTCP_XR = 207 

72 

73RTCP_RTPFB_NACK = 1 

74RTCP_RTPFB_TWCC = 15 

75 

76RTCP_PSFB_PLI = 1 

77RTCP_PSFB_SLI = 2 

78RTCP_PSFB_RPSI = 3 

79RTCP_PSFB_FIR = 4 

80RTCP_PSFB_APP = 15 

81 

82RTCP_XR_RRTR = 4 

83RTCP_XR_DLRR = 5 

84 

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" 

91 

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" 

95 

96 

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 

117 

118 

119class HeaderExtensionsMap: 

120 def __init__(self) -> None: 

121 self.__ids = HeaderExtensions() 

122 self.__mutable: dict[int, int] = {} 

123 

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 } 

166 

167 def has_dependency_descriptor(self) -> bool: 

168 """Whether the peer negotiated the dependency descriptor.""" 

169 return bool(self.__ids.dependency_descriptor) 

170 

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 

227 

228 def for_fec(self, packet: bytes) -> bytes: 

229 """Copy an RTP packet for FlexFEC with mutable extension values zeroed. 

230 

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) 

278 

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) 

358 

359 

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) 

367 

368 

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") 

394 

395 

396def clamp_packets_lost(count: int) -> int: 

397 return max(PACKETS_LOST_MIN, min(count, PACKETS_LOST_MAX)) 

398 

399 

400def pack_packets_lost(count: int) -> bytes: 

401 return pack("!l", count)[1:] 

402 

403 

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] 

410 

411 

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 

415 

416 

417def pack_remb_fci(bitrate: int, ssrcs: list[int]) -> bytes: 

418 """ 

419 Pack the FCI for a Receiver Estimated Maximum Bitrate report. 

420 

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 

435 

436 

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). 

440 

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. 

445 

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 

489 

490 

491def unpack_remb_fci(data: bytes) -> tuple[int, list[int]]: 

492 """ 

493 Unpack the FCI for a Receiver Estimated Maximum Bitrate report. 

494 

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") 

502 

503 exponent = (data[5] & 0xFC) >> 2 

504 mantissa = ((data[5] & 0x03) << 16) | (data[6] << 8) | data[7] 

505 bitrate = mantissa << exponent 

506 

507 pos = 8 

508 ssrcs = [] 

509 for r in range(data[4]): 

510 ssrcs.append(unpack_from("!L", data, pos)[0]) 

511 pos += 4 

512 

513 return (bitrate, ssrcs) 

514 

515 

516def is_rtcp(msg: bytes) -> bool: 

517 return len(msg) >= 2 and msg[1] >= 192 and msg[1] <= 208 

518 

519 

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 

525 

526 

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 

535 

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 

543 

544 x_id = (extension_value[pos] & 0xF0) >> 4 

545 x_length = (extension_value[pos] & 0x0F) + 1 

546 pos += 1 

547 

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 

560 

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 

565 

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 

571 

572 return extensions 

573 

574 

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"" 

581 

582 if not extensions: 

583 return extension_profile, extension_value 

584 

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 

592 

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 

609 

610 extension_value += b"\x00" * padl(len(extension_value)) 

611 return extension_profile, extension_value 

612 

613 

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) 

635 

636 

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 

646 

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 

652 

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 ) 

667 

668 

669@dataclass 

670class RtcpSenderInfo: 

671 ntp_timestamp: int 

672 rtp_timestamp: int 

673 packet_count: int 

674 octet_count: int 

675 

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 ) 

684 

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 ) 

694 

695 

696@dataclass 

697class RtcpSourceInfo: 

698 ssrc: int 

699 items: list[tuple[Any, bytes]] 

700 

701 

702@dataclass 

703class RtcpByePacket: 

704 sources: list[int] 

705 

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) 

709 

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) 

719 

720 

721@dataclass 

722class RtcpPsfbPacket: 

723 """ 

724 Payload-Specific Feedback Message (RFC 4585). 

725 """ 

726 

727 fmt: int 

728 ssrc: int 

729 media_ssrc: int 

730 fci: bytes = b"" 

731 

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) 

735 

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") 

740 

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) 

744 

745 

746@dataclass 

747class RtcpRrPacket: 

748 ssrc: int 

749 reports: list[RtcpReceiverInfo] = field(default_factory=list) 

750 

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)) 

756 

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") 

761 

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) 

769 

770 

771@dataclass 

772class RtcpRtpfbPacket: 

773 """ 

774 Generic RTP Feedback Message (RFC 4585). 

775 """ 

776 

777 fmt: int 

778 ssrc: int 

779 media_ssrc: int 

780 

781 # generick NACK 

782 lost: list[int] = field(default_factory=list) 

783 

784 # raw FCI bytes for formats with structured payloads (e.g. transport-cc) 

785 fci: bytes = b"" 

786 

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) 

803 

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") 

811 

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:]) 

822 

823 

824@dataclass 

825class RtcpSdesPacket: 

826 chunks: list[RtcpSourceInfo] = field(default_factory=list) 

827 

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) 

838 

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 

848 

849 items = [] 

850 while pos < len(data) - 1: 

851 d_type, d_length = unpack_from("!BB", data, pos) 

852 pos += 2 

853 

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) 

864 

865 

866@dataclass 

867class RtcpSrPacket: 

868 ssrc: int 

869 sender_info: RtcpSenderInfo 

870 reports: list[RtcpReceiverInfo] = field(default_factory=list) 

871 

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) 

878 

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") 

883 

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) 

892 

893 

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 """ 

903 

904 ssrc: int 

905 rrtr: Optional[int] = None 

906 dlrr: list[tuple[int, int, int]] = field(default_factory=list) 

907 

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) 

917 

918 @classmethod 

919 def parse(cls, data: bytes) -> "RtcpXrPacket": 

920 if len(data) < 4: 

921 raise ValueError("RTCP extended report length is invalid") 

922 

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 

937 

938 

939AnyRtcpPacket = Union[ 

940 RtcpByePacket, 

941 RtcpPsfbPacket, 

942 RtcpRrPacket, 

943 RtcpRtpfbPacket, 

944 RtcpSdesPacket, 

945 RtcpSrPacket, 

946 RtcpXrPacket, 

947] 

948 

949 

950class RtcpPacket: 

951 @classmethod 

952 def parse(cls, data: bytes) -> list[AnyRtcpPacket]: 

953 pos = 0 

954 packets: list[AnyRtcpPacket] = [] 

955 

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 ) 

961 

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 

969 

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 

975 

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]] 

980 

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)) 

995 

996 return packets 

997 

998 

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 

1019 

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 ) 

1026 

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 ) 

1035 

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") 

1045 

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 ) 

1053 

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 

1058 

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 

1065 

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) 

1071 

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:] 

1080 

1081 return packet 

1082 

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) 

1088 

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 

1111 

1112 

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. 

1118 

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 """ 

1122 

1123 __slots__ = ("_packets", "_order", "_horizon", "_capacity", "_lost") 

1124 

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] = {} 

1132 

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) 

1140 

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 

1147 

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] 

1152 

1153 def get(self, sequence_number: int) -> Optional[RtpPacket]: 

1154 entry = self._packets.get(sequence_number) 

1155 return entry[0] if entry else None 

1156 

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] 

1165 

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 

1174 

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 

1182 

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 

1196 

1197 def __len__(self) -> int: 

1198 return len(self._order) 

1199 

1200 

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 

1216 

1217 

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 

1235 

1236 

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. 

1254 

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") 

1282 

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) 

1290 

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