diff --git a/src/net/RTP/MediaStream.cs b/src/net/RTP/MediaStream.cs index e23e3ff49..575d2a57a 100644 --- a/src/net/RTP/MediaStream.cs +++ b/src/net/RTP/MediaStream.cs @@ -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(); diff --git a/src/net/RTP/RTPHeader.cs b/src/net/RTP/RTPHeader.cs index 9ac9698ff..11306baae 100644 --- a/src/net/RTP/RTPHeader.cs +++ b/src/net/RTP/RTPHeader.cs @@ -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. @@ -253,7 +254,7 @@ public List GetHeaderExtensions() private bool HasOneByteExtension() { - return ExtensionProfile == 0xBEDE; + return ExtensionProfile == ONE_BYTE_EXTENSION_PROFILE; } private bool HasTwoByteExtension() @@ -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); + } } } diff --git a/src/net/SDP/SDP.cs b/src/net/SDP/SDP.cs index 3d93ff8e1..bd155a750 100644 --- a/src/net/SDP/SDP.cs +++ b/src/net/SDP/SDP.cs @@ -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 + @"(?\d+)\s+(?.*)$"); + Match formatAttributeMatch = Regex.Match(sdpLineTrimmed, SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUTE_PREFIX + @"(?\d+)\s+(?.*)$"); if (formatAttributeMatch.Success) { string formatID = formatAttributeMatch.Result("${id}"); @@ -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 + @"(?\S+)\s+(?.*)$"); + Match formatAttributeMatch = Regex.Match(sdpLineTrimmed, SDPMediaAnnouncement.MEDIA_FORMAT_ATTRIBUTE_PREFIX + @"(?\S+)\s+(?.*)$"); if (formatAttributeMatch.Success) { string formatID = formatAttributeMatch.Result("${id}"); diff --git a/src/net/SDP/SDPAudioVideoMediaFormat.cs b/src/net/SDP/SDPAudioVideoMediaFormat.cs index e01fbee39..f8b7dcdd8 100644 --- a/src/net/SDP/SDPAudioVideoMediaFormat.cs +++ b/src/net/SDP/SDPAudioVideoMediaFormat.cs @@ -96,6 +96,14 @@ public struct SDPAudioVideoMediaFormat /// public string Fmtp { get; } + public IEnumerable SupportedRtcpFeedbackMessages + { + get + { + yield return "goog-remb"; + } + } + /// /// The standard name of the media format. /// diff --git a/src/net/SDP/SDPMediaAnnouncement.cs b/src/net/SDP/SDPMediaAnnouncement.cs index 85150bd87..9ab3f2d7c 100644 --- a/src/net/SDP/SDPMediaAnnouncement.cs +++ b/src/net/SDP/SDPMediaAnnouncement.cs @@ -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:"; @@ -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"; @@ -206,6 +210,16 @@ public SDPMediaAnnouncement(SDPMediaTypesEnum mediaType, int port, List + { + { + 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 appMediaFormats) @@ -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) @@ -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) diff --git a/test/unit/net/RTP/RTPHeaderExtensionUnitTest.cs b/test/unit/net/RTP/RTPHeaderExtensionUnitTest.cs index ffc63f10f..5bfeac436 100644 --- a/test/unit/net/RTP/RTPHeaderExtensionUnitTest.cs +++ b/test/unit/net/RTP/RTPHeaderExtensionUnitTest.cs @@ -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]); + } } }