Skip to content

Commit

Permalink
Support for abs-send-time - closes #1081 (#1084)
Browse files Browse the repository at this point in the history
* Add abs-send-time to each RTP packet

* Add a=rtcp-fb with goog-remb to SDP

* Move Header Extensions declaration to SDPMediaAnnouncement constructor

* UnixEpoch not available on lower frameworks

* Move AbsSendTime to RTPHeader class

* Comments for AbsSendTime

* Send abs-send-time only if remote track supports it

* Add unit test

* Revert whitespace changes
  • Loading branch information
theimowski committed Mar 21, 2024
1 parent 5466285 commit 1c1ca9f
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 8 deletions.
6 changes: 6 additions & 0 deletions src/net/RTP/MediaStream.cs
Expand Up @@ -366,6 +366,12 @@ protected void SendRtpRaw(byte[] data, uint timestamp, int markerBit, int payloa
rtpPacket.Header.MarkerBit = markerBit;
rtpPacket.Header.PayloadType = payloadType;

if (RemoteTrack.HeaderExtensions.TryGetValue(SDPMediaAnnouncement.RTP_HEADER_EXTENSION_ID_ABS_SEND_TIME, out var ext) &&
ext.Uri == SDPMediaAnnouncement.RTP_HEADER_EXTENSION_URI_ABS_SEND_TIME)
{
rtpPacket.Header.AddAbsSendTimeExtension();
}

Buffer.BlockCopy(data, 0, rtpPacket.Payload, 0, data.Length);

var rtpBuffer = rtpPacket.GetBytes();
Expand Down
54 changes: 53 additions & 1 deletion src/net/RTP/RTPHeader.cs
Expand Up @@ -28,6 +28,7 @@ public class RTPHeader
public const int MIN_HEADER_LEN = 12;

public const int RTP_VERSION = 2;
private const int ONE_BYTE_EXTENSION_PROFILE = 0xBEDE;

public int Version = RTP_VERSION; // 2 bits.
public int PaddingFlag = 0; // 1 bit.
Expand Down Expand Up @@ -253,7 +254,7 @@ public List<RTPHeaderExtensionData> GetHeaderExtensions()

private bool HasOneByteExtension()
{
return ExtensionProfile == 0xBEDE;
return ExtensionProfile == ONE_BYTE_EXTENSION_PROFILE;
}

private bool HasTwoByteExtension()
Expand Down Expand Up @@ -323,5 +324,56 @@ private bool HasTwoByteExtension()
consumed = offset;
return header.PayloadSize>=0;
}

// DateTimeOffset.UnixEpoch only available in newer target frameworks
private static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero);

// inspired by https://github.com/pion/rtp/blob/master/abssendtimeextension.go
internal static byte[] AbsSendTime(DateTimeOffset now)
{
ulong unixNanoseconds = (ulong)((now - UnixEpoch).Ticks * 100L);
var seconds = unixNanoseconds / (ulong)1e9;
seconds += 0x83AA7E80UL; // offset in seconds between unix epoch and ntp epoch
var f = unixNanoseconds % (ulong)1e9;
f <<= 32;
f /= (ulong)1e9;
seconds <<= 32;
var ntp = seconds | f;
var abs = ntp >> 14;
var length = 2; // extension length (3-1)

return new[]
{
(byte)((SDPMediaAnnouncement.RTP_HEADER_EXTENSION_ID_ABS_SEND_TIME << 4) | length),
(byte)((abs & 0xff0000UL) >> 16),
(byte)((abs & 0xff00UL) >> 8),
(byte)(abs & 0xffUL)
};
}

