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

Support for abs-send-time - closes #1081 #1084

Merged
merged 9 commits into from Mar 21, 2024
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]);
}
}
}