Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/selkies/webrtc/rtcrtpreceiver.py: 24%

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

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

35import datetime 

36import logging 

37import random 

38import time 

39from collections.abc import Callable 

40from dataclasses import dataclass 

41from typing import Any, Optional 

42 

43 

44from . import clock 

45from .codecs import frame_assembler, depayload, get_capabilities, is_rtx 

46from .exceptions import InvalidStateError 

47from .jitterbuffer import JitterBuffer 

48from .mediastreams import MediaStreamError, MediaStreamTrack 

49from .rate import RemoteBitrateEstimator 

50from .rtcdtlstransport import RTCDtlsTransport 

51from .rtcrtpparameters import ( 

52 RTCRtpCapabilities, 

53 RTCRtpCodecParameters, 

54 RTCRtpReceiveParameters, 

55) 

56from .rtp import ( 

57 RTCP_PSFB_APP, 

58 RTCP_PSFB_PLI, 

59 RTCP_RTPFB_NACK, 

60 RTP_HISTORY_SIZE, 

61 AnyRtcpPacket, 

62 RtcpByePacket, 

63 RtcpPsfbPacket, 

64 RtcpReceiverInfo, 

65 RtcpRrPacket, 

66 RtcpRtpfbPacket, 

67 RtcpSrPacket, 

68 RtpPacket, 

69 clamp_packets_lost, 

70 pack_remb_fci, 

71 unwrap_rtx, 

72) 

73from .stats import ( 

74 RTCInboundRtpStreamStats, 

75 RTCRemoteOutboundRtpStreamStats, 

76 RTCStatsReport, 

77) 

78from .utils import uint16_add, uint16_gt 

79 

80logger = logging.getLogger(__name__) 

81 

82 

83class NackGenerator: 

84 def __init__(self) -> None: 

85 self.max_seq: Optional[int] = None 

86 self.missing: set[int] = set() 

87 

88 def add(self, packet: RtpPacket) -> bool: 

89 """ 

90 Mark a new packet as received, and deduce missing packets. 

91 """ 

92 missed = False 

93 

94 if self.max_seq is None: 

95 self.max_seq = packet.sequence_number 

96 return missed 

97 

98 # mark missing packets 

99 if uint16_gt(packet.sequence_number, self.max_seq): 

100 seq = uint16_add(self.max_seq, 1) 

101 while uint16_gt(packet.sequence_number, seq): 

102 self.missing.add(seq) 

103 missed = True 

104 seq = uint16_add(seq, 1) 

105 self.max_seq = packet.sequence_number 

106 else: 

107 self.missing.discard(packet.sequence_number) 

108 

109 # limit number of tracked packets 

110 self.truncate() 

111 

112 return missed 

113 

114 def truncate(self) -> None: 

115 """ 

116 Limit the number of missing packets we track. 

117 

118 Otherwise, the size of RTCP FB messages grows indefinitely. 

119 """ 

120 if self.max_seq is not None: 

121 min_seq = uint16_add(self.max_seq, -RTP_HISTORY_SIZE) 

122 for seq in list(self.missing): 

123 if uint16_gt(min_seq, seq): 

124 self.missing.discard(seq) 

125 

126 

127class StreamStatistics: 

128 def __init__(self, clockrate: int) -> None: 

129 self.base_seq: Optional[int] = None 

130 self.max_seq: Optional[int] = None 

131 self.cycles = 0 

132 self.packets_received = 0 

133 

134 # jitter 

135 self._clockrate = clockrate 

136 self._jitter_q4 = 0 

137 self._last_arrival: Optional[int] = None 

138 self._last_timestamp: Optional[int] = None 

139 

140 # fraction lost 

141 self._expected_prior = 0 

142 self._received_prior = 0 

143 

144 def add(self, packet: RtpPacket) -> None: 

145 in_order = self.max_seq is None or uint16_gt( 

146 packet.sequence_number, self.max_seq 

147 ) 

148 self.packets_received += 1 

149 

