diff --git a/src/aiortc/codecs/__init__.py b/src/aiortc/codecs/__init__.py index a512939b9..1cb0a11db 100644 --- a/src/aiortc/codecs/__init__.py +++ b/src/aiortc/codecs/__init__.py @@ -48,6 +48,9 @@ RTCRtpHeaderExtensionParameters( id=2, uri="http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" ), + RTCRtpHeaderExtensionParameters( + id=3, uri="http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" + ), ], } diff --git a/src/aiortc/rtcrtpsender.py b/src/aiortc/rtcrtpsender.py index 9e2b243a0..6faa12030 100644 --- a/src/aiortc/rtcrtpsender.py +++ b/src/aiortc/rtcrtpsender.py @@ -38,7 +38,7 @@ RTCRemoteInboundRtpStreamStats, RTCStatsReport, ) -from .utils import random16, random32, uint16_add, uint32_add +from .utils import random16, random32, uint16_add, uint16_gt, uint32_add logger = logging.getLogger(__name__) @@ -96,6 +96,10 @@ def __init__(self, trackOrKind: Union[MediaStreamTrack, str], transport) -> None self.__stats = RTCStatsReport() self.__transport = transport + self.__playout_delay = 0 + self.__playout_delay_first_written = 0 + self.__playout_delay_write = False + # stats self.__lsr: Optional[int] = None self.__lsr_time: Optional[float] = None @@ -178,6 +182,20 @@ def replaceTrack(self, track: Optional[MediaStreamTrack]) -> None: def setTransport(self, transport) -> None: self.__transport = transport + def setPlayoutDelay(self, min_delay, max_delay) -> None: + mask = 0xFFF + + def check(value): + if value < 0 or value > mask: + raise ValueError("Playout delay values must be in the [0,4095] range") + + check(min_delay) + check(max_delay) + + self.__playout_delay = ((min_delay & mask) << 12) | (max_delay & mask) + self.__playout_delay_first_written = 0xFFFF + self.__playout_delay_write = True + async def send(self, parameters: RTCRtpSendParameters) -> None: """ Attempt to set the parameters controlling the sending of media. @@ -229,6 +247,12 @@ async def _handle_rtcp_packet(self, packet): else: self.__rtt = RTT_ALPHA * self.__rtt + (1 - RTT_ALPHA) * rtt + # Stop writing playout delay when seen by receiver + if self.__playout_delay_write: + self.__playout_delay_write = uint16_gt( + self.__playout_delay_first_written, report.highest_sequence + ) + self.__stats.add( RTCRemoteInboundRtpStreamStats( # RTCStats @@ -343,6 +367,14 @@ async def _run_rtp(self, codec: RTCRtpCodecParameters) -> None: clock.current_ntp_time() >> 14 ) & 0x00FFFFFF packet.extensions.mid = self.__mid + + if self.__playout_delay_write: + packet.extensions.playout_delay = self.__playout_delay + if uint16_gt( + self.__playout_delay_first_written, sequence_number + ): + self.__playout_delay_first_written = sequence_number + if enc_frame.audio_level is not None: packet.extensions.audio_level = (False, -enc_frame.audio_level) diff --git a/src/aiortc/rtp.py b/src/aiortc/rtp.py index fdc1b134e..99a91de44 100644 --- a/src/aiortc/rtp.py +++ b/src/aiortc/rtp.py @@ -40,6 +40,7 @@ @dataclass class HeaderExtensions: abs_send_time: Optional[int] = None + playout_delay: Optional[int] = None audio_level: Any = None mid: Any = None repaired_rtp_stream_id: Any = None @@ -64,6 +65,10 @@ def configure(self, parameters: RTCRtpParameters) -> None: ext.uri == "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" ): self.__ids.abs_send_time = ext.id + elif ( + ext.uri == "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" + ): + self.__ids.playout_delay = ext.id elif ext.uri == "urn:ietf:params:rtp-hdrext:toffset": self.__ids.transmission_offset = ext.id elif ext.uri == "urn:ietf:params:rtp-hdrext:ssrc-audio-level": @@ -87,6 +92,8 @@ def get(self, extension_profile: int, extension_value: bytes) -> HeaderExtension values.rtp_stream_id = x_value.decode("ascii") elif x_id == self.__ids.abs_send_time: values.abs_send_time = unpack("!L", b"\00" + x_value)[0] + elif x_id == self.__ids.playout_delay: + values.playout_delay = unpack("!L", b"\00" + x_value)[0] elif x_id == self.__ids.transmission_offset: values.transmission_offset = unpack("!l", x_value + b"\00")[0] >> 8 elif x_id == self.__ids.audio_level: @@ -118,6 +125,10 @@ def set(self, values: HeaderExtensions): extensions.append( (self.__ids.abs_send_time, pack("!L", values.abs_send_time)[1:]) ) + if values.playout_delay is not None and self.__ids.playout_delay: + extensions.append( + (self.__ids.playout_delay, pack("!L", values.playout_delay)[1:]) + ) if values.transmission_offset is not None and self.__ids.transmission_offset: extensions.append( ( diff --git a/tests/test_rtcrtpreceiver.py b/tests/test_rtcrtpreceiver.py index 51d5b714e..a9c56cec3 100644 --- a/tests/test_rtcrtpreceiver.py +++ b/tests/test_rtcrtpreceiver.py @@ -291,6 +291,9 @@ def test_capabilities(self): RTCRtpHeaderExtensionCapability( uri="http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" ), + RTCRtpHeaderExtensionCapability( + uri="http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" + ), ], ) diff --git a/tests/test_rtcrtpsender.py b/tests/test_rtcrtpsender.py index c02f8a470..2bd9c4c77 100644 --- a/tests/test_rtcrtpsender.py +++ b/tests/test_rtcrtpsender.py @@ -12,6 +12,7 @@ RTCRtpCodecCapability, RTCRtpCodecParameters, RTCRtpHeaderExtensionCapability, + RTCRtpHeaderExtensionParameters, RTCRtpParameters, ) from aiortc.rtcrtpsender import RTCRtpSender @@ -19,6 +20,7 @@ RTCP_PSFB_APP, RTCP_PSFB_PLI, RTCP_RTPFB_NACK, + HeaderExtensionsMap, RtcpPsfbPacket, RtcpReceiverInfo, RtcpRrPacket, @@ -117,6 +119,9 @@ def test_capabilities(self): RTCRtpHeaderExtensionCapability( uri="http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" ), + RTCRtpHeaderExtensionCapability( + uri="http://www.webrtc.org/experiments/rtp-hdrext/playout-delay" + ), ], ) @@ -210,6 +215,72 @@ async def test_handle_rtcp_remb(self): # clean shutdown await sender.stop() + @asynctest + async def test_playout_delay(self): + """ + Simulate playout delay request + """ + queue = asyncio.Queue() + + parameters = RTCRtpParameters( + codecs=[VP8_CODEC], + headerExtensions=[ + RTCRtpHeaderExtensionParameters( + id=6, + uri="http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", + ) + ], + ) + + ext_map = HeaderExtensionsMap() + ext_map.configure(parameters) + + async def mock_send_rtp(data): + if not is_rtcp(data): + await queue.put(RtpPacket.parse(data, ext_map)) + + async with dummy_dtls_transport_pair() as (local_transport, _): + local_transport._send_rtp = mock_send_rtp + + sender = RTCRtpSender(VideoStreamTrack(), local_transport) + self.assertEqual(sender.kind, "video") + + self.assertRaises(ValueError, lambda: sender.setPlayoutDelay(4096, 0)) + self.assertRaises(ValueError, lambda: sender.setPlayoutDelay(0, 4096)) + + min_delay = 4000 + max_delay = 4095 + sender.setPlayoutDelay(min_delay, max_delay) + + await sender.send(parameters) + + # wait for packet to be transmitted, expect playout delay + packet = await queue.get() + encoded_delay = (min_delay << 12) | max_delay + self.assertEqual(packet.extensions.playout_delay, encoded_delay) + + # receive RTCP RR + packet = RtcpRrPacket( + ssrc=1234, + reports=[ + RtcpReceiverInfo( + ssrc=sender._ssrc, + fraction_lost=0, + packets_lost=0, + highest_sequence=packet.sequence_number, + jitter=1906, + lsr=0, + dlsr=0, + ) + ], + ) + + await sender._handle_rtcp_packet(packet) + + # wait for packet to be transmitted, expect no more playout delay + packet = await queue.get() + self.assertEqual(packet.extensions.playout_delay, None) + @asynctest async def test_handle_rtcp_rr(self): async with dummy_dtls_transport_pair() as (local_transport, _): diff --git a/tests/test_sdp.py b/tests/test_sdp.py index 917701b9f..69e393bed 100644 --- a/tests/test_sdp.py +++ b/tests/test_sdp.py @@ -1946,6 +1946,40 @@ def test_safari(self): self.assertEqual(d.media[1].msid, None) self.assertEqual(d.webrtc_track_id(d.media[0]), None) + self.assertEqual( + d.media[1].rtp.headerExtensions, + [ + RTCRtpHeaderExtensionParameters( + id=2, uri="urn:ietf:params:rtp-hdrext:toffset" + ), + RTCRtpHeaderExtensionParameters( + id=3, + uri="http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", + ), + RTCRtpHeaderExtensionParameters(id=4, uri="urn:3gpp:video-orientation"), + RTCRtpHeaderExtensionParameters( + id=5, + uri="http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", + ), + RTCRtpHeaderExtensionParameters( + id=6, + uri="http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", + ), + RTCRtpHeaderExtensionParameters( + id=7, + uri="http://www.webrtc.org/experiments/rtp-hdrext/video-content-type", + ), + RTCRtpHeaderExtensionParameters( + id=8, + uri="http://www.webrtc.org/experiments/rtp-hdrext/video-timing", + ), + RTCRtpHeaderExtensionParameters( + id=10, + uri="http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07", + ), + ], + ) + self.assertEqual(d.media[2].kind, "application") self.assertEqual(d.media[2].host, "1.2.3.4") self.assertEqual(d.media[2].port, 60277)