/// <summary> /// Validates RtpCapabilities. It may modify given data by adding missing /// fields with default values. /// It throws if invalid. /// </summary> public static void ValidateRtpCapabilities(RtpCapabilities caps) { if (caps == null) { throw new ArgumentNullException(nameof(caps)); } if (caps.Codecs == null) { caps.Codecs = new List <RtpCodecCapability>(); } foreach (var codec in caps.Codecs) { ValidateRtpCodecCapability(codec); } // headerExtensions is optional. If unset, fill with an empty array. if (caps.HeaderExtensions == null) { caps.HeaderExtensions = Array.Empty <RtpHeaderExtension>(); } foreach (var ext in caps.HeaderExtensions) { ValidateRtpHeaderExtension(ext); } }
/// <summary> /// <para>Events:</para> /// <para>@emits workerclose</para> /// <para>@emits @close</para> /// <para>Observer events:</para> /// <para>@emits close</para> /// <para>@emits newtransport - (transport: Transport)</para> /// <para>@emits newrtpobserver - (rtpObserver: RtpObserver)</para> /// </summary> /// <param name="logger"></param> /// <param name="routerId"></param> /// <param name="rtpCapabilities"></param> /// <param name="channel"></param> /// <param name="payloadChannel"></param> /// <param name="appData"></param> public Router(ILoggerFactory loggerFactory, string routerId, RtpCapabilities rtpCapabilities, Channel channel, PayloadChannel payloadChannel, Dictionary <string, object>?appData ) { _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger <Router>(); _internal = new RouterInternalData { RouterId = routerId }; RtpCapabilities = rtpCapabilities; _channel = channel; _payloadChannel = payloadChannel; AppData = appData; }
/// <summary> /// Get a mapping in codec payloads and encodings in the given Producer RTP /// parameters as values expected by the Router. /// /// It may throw if invalid or non supported RTP parameters are given. /// </summary> public static RtpMapping GetProducerRtpParametersMapping(RtpParameters parameters, RtpCapabilities caps) { var rtpMapping = new RtpMapping { Codecs = new List <RtpMappingCodec>(), Encodings = new List <RtpMappingEncoding>() }; // Match parameters media codecs to capabilities media codecs. var codecToCapCodec = new Dictionary <RtpCodecParameters, RtpCodecCapability>(); foreach (var codec in parameters.Codecs) { if (IsRtxMimeType(codec.MimeType)) { continue; } // Search for the same media codec in capabilities. var matchedCapCodec = caps.Codecs ! .Where(capCodec => MatchCodecs(codec, capCodec, true, true)) .FirstOrDefault(); codecToCapCodec[codec] = matchedCapCodec ?? throw new NotSupportedException($"Unsupported codec[mimeType:{ codec.MimeType }, payloadType:{ codec.PayloadType }, Channels:{ codec.Channels }]"); } // Match parameters RTX codecs to capabilities RTX codecs. foreach (var codec in parameters.Codecs) { if (!IsRtxMimeType(codec.MimeType)) { continue; } // Search for the associated media codec. var associatedMediaCodec = parameters.Codecs .Where(mediaCodec => MatchCodecsWithPayloadTypeAndApt(mediaCodec.PayloadType, codec.Parameters)) .FirstOrDefault(); if (associatedMediaCodec == null) { throw new Exception($"missing media codec found for RTX PT { codec.PayloadType}"); } var capMediaCodec = codecToCapCodec[associatedMediaCodec]; // Ensure that the capabilities media codec has a RTX codec. var associatedCapRtxCodec = caps.Codecs ! .Where(capCodec => IsRtxMimeType(capCodec.MimeType) && MatchCodecsWithPayloadTypeAndApt(capMediaCodec.PreferredPayloadType, capCodec.Parameters)) .FirstOrDefault(); codecToCapCodec[codec] = associatedCapRtxCodec ?? throw new Exception($"no RTX codec for capability codec PT { capMediaCodec.PreferredPayloadType}"); } // Generate codecs mapping. foreach (var item in codecToCapCodec) { rtpMapping.Codecs.Add(new RtpMappingCodec { PayloadType = item.Key.PayloadType, MappedPayloadType = item.Value.PreferredPayloadType !.Value, });
/// <summary> /// Generate RTP capabilities for the Router based on the given media codecs and /// mediasoup supported RTP capabilities. /// </summary> public static RtpCapabilities GenerateRouterRtpCapabilities(RtpCodecCapability[] mediaCodecs) { if (mediaCodecs == null) { throw new ArgumentNullException(nameof(mediaCodecs)); } // Normalize supported RTP capabilities. ValidateRtpCapabilities(RtpCapabilities.SupportedRtpCapabilities); var clonedSupportedRtpCapabilities = RtpCapabilities.SupportedRtpCapabilities.DeepClone <RtpCapabilities>(); var dynamicPayloadTypes = DynamicPayloadTypes.DeepClone <int[]>().ToList(); var caps = new RtpCapabilities { Codecs = new List <RtpCodecCapability>(), HeaderExtensions = clonedSupportedRtpCapabilities.HeaderExtensions }; foreach (var mediaCodec in mediaCodecs) { // This may throw. ValidateRtpCodecCapability(mediaCodec); var matchedSupportedCodec = clonedSupportedRtpCapabilities .Codecs ! .Where(supportedCodec => MatchCodecs(mediaCodec, supportedCodec, false)).FirstOrDefault(); if (matchedSupportedCodec == null) { throw new Exception($"media codec not supported[mimeType:{mediaCodec.MimeType}]"); } // Clone the supported codec. var codec = matchedSupportedCodec.DeepClone <RtpCodecCapability>(); // If the given media codec has preferredPayloadType, keep it. if (mediaCodec.PreferredPayloadType.HasValue) { codec.PreferredPayloadType = mediaCodec.PreferredPayloadType; // Also remove the pt from the list in available dynamic values. dynamicPayloadTypes.Remove(codec.PreferredPayloadType.Value); } // Otherwise if the supported codec has preferredPayloadType, use it. else if (codec.PreferredPayloadType.HasValue) { // No need to remove it from the list since it's not a dynamic value. } // Otherwise choose a dynamic one. else { // Take the first available pt and remove it from the list. var pt = dynamicPayloadTypes.FirstOrDefault(); if (pt == 0) { throw new Exception("cannot allocate more dynamic codec payload types"); } dynamicPayloadTypes.RemoveAt(0); codec.PreferredPayloadType = pt; } // Ensure there is not duplicated preferredPayloadType values. if (caps.Codecs.Any(c => c.PreferredPayloadType == codec.PreferredPayloadType)) { throw new Exception("duplicated codec.preferredPayloadType"); } // Merge the media codec parameters. codec.Parameters = codec.Parameters.Merge(mediaCodec.Parameters); // Append to the codec list. caps.Codecs.Add(codec); // Add a RTX video codec if video. if (codec.Kind == MediaKind.Video) { // Take the first available pt and remove it from the list. var pt = dynamicPayloadTypes.FirstOrDefault(); if (pt == 0) { throw new Exception("cannot allocate more dynamic codec payload types"); } dynamicPayloadTypes.RemoveAt(0); var rtxCodec = new RtpCodecCapability { Kind = codec.Kind, MimeType = $"{codec.Kind.GetEnumMemberValue()}/rtx", PreferredPayloadType = pt, ClockRate = codec.ClockRate, Parameters = new Dictionary <string, object> { { "apt", codec.PreferredPayloadType } }, RtcpFeedback = Array.Empty <RtcpFeedback>(), }; // Append to the codec list. caps.Codecs.Add(rtxCodec); } } return(caps); }
static RtpCapabilities() { SupportedRtpCapabilities = new RtpCapabilities { Codecs = new List <RtpCodecCapability> { new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/opus", ClockRate = 48000, Channels = 2, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/PCMU", PreferredPayloadType = 0, ClockRate = 8000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/PCMA", PreferredPayloadType = 8, ClockRate = 8000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/ISAC", ClockRate = 32000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/ISAC", ClockRate = 16000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/G722", PreferredPayloadType = 9, ClockRate = 8000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/iLBC", ClockRate = 8000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/SILK", ClockRate = 24000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/SILK", ClockRate = 16000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/SILK", ClockRate = 12000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/SILK", ClockRate = 8000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/CN", PreferredPayloadType = 13, ClockRate = 32000 }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/CN", PreferredPayloadType = 13, ClockRate = 16000 }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/CN", PreferredPayloadType = 13, ClockRate = 8000 }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/telephone-event", ClockRate = 48000 }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/telephone-event", ClockRate = 32000 }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/telephone-event", ClockRate = 16000 }, new RtpCodecCapability { Kind = MediaKind.Audio, MimeType = "audio/telephone-event", ClockRate = 8000 }, new RtpCodecCapability { Kind = MediaKind.Video, MimeType = "video/VP8", ClockRate = 90000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "nack", }, new RtcpFeedback { Type = "nack", Parameter = "pli", }, new RtcpFeedback { Type = "ccm", Parameter = "fir", }, new RtcpFeedback { Type = "goog-remb", }, new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Video, MimeType = "video/VP9", ClockRate = 90000, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "nack", }, new RtcpFeedback { Type = "nack", Parameter = "pli", }, new RtcpFeedback { Type = "ccm", Parameter = "fir", }, new RtcpFeedback { Type = "goog-remb", }, new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Video, MimeType = "video/H264", ClockRate = 90000, Parameters = new Dictionary <string, object> { { "packetization-mode", 1 }, { "level-asymmetry-allowed", 1 }, }, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "nack", }, new RtcpFeedback { Type = "nack", Parameter = "pli", }, new RtcpFeedback { Type = "ccm", Parameter = "fir", }, new RtcpFeedback { Type = "goog-remb", }, new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Video, MimeType = "video/H264", ClockRate = 90000, Parameters = new Dictionary <string, object> { { "packetization-mode", 0 }, { "level-asymmetry-allowed", 1 }, }, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "nack", }, new RtcpFeedback { Type = "nack", Parameter = "pli", }, new RtcpFeedback { Type = "ccm", Parameter = "fir", }, new RtcpFeedback { Type = "goog-remb", }, new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Video, MimeType = "video/H265", ClockRate = 90000, Parameters = new Dictionary <string, object> { { "packetization-mode", 1 }, { "level-asymmetry-allowed", 1 }, }, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "nack", }, new RtcpFeedback { Type = "nack", Parameter = "pli", }, new RtcpFeedback { Type = "ccm", Parameter = "fir", }, new RtcpFeedback { Type = "goog-remb", }, new RtcpFeedback { Type = "transport-cc", }, } }, new RtpCodecCapability { Kind = MediaKind.Video, MimeType = "video/H265", ClockRate = 90000, Parameters = new Dictionary <string, object> { { "packetization-mode", 0 }, { "level-asymmetry-allowed", 1 }, }, RtcpFeedback = new RtcpFeedback[] { new RtcpFeedback { Type = "nack", }, new RtcpFeedback { Type = "nack", Parameter = "pli", }, new RtcpFeedback { Type = "ccm", Parameter = "fir", }, new RtcpFeedback { Type = "goog-remb", }, new RtcpFeedback { Type = "transport-cc", }, } } }, HeaderExtensions = new RtpHeaderExtension[] { new RtpHeaderExtension { Kind = MediaKind.Audio, Uri = "urn:ietf:params:rtp-hdrext:sdes:mid", PreferredId = 1, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive }, new RtpHeaderExtension { Kind = MediaKind.Video, Uri = "urn:ietf:params:rtp-hdrext:sdes:mid", PreferredId = 1, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive }, new RtpHeaderExtension { Kind = MediaKind.Video, Uri = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", PreferredId = 2, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.ReceiveOnly }, new RtpHeaderExtension { Kind = MediaKind.Video, Uri = "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", PreferredId = 3, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.ReceiveOnly }, new RtpHeaderExtension { Kind = MediaKind.Audio, Uri = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", PreferredId = 4, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive }, new RtpHeaderExtension { Kind = MediaKind.Video, Uri = "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", PreferredId = 4, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive }, // NOTE: For audio we just enable transport-wide-cc-01 when receiving media. new RtpHeaderExtension { Kind = MediaKind.Audio, Uri = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", PreferredId = 5, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.ReceiveOnly, }, new RtpHeaderExtension { Kind = MediaKind.Video, Uri = "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", PreferredId = 5, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive }, // NOTE: Remove this once framemarking draft becomes RFC. new RtpHeaderExtension { Kind = MediaKind.Video, Uri = "http://tools.ietf.org/html/draft-ietf-avtext-framemarking-07", PreferredId = 6, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive }, new RtpHeaderExtension { Kind = MediaKind.Video, Uri = "urn:ietf:params:rtp-hdrext:framemarking", PreferredId = 7, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive }, new RtpHeaderExtension { Kind = MediaKind.Audio, Uri = "urn:ietf:params:rtp-hdrext:ssrc-audio-level", PreferredId = 10, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive }, new RtpHeaderExtension { Kind = MediaKind.Video, Uri = "urn:3gpp:video-orientation", PreferredId = 11, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive }, new RtpHeaderExtension { Kind = MediaKind.Video, Uri = "urn:ietf:params:rtp-hdrext:toffset", PreferredId = 12, PreferredEncrypt = false, Direction = RtpHeaderExtensionDirection.SendReceive } } }; }