150 if self.base_seq is None: 

151 self.base_seq = packet.sequence_number 

152 

153 if in_order: 

154 arrival = int(time.time() * self._clockrate) 

155 

156 if self.max_seq is not None and packet.sequence_number < self.max_seq: 

157 self.cycles += 1 << 16 

158 self.max_seq = packet.sequence_number 

159 

160 if packet.timestamp != self._last_timestamp and self.packets_received > 1: 

161 diff = abs( 

162 (arrival - self._last_arrival) 

163 - (packet.timestamp - self._last_timestamp) 

164 ) 

165 self._jitter_q4 += diff - ((self._jitter_q4 + 8) >> 4) 

166 

167 self._last_arrival = arrival 

168 self._last_timestamp = packet.timestamp 

169 

170 @property 

171 def fraction_lost(self) -> int: 

172 expected_interval = self.packets_expected - self._expected_prior 

173 self._expected_prior = self.packets_expected 

174 received_interval = self.packets_received - self._received_prior 

175 self._received_prior = self.packets_received 

176 lost_interval = expected_interval - received_interval 

177 if expected_interval == 0 or lost_interval <= 0: 

178 return 0 

179 else: 

180 return (lost_interval << 8) // expected_interval 

181 

182 @property 

183 def jitter(self) -> int: 

184 return self._jitter_q4 >> 4 

185 

186 @property 

187 def packets_expected(self) -> int: 

188 return self.cycles + self.max_seq - self.base_seq + 1 

189 

190 @property 

191 def packets_lost(self) -> int: 

192 return clamp_packets_lost(self.packets_expected - self.packets_received) 

193 

194 

195class RemoteStreamTrack(MediaStreamTrack): 

196 def __init__(self, kind: str, id: Optional[str] = None) -> None: 

197 super().__init__() 

198 self.kind = kind 

199 if id is not None: 

200 self._id = id 

201 self._queue: asyncio.Queue = asyncio.Queue() 

202 

203 async def recv(self) -> Any: 

204 """ 

205 Receive the next frame. 

206 """ 

207 if self.readyState != "live": 

208 raise MediaStreamError 

209 

210 frame = await self._queue.get() 

211 if frame is None: 

212 self.stop() 

213 raise MediaStreamError 

214 return frame 

215 

216 

217class TimestampMapper: 

218 def __init__(self) -> None: 

219 self._last: Optional[int] = None 

220 self._origin: Optional[int] = None 

221 

222 def map(self, timestamp: int) -> int: 

223 if self._origin is None: 

224 # first timestamp 

225 self._origin = timestamp 

226 elif timestamp < self._last: 

227 # RTP timestamp wrapped 

228 self._origin -= 1 << 32 

229 

230 self._last = timestamp 

231 return timestamp - self._origin 

232 

233 

234@dataclass 

235class RTCRtpContributingSource: 

236 """ 

237 The :class:`RTCRtpContributingSource` dictionary contains information about 

238 a contributing source (CSRC). 

239 """ 

240 

241 timestamp: datetime.datetime 

242 "The timestamp associated with this source." 

243 source: int 

244 "The CSRC identifier associated with this source." 

245 

246 

247@dataclass 

248class RTCRtpSynchronizationSource: 

249 """ 

250 The :class:`RTCRtpSynchronizationSource` dictionary contains information about 

251 a synchronization source (SSRC). 

252 """ 

253 

254 timestamp: datetime.datetime 

255 "The timestamp associated with this source." 

256 source: int 

257 "The SSRC identifier associated with this source." 

258 

259 

260class RTCRtpReceiver: 

261 """ 

262 The :class:`RTCRtpReceiver` interface manages the reception and decoding 

263 of data for a :class:`MediaStreamTrack`. 

264 

265 :param kind: The kind of media (`'audio'` or `'video'`). 

266 :param transport: An :class:`RTCDtlsTransport`. 

267 """ 

268 

269 def __init__(self, kind: str, transport: RTCDtlsTransport) -> None: 