/*
An example header extension, with three extension elements, some
padding, and including the required RTP fields, follows:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 0xBE | 0xDE | length=3 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ID | L=0 | data | ID | L=1 | data... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| ...data | 0 (pad) | 0 (pad) | ID | L=3 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
// https://datatracker.ietf.org/doc/html/rfc5285#section-4.2
public void AddAbsSendTimeExtension()
{
HeaderExtensionFlag = 1;
ExtensionProfile = ONE_BYTE_EXTENSION_PROFILE;
ExtensionLength = 1; // only abs-send-time for now
ExtensionPayload = AbsSendTime(DateTimeOffset.Now);
}
}
}
6 changes: 3 additions & 3 deletions src/net/SDP/SDP.cs
Expand Up @@ -434,13 +434,13 @@ public static SDP ParseSDPDescription(string sdpDescription)
}

break;
case var l when l.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUE_PREFIX):
case var l when l.StartsWith(SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUTE_PREFIX):
if (activeAnnouncement != null)
{
if (activeAnnouncement.Media == SDPMediaTypesEnum.audio || activeAnnouncement.Media == SDPMediaTypesEnum.video)
{
// Parse the rtpmap attribute for audio/video announcements.
Match formatAttributeMatch = Regex.Match(sdpLineTrimmed, SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUE_PREFIX + @"(?<id>\d+)\s+(?<attribute>.*)$");
Match formatAttributeMatch = Regex.Match(sdpLineTrimmed, SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUTE_PREFIX + @"(?<id>\d+)\s+(?<attribute>.*)$");
if (formatAttributeMatch.Success)
{
string formatID = formatAttributeMatch.Result("${id}");
Expand Down Expand Up @@ -471,7 +471,7 @@ public static SDP ParseSDPDescription(string sdpDescription)
else
{
// Parse the rtpmap attribute for NON audio/video announcements.
Match formatAttributeMatch = Regex.Match(sdpLineTrimmed, SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUE_PREFIX + @"(?<id>\S+)\s+(?<attribute>.*)$");
Match formatAttributeMatch = Regex.Match(sdpLineTrimmed, SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUTE_PREFIX + @"(?<id>\S+)\s+(?<attribute>.*)$");
if (formatAttributeMatch.Success)
{
string formatID = formatAttributeMatch.Result("${id}");
Expand Down
8 changes: 8 additions & 0 deletions src/net/SDP/SDPAudioVideoMediaFormat.cs
Expand Up @@ -96,6 +96,14 @@ public struct SDPAudioVideoMediaFormat
/// </summary>
public string Fmtp { get; }

public IEnumerable<string> SupportedRtcpFeedbackMessages
{
get
{
yield return "goog-remb";
}
}

/// <summary>
/// The standard name of the media format.
/// <code>
Expand Down
27 changes: 23 additions & 4 deletions src/net/SDP/SDPMediaAnnouncement.cs
Expand Up @@ -70,7 +70,8 @@ public SDPSsrcAttribute(uint ssrc, string cname, string groupID)
public class SDPMediaAnnouncement
{
public const string MEDIA_EXTENSION_MAP_ATTRIBUE_PREFIX = "a=extmap:";
public const string MEDIA_FORMAT_ATTRIBUE_PREFIX = "a=rtpmap:";
public const string MEDIA_FORMAT_ATTRIBUTE_PREFIX = "a=rtpmap:";
public const string MEDIA_FORMAT_FEEDBACK_PREFIX = "a=rtcp-fb:";
public const string MEDIA_FORMAT_PARAMETERS_ATTRIBUE_PREFIX = "a=fmtp:";
public const string MEDIA_FORMAT_SSRC_ATTRIBUE_PREFIX = "a=ssrc:";
public const string MEDIA_FORMAT_SSRC_GROUP_ATTRIBUE_PREFIX = "a=ssrc-group:";
Expand All @@ -80,6 +81,9 @@ public class SDPMediaAnnouncement
public const string MEDIA_FORMAT_PATH_MSRP_PREFIX = "a=path:msrp:";
public const string MEDIA_FORMAT_PATH_ACCEPT_TYPES_PREFIX = "a=accept-types:";
public const string TIAS_BANDWIDTH_ATTRIBUE_PREFIX = "b=TIAS:";
public const int RTP_HEADER_EXTENSION_ID_ABS_SEND_TIME = 2;
public const string RTP_HEADER_EXTENSION_URI_ABS_SEND_TIME = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time";

public const MediaStreamStatusEnum DEFAULT_STREAM_STATUS = MediaStreamStatusEnum.SendRecv;

public const string m_CRLF = "\r\n";
Expand Down Expand Up @@ -206,6 +210,16 @@ public SDPMediaAnnouncement(SDPMediaTypesEnum mediaType, int port, List<SDPAudio
}
}
}

HeaderExtensions = new Dictionary<int, RTPHeaderExtension>
{
{
RTP_HEADER_EXTENSION_ID_ABS_SEND_TIME,
new RTPHeaderExtension(
RTP_HEADER_EXTENSION_ID_ABS_SEND_TIME,
RTP_HEADER_EXTENSION_URI_ABS_SEND_TIME)
}
};
}

public SDPMediaAnnouncement(SDPMediaTypesEnum mediaType, int port, List<SDPApplicationMediaFormat> appMediaFormats)
Expand Down Expand Up @@ -422,7 +436,7 @@ public string GetFormatListAttributesToString()
{
if (appFormat.Value.Rtpmap != null)
{
sb.Append($"{MEDIA_FORMAT_ATTRIBUE_PREFIX}{appFormat.Key} {appFormat.Value.Rtpmap}{m_CRLF}");
sb.Append($"{MEDIA_FORMAT_ATTRIBUTE_PREFIX}{appFormat.Key} {appFormat.Value.Rtpmap}{m_CRLF}");
}

if (appFormat.Value.Fmtp != null)
Expand Down Expand Up @@ -474,11 +488,16 @@ public string GetFormatListAttributesToString()
{
// Well known media formats are not required to add an rtpmap but we do so any way as some SIP
// stacks don't work without it.
formatAttributes += MEDIA_FORMAT_ATTRIBUE_PREFIX + mediaFormat.ID + " " + mediaFormat.Name() + "/" + mediaFormat.ClockRate() + m_CRLF;
formatAttributes += MEDIA_FORMAT_ATTRIBUTE_PREFIX + mediaFormat.ID + " " + mediaFormat.Name() + "/" + mediaFormat.ClockRate() + m_CRLF;
}
else
{
formatAttributes += MEDIA_FORMAT_ATTRIBUE_PREFIX + mediaFormat.ID + " " + mediaFormat.Rtpmap + m_CRLF;
formatAttributes += MEDIA_FORMAT_ATTRIBUTE_PREFIX + mediaFormat.ID + " " + mediaFormat.Rtpmap + m_CRLF;
}

foreach (var rtcpFeedbackMessage in mediaFormat.SupportedRtcpFeedbackMessages)
{
formatAttributes += MEDIA_FORMAT_FEEDBACK_PREFIX + mediaFormat.ID + " " + rtcpFeedbackMessage + m_CRLF;
}

if (mediaFormat.Fmtp != null)
Expand Down
15 changes: 15 additions & 0 deletions test/unit/net/RTP/RTPHeaderExtensionUnitTest.cs
Expand Up @@ -67,5 +67,20 @@ public void ReturnsNullTimestamps()

Assert.Null(timestamps);
}

[Fact]
public void AbsSendTime()
{
logger.LogDebug("--> " + System.Reflection.MethodBase.GetCurrentMethod().Name);
logger.BeginScope(System.Reflection.MethodBase.GetCurrentMethod().Name);

var time = new DateTimeOffset(2024, 2, 11, 14, 51, 02, 999, new TimeSpan(-5, 0, 0));
var bytes = RTPHeader.AbsSendTime(time);

Assert.Equal(0x22, bytes[0]); // 2 for ID and 2 for Length (3-1)
Assert.Equal(155, bytes[1]);
Assert.Equal(254, bytes[2]);
Assert.Equal(249, bytes[3]);
}
}
}

1 comment on commit 1c1ca9f

@ispysoftware
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this broke something on firefox
#1093

Please sign in to comment.