/// <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> /// Check whether the given RTP capabilities can consume the given Producer. /// </summary> public static bool CanConsume(RtpParameters consumableParams, RtpCapabilities caps) { // This may throw. ValidateRtpCapabilities(caps); var matchingCodecs = new List <RtpCodecParameters>(); foreach (var codec in consumableParams.Codecs) { var matchedCapCodec = caps.Codecs .Where(capCodec => MatchCodecs(capCodec, codec, true)) .FirstOrDefault(); if (matchedCapCodec == null) { continue; } matchingCodecs.Add(codec); } // Ensure there is at least one media codec. if (matchingCodecs.Count == 0 || IsRtxMimeType(matchingCodecs[0].MimeType)) { return(false); } return(true); }
/// <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="appData"></param> public Router(ILoggerFactory loggerFactory, string routerId, RtpCapabilities rtpCapabilities, Channel channel, Dictionary <string, object>?appData) { _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger <Router>(); RouterId = routerId; _internal = new { RouterId, }; RtpCapabilities = rtpCapabilities; _channel = channel; AppData = appData; }
/// <summary> /// Check whether the given RTP capabilities can consume the given Producer. /// </summary> public bool CanConsume(string producerId, RtpCapabilities rtpCapabilities) { if (!_producers.TryGetValue(producerId, out Producer producer)) { _logger.LogError($"CanConsume() | Producer with id {producerId} not found"); return(false); } try { return(ORTC.CanConsume(producer.ConsumableRtpParameters, rtpCapabilities)); } catch (Exception ex) { _logger.LogError(ex, "CanConsume() | unexpected error"); return(false); } }
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 } } }; }
/// <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 Exception($"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.GetEnumStringValue()}/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); }
/// <summary> /// Generate RTP parameters for a specific Consumer. /// /// It reduces encodings to just one and takes into account given RTP capabilities /// to reduce codecs, codecs' RTCP feedback and header extensions, and also enables /// or disabled RTX. /// </summary> public static RtpParameters GetConsumerRtpParameters(RtpParameters consumableParams, RtpCapabilities caps) { var consumerParams = new RtpParameters { Codecs = new List <RtpCodecParameters>(), HeaderExtensions = new List <RtpHeaderExtensionParameters>(), Encodings = new List <RtpEncodingParameters>(), Rtcp = consumableParams.Rtcp }; foreach (var capCodec in caps.Codecs) // TODO: (alby)注意 null 引用 { ValidateRtpCodecCapability(capCodec); } var consumableCodecs = consumableParams.Codecs.DeepClone <List <RtpCodecParameters> >(); var rtxSupported = false; foreach (var codec in consumableCodecs) { var matchedCapCodec = caps.Codecs .Where(capCodec => MatchCodecs(capCodec, codec, true)) .FirstOrDefault(); if (matchedCapCodec == null) { continue; } codec.RtcpFeedback = matchedCapCodec.RtcpFeedback; consumerParams.Codecs.Add(codec); if (!rtxSupported && IsRtxMimeType(codec.MimeType)) { rtxSupported = true; } } // Ensure there is at least one media codec. if (consumerParams.Codecs.Count == 0 || IsRtxMimeType(consumerParams.Codecs[0].MimeType)) { throw new Exception("no compatible media codecs"); } consumerParams.HeaderExtensions = consumableParams.HeaderExtensions .Where(ext => caps.HeaderExtensions .Any(capExt => capExt.PreferredId == ext.Id && capExt.Uri == ext.Uri) ).ToList(); // Reduce codecs' RTCP feedback. Use Transport-CC if available, REMB otherwise. if (consumerParams.HeaderExtensions.Any(ext => ext.Uri == "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01")) { foreach (var codec in consumerParams.Codecs) { codec.RtcpFeedback = codec.RtcpFeedback.Where(fb => fb.Type != "goog-remb").ToArray(); } } else if (consumerParams.HeaderExtensions.Any(ext => ext.Uri == "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time")) { foreach (var codec in consumerParams.Codecs) { codec.RtcpFeedback = codec.RtcpFeedback.Where(fb => fb.Type != "transport-cc").ToArray(); } } else { foreach (var codec in consumerParams.Codecs) { codec.RtcpFeedback = codec.RtcpFeedback.Where(fb => fb.Type != "transport-cc" && fb.Type != "goog-remb").ToArray(); } } var consumerEncoding = new RtpEncodingParameters { Ssrc = Utils.GenerateRandomNumber() }; if (rtxSupported) { consumerEncoding.Rtx = new Rtx { Ssrc = Utils.GenerateRandomNumber() }; } // If any in the consumableParams.Encodings has scalabilityMode, process it // (assume all encodings have the same value). var encodingWithScalabilityMode = consumableParams.Encodings.Where(encoding => !encoding.ScalabilityMode.IsNullOrWhiteSpace()).FirstOrDefault(); var scalabilityMode = encodingWithScalabilityMode?.ScalabilityMode; // If there is simulast, mangle spatial layers in scalabilityMode. if (consumableParams.Encodings.Count > 1) // TODO: (alby)注意 null 引用 { var scalabilityModeObject = ScalabilityMode.Parse(scalabilityMode); // TODO: (alby)注意 null 引用 scalabilityMode = $"S{ consumableParams.Encodings.Count }T{ scalabilityModeObject.TemporalLayers }"; } if (!scalabilityMode.IsNullOrWhiteSpace()) { consumerEncoding.ScalabilityMode = scalabilityMode; } // Use the maximum maxBitrate in any encoding and honor it in the Consumer's // encoding. var maxEncodingMaxBitrate = consumableParams.Encodings.Max(m => m.MaxBitrate); if (maxEncodingMaxBitrate.HasValue && maxEncodingMaxBitrate.Value > 0) { consumerEncoding.MaxBitrate = maxEncodingMaxBitrate; } // Set a single encoding for the Consumer. consumerParams.Encodings.Add(consumerEncoding); // Copy verbatim. consumerParams.Rtcp = consumableParams.Rtcp; return(consumerParams); }
/// <summary> /// Generate RTP parameters to be internally used by Consumers given the RTP /// parameters in a Producer and the RTP capabilities in the Router. /// </summary> public static RtpParameters GetConsumableRtpParameters(MediaKind kind, RtpParameters parameters, RtpCapabilities caps, RtpMapping rtpMapping) { var consumableParams = new RtpParameters { Codecs = new List <RtpCodecParameters>(), HeaderExtensions = new List <RtpHeaderExtensionParameters>(), Encodings = new List <RtpEncodingParameters>(), Rtcp = new RtcpParameters(), }; foreach (var codec in parameters.Codecs) { if (IsRtxMimeType(codec.MimeType)) { continue; } var consumableCodecPt = rtpMapping.Codecs .Where(entry => entry.PayloadType == codec.PayloadType) .Select(m => m.MappedPayloadType) .FirstOrDefault(); var matchedCapCodec = caps.Codecs .Where(capCodec => capCodec.PreferredPayloadType == consumableCodecPt) .FirstOrDefault(); var consumableCodec = new RtpCodecParameters { MimeType = matchedCapCodec.MimeType, PayloadType = matchedCapCodec.PreferredPayloadType.Value, // TODO: (alby)注意 null 引用 ClockRate = matchedCapCodec.ClockRate, Channels = matchedCapCodec.Channels, Parameters = codec.Parameters, // Keep the Producer codec parameters. RtcpFeedback = matchedCapCodec.RtcpFeedback }; consumableParams.Codecs.Add(consumableCodec); var consumableCapRtxCodec = caps.Codecs .Where(capRtxCodec => IsRtxMimeType(capRtxCodec.MimeType) && MatchCodecsWithPayloadTypeAndApt(consumableCodec.PayloadType, capRtxCodec.Parameters)) .FirstOrDefault(); if (consumableCapRtxCodec != null) { var consumableRtxCodec = new RtpCodecParameters { MimeType = consumableCapRtxCodec.MimeType, PayloadType = consumableCapRtxCodec.PreferredPayloadType.Value, // TODO: (alby)注意 null 引用 ClockRate = consumableCapRtxCodec.ClockRate, Channels = consumableCapRtxCodec.Channels, Parameters = consumableCapRtxCodec.Parameters, // Keep the Producer codec parameters. RtcpFeedback = consumableCapRtxCodec.RtcpFeedback }; consumableParams.Codecs.Add(consumableRtxCodec); } } foreach (var capExt in caps.HeaderExtensions) // TODO: (alby)注意 null 引用 { // Just take RTP header extension that can be used in Consumers. if (capExt.Kind != kind || (capExt.Direction != RtpHeaderExtensionDirection.SendReceive && capExt.Direction != RtpHeaderExtensionDirection.SendOnly)) { continue; } var consumableExt = new RtpHeaderExtensionParameters { Uri = capExt.Uri, Id = capExt.PreferredId, Encrypt = capExt.PreferredEncrypt, Parameters = new Dictionary <string, object>(), }; consumableParams.HeaderExtensions.Add(consumableExt); } // Clone Producer encodings since we'll mangle them. var consumableEncodings = parameters.Encodings.DeepClone <List <RtpEncodingParameters> >(); for (var i = 0; i < consumableEncodings.Count; ++i) { var consumableEncoding = consumableEncodings[i]; var mappedSsrc = rtpMapping.Encodings[i].MappedSsrc; // Remove useless fields. // 在 Node.js 实现中,rid, rtx, codecPayloadType 被 delete 了。 consumableEncoding.Rid = null; consumableEncoding.Rtx = null; consumableEncoding.CodecPayloadType = null; // Set the mapped ssrc. consumableEncoding.Ssrc = mappedSsrc; consumableParams.Encodings.Add(consumableEncoding); } consumableParams.Rtcp = new RtcpParameters { CNAME = parameters.Rtcp.CNAME, // TODO: (alby)注意 null 引用 ReducedSize = true, Mux = true, }; return(consumableParams); }