270 if transport.state == "closed": 

271 raise InvalidStateError 

272 

273 self._enabled = True 

274 self.__active_ssrc: dict[int, datetime.datetime] = {} 

275 self.__codecs: dict[int, RTCRtpCodecParameters] = {} 

276 # Optional encoded-frame sinks (mic and webcam uplinks): when set, complete 

277 # encoded frames are handed here instead of the Python decoder, so pcmflux 

278 # and pixelflux are the only media decoders in the system. 

279 self._encoded_audio_sink = None 

280 self._encoded_video_sink = None 

281 self.__kind = kind 

282 if kind == "audio": 

283 self.__jitter_buffer = JitterBuffer(capacity=16, prefetch=4) 

284 self.__nack_generator = None 

285 self.__remote_bitrate_estimator = None 

286 else: 

287 self.__jitter_buffer = JitterBuffer(capacity=128, is_video=True) 

288 self.__nack_generator = NackGenerator() 

289 self.__remote_bitrate_estimator = RemoteBitrateEstimator() 

290 self._track: Optional[RemoteStreamTrack] = None 

291 self.__rtcp_exited = asyncio.Event() 

292 self.__rtcp_started = asyncio.Event() 

293 self.__rtcp_task: Optional[asyncio.Future[None]] = None 

294 self.__rtx_ssrc: dict[int, int] = {} 

295 self.__started = False 

296 self.__stats = RTCStatsReport() 

297 self.__timestamp_mapper = TimestampMapper() 

298 self.__transport = transport 

299 

300 # RTCP 

301 self.__lsr: dict[int, int] = {} 

302 self.__lsr_time: dict[int, float] = {} 

303 self.__remote_streams: dict[int, StreamStatistics] = {} 

304 self.__rtcp_ssrc: Optional[int] = None 

305 

306 # logging 

307 self.__log_debug: Callable[..., None] = lambda *args: None 

308 if logger.isEnabledFor(logging.DEBUG): 

309 self.__log_debug = lambda msg, *args: logger.debug( 

310 f"RTCRtpReceiver(%s) {msg}", self.__kind, *args 

311 ) 

312 

313 @property 

314 def track(self) -> MediaStreamTrack: 

315 """ 

316 The :class:`MediaStreamTrack` which is being handled by the receiver. 

317 """ 

318 return self._track 

319 

320 @property 

321 def transport(self) -> RTCDtlsTransport: 

322 """ 

323 The :class:`RTCDtlsTransport` over which the media for the receiver's 

324 track is received. 

325 """ 

326 return self.__transport 

327 

328 @classmethod 

329 def getCapabilities(cls, kind: str) -> Optional[RTCRtpCapabilities]: 

330 """ 

331 Returns the most optimistic view of the system's capabilities for 

332 receiving media of the given `kind`. 

333 

334 :rtype: :class:`RTCRtpCapabilities` 

335 """ 

336 return get_capabilities(kind) 

337 

338 async def getStats(self) -> RTCStatsReport: 

339 """ 

340 Returns statistics about the RTP receiver. 

341 

342 :rtype: :class:`RTCStatsReport` 

343 """ 

344 for ssrc, stream in self.__remote_streams.items(): 

345 self.__stats.add( 

346 RTCInboundRtpStreamStats( 

347 # RTCStats 

348 timestamp=clock.current_datetime(), 

349 type="inbound-rtp", 

350 id="inbound-rtp_" + str(id(self)), 

351 # RTCStreamStats 

352 ssrc=ssrc, 

353 kind=self.__kind, 

354 transportId=self.transport._stats_id, 

355 # RTCReceivedRtpStreamStats 

356 packetsReceived=stream.packets_received, 

357 packetsLost=stream.packets_lost, 

358 jitter=stream.jitter, 

359 # RTPInboundRtpStreamStats 

360 ) 

361 ) 

362 self.__stats.update(self.transport._get_stats()) 

363 

364 return self.__stats 

365 

