Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Playout delay support #668

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions src/aiortc/codecs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
),
],
}

Expand Down
34 changes: 33 additions & 1 deletion src/aiortc/rtcrtpsender.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
11 changes: 11 additions & 0 deletions src/aiortc/rtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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":
Expand All @@ -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:
Expand Down Expand Up @@ -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(
(
Expand Down
3 changes: 3 additions & 0 deletions tests/test_rtcrtpreceiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
),
],
)

Expand Down
71 changes: 71 additions & 0 deletions tests/test_rtcrtpsender.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
RTCRtpCodecCapability,
RTCRtpCodecParameters,
RTCRtpHeaderExtensionCapability,
RTCRtpHeaderExtensionParameters,
RTCRtpParameters,
)
from aiortc.rtcrtpsender import RTCRtpSender
from aiortc.rtp import (
RTCP_PSFB_APP,
RTCP_PSFB_PLI,
RTCP_RTPFB_NACK,
HeaderExtensionsMap,
RtcpPsfbPacket,
RtcpReceiverInfo,
RtcpRrPacket,
Expand Down Expand Up @@ -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"
),
],
)

Expand Down Expand Up @@ -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, _):
Expand Down
34 changes: 34 additions & 0 deletions tests/test_sdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down