366 def getSynchronizationSources(self) -> list[RTCRtpSynchronizationSource]: 

367 """ 

368 Returns a :class:`RTCRtpSynchronizationSource` for each unique SSRC identifier 

369 received in the last 10 seconds. 

370 """ 

371 cutoff = clock.current_datetime() - datetime.timedelta(seconds=10) 

372 sources = [] 

373 for source, timestamp in self.__active_ssrc.items(): 

374 if timestamp >= cutoff: 

375 sources.append( 

376 RTCRtpSynchronizationSource(source=source, timestamp=timestamp) 

377 ) 

378 return sources 

379 

380 async def receive(self, parameters: RTCRtpReceiveParameters) -> None: 

381 """ 

382 Attempt to set the parameters controlling the receiving of media. 

383 

384 :param parameters: The :class:`RTCRtpParameters` for the receiver. 

385 """ 

386 if not self.__started: 

387 for codec in parameters.codecs: 

388 self.__codecs[codec.payloadType] = codec 

389 for encoding in parameters.encodings: 

390 if encoding.rtx: 

391 self.__rtx_ssrc[encoding.rtx.ssrc] = encoding.ssrc 

392 

393 # Received media is handed off encoded: audio to pcmflux, video to 

394 # pixelflux, both of which decode off the GIL. A receiver with no 

395 # encoded-frame sink has nothing to decode it in-process. 

396 self.__transport._register_rtp_receiver(self, parameters) 

397 self.__rtcp_task = asyncio.ensure_future(self._run_rtcp()) 

398 self.__started = True 

399 

400 async def request_keyframe(self) -> None: 

401 """ 

402 Ask the sender for a keyframe (RTCP PLI) on every SSRC currently 

403 delivering to this receiver; used by an encoded-frame sink whose 

404 decoder lost its reference. 

405 """ 

406 for ssrc in list(self.__active_ssrc.keys()): 

407 try: 

408 await self._send_rtcp_pli(ssrc) 

409 except Exception: 

410 pass 

411 

412 def setTransport(self, transport: RTCDtlsTransport) -> None: 

413 self.__transport = transport 

414 

415 async def stop(self) -> None: 

416 """ 

417 Irreversibly stop the receiver. 

418 """ 

419 if self.__started: 

420 self.__transport._unregister_rtp_receiver(self) 

421 

422 # shutdown RTCP task 

423 await self.__rtcp_started.wait() 

424 self.__rtcp_task.cancel() 

425 await self.__rtcp_exited.wait() 

426 

427 def _handle_disconnect(self) -> None: 

428 # End the media track (unused on Selkies' encoded-sink path) on disconnect. 

429 if self._track is not None: 

430 self._track.stop() 

431 

432 async def _handle_rtcp_packet(self, packet: AnyRtcpPacket) -> None: 

433 self.__log_debug("< %s", packet) 

434 

435 if isinstance(packet, RtcpSrPacket): 

436 self.__stats.add( 

437 RTCRemoteOutboundRtpStreamStats( 

438 # RTCStats 

439 timestamp=clock.current_datetime(), 

440 type="remote-outbound-rtp", 

441 id=f"remote-outbound-rtp_{id(self)}", 

442 # RTCStreamStats 

443 ssrc=packet.ssrc, 

444 kind=self.__kind, 

445 transportId=self.transport._stats_id, 

446 # RTCSentRtpStreamStats 

447 packetsSent=packet.sender_info.packet_count, 

448 bytesSent=packet.sender_info.octet_count, 

449 # RTCRemoteOutboundRtpStreamStats 

450 remoteTimestamp=clock.datetime_from_ntp( 

451 packet.sender_info.ntp_timestamp 

452 ), 

453 ) 

454 ) 

455 self.__lsr[packet.ssrc] = ( 

456 (packet.sender_info.ntp_timestamp) >> 16 

457 ) & 0xFFFFFFFF 

458 self.__lsr_time[packet.ssrc] = time.time() 

459 elif isinstance(packet, RtcpByePacket): 

460 # Sender is done: end the media track (inert on the encoded-sink path). 

461 if self._track is not None: 

462 self._track.stop() 

463 

464 async def _handle_rtp_packet(self, packet: RtpPacket, arrival_time_ms: int) -> None: 

465 """ 

466 Handle an incoming RTP packet. 

467 """ 

468 self.__log_debug("< %s", packet) 

469 

470 # If the receiver is disabled, discard the packet. 

471 if not self._enabled: 

472 return 

473 

474 # answer transport-wide congestion control 

475 if packet.extensions.transport_sequence_number is not None: 

476 await self.transport._twcc_received( 

477 packet.extensions.transport_sequence_number, 

478 arrival_time_ms, 

479 packet.ssrc, 

480 self.__rtcp_ssrc, 

481 ) 

482 

483 # feed bitrate estimator 

484 if self.__remote_bitrate_estimator is not None: 

485 if packet.extensions.abs_send_time is not None: 

486 remb = self.__remote_bitrate_estimator.add( 

487 abs_send_time=packet.extensions.abs_send_time, 

488 arrival_time_ms=arrival_time_ms, 

489 payload_size=len(packet.payload) + packet.padding_size, 

490 ssrc=packet.ssrc, 

491 ) 

492 if self.__rtcp_ssrc is not None and remb is not None: 

493 # send Receiver Estimated Maximum Bitrate feedback 

494 rtcp_packet = RtcpPsfbPacket( 

495 fmt=RTCP_PSFB_APP, 

496 ssrc=self.__rtcp_ssrc, 

497 media_ssrc=0, 

498 fci=pack_remb_fci(*remb), 

499 ) 

500 await self._send_rtcp(rtcp_packet) 

501 

502 # keep track of sources 

503 self.__active_ssrc[packet.ssrc] = clock.current_datetime() 

504 

505 # check the codec is known 

506 codec = self.__codecs.get(packet.payload_type) 

507 if codec is None: 

508 self.__log_debug( 

509 "x RTP packet with unknown payload type %d", packet.payload_type 

510 ) 

511 return 

512 

513 # feed RTCP statistics 

514 if packet.ssrc not in self.__remote_streams: 

515 self.__remote_streams[packet.ssrc] = StreamStatistics(codec.clockRate) 

516 self.__remote_streams[packet.ssrc].add(packet) 

517 

518 # A FlexFEC repair stream numbers its own packets: fed to the NACK 

519 # generator it would name holes the media stream never had, and nothing 

520 # here recovers a frame from it. 

521 if codec.mimeType.lower() == "video/flexfec-03": 

522 return 

523 

524 # unwrap retransmission packet 

525 if is_rtx(codec): 

526 original_ssrc = self.__rtx_ssrc.get(packet.ssrc) 

527 if original_ssrc is None: 

528 self.__log_debug("x RTX packet from unknown SSRC %d", packet.ssrc) 

529 return 

530 

531 apt = codec.parameters.get("apt") 

532 if ( 

533 len(packet.payload) < 2 

534 or not isinstance(apt, int) 

535 or apt not in self.__codecs 

536 ): 

537 return 

538 

539 packet = unwrap_rtx(packet, payload_type=apt, ssrc=original_ssrc) 

540 codec = self.__codecs[apt] 

541 

542 # send NACKs for any missing any packets 

543 if self.__nack_generator is not None and self.__nack_generator.add(packet): 

544 await self._send_rtcp_nack( 

545 packet.ssrc, sorted(self.__nack_generator.missing) 

546 ) 

547 

548 # parse codec-specific information 

549 try: 

550 if packet.payload: 

551 packet._data = depayload(codec, packet.payload) # type: ignore 

552 else: 

553 packet._data = b"" # type: ignore 

554 packet._assemble = frame_assembler(codec) # type: ignore 

555 except ValueError as exc: 

556 self.__log_debug("x RTP payload parsing failed: %s", exc) 

557 return 

558 

559 # try to re-assemble encoded frame 

560 pli_flag, encoded_frame = self.__jitter_buffer.add(packet) 

561 # check if the PLI should be sent 

562 if pli_flag: 

563 await self._send_rtcp_pli(packet.ssrc) 

564 

565 # if we have a complete encoded frame, hand it off 

566 if encoded_frame is not None: 

567 encoded_frame.timestamp = self.__timestamp_mapper.map( 

568 encoded_frame.timestamp 

569 ) 

570 if self._encoded_audio_sink is not None: 

571 # Forward the encoded payload (bare Opus, or RED to de-frame downstream) 

572 # rather than decoding it in Python. 

573 self._encoded_audio_sink(codec, encoded_frame) 

574 elif self._encoded_video_sink is not None: 

575 # Same for video: the depacketized frame (Annex-B H.264 or H.265, 

576 # VP8/VP9, an AV1 temporal unit) goes to the virtual webcam's decoder. 

577 self._encoded_video_sink(codec, encoded_frame) 

578 

579 async def _run_rtcp(self) -> None: 

580 self.__log_debug("- RTCP started") 

581 self.__rtcp_started.set() 

582 

583 try: 

584 while True: 

585 # The interval between RTCP packets is varied randomly over the 

586 # range [0.5, 1.5] times the calculated interval. 

587 await asyncio.sleep(0.5 + random.random()) 

588 

589 # RTCP RR 

590 reports = [] 

591 for ssrc, stream in self.__remote_streams.items(): 

592 lsr = 0 

593 dlsr = 0 

594 if ssrc in self.__lsr: 

595 lsr = self.__lsr[ssrc] 

596 delay = time.time() - self.__lsr_time[ssrc] 

597 if delay > 0 and delay < 65536: 

598 dlsr = int(delay * 65536) 

599 

600 reports.append( 

601 RtcpReceiverInfo( 

602 ssrc=ssrc, 

603 fraction_lost=stream.fraction_lost, 

604 packets_lost=stream.packets_lost, 

605 highest_sequence=stream.max_seq, 

606 jitter=stream.jitter, 

607 lsr=lsr, 

608 dlsr=dlsr, 

609 ) 

610 ) 

611 

612 if self.__rtcp_ssrc is not None and reports: 

613 packet = RtcpRrPacket(ssrc=self.__rtcp_ssrc, reports=reports) 

614 await self._send_rtcp(packet) 

615 

616 except asyncio.CancelledError: 

617 pass 

618 

619 self.__log_debug("- RTCP finished") 

620 self.__rtcp_exited.set() 

621 

622 async def _send_rtcp(self, packet: AnyRtcpPacket) -> None: 

623 self.__log_debug("> %s", packet) 

624 try: 

625 await self.transport._send_rtp(bytes(packet)) 

626 except ConnectionError: 

627 pass 

628 

629 async def _send_rtcp_nack(self, media_ssrc: int, lost: list[int]) -> None: 

630 """ 

631 Send an RTCP packet to report missing RTP packets. 

632 """ 

633 if self.__rtcp_ssrc is not None: 

634 packet = RtcpRtpfbPacket( 

635 fmt=RTCP_RTPFB_NACK, ssrc=self.__rtcp_ssrc, media_ssrc=media_ssrc 

636 ) 

637 packet.lost = lost 

638 await self._send_rtcp(packet) 

639 

640 async def _send_rtcp_pli(self, media_ssrc: int) -> None: 

641 """ 

642 Send an RTCP packet to report picture loss. 

643 """ 

644 if self.__rtcp_ssrc is not None: 

645 packet = RtcpPsfbPacket( 

646 fmt=RTCP_PSFB_PLI, ssrc=self.__rtcp_ssrc, media_ssrc=media_ssrc 

647 ) 

648 await self._send_rtcp(packet) 

649 

650 def _set_rtcp_ssrc(self, ssrc: int) -> None: 

651 self.__rtcp_ssrc = ssrc 

652