/// <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> /// 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> /// <para>Events:</para> /// <para>@emits transportclose</para> /// <para>@emits producerclose</para> /// <para>@emits producerpause</para> /// <para>@emits producerresume</para> /// <para>@emits score - (score: ConsumerScore)</para> /// <para>@emits layerschange - (layers: ConsumerLayers | undefined)</para> /// <para>@emits trace - (trace: ConsumerTraceEventData)</para> /// <para>@emits @close</para> /// <para>@emits @producerclose</para> /// <para>Observer events:</para> /// <para>@emits close</para> /// <para>@emits pause</para> /// <para>@emits resume</para> /// <para>@emits score - (score: ConsumerScore)</para> /// <para>@emits layerschange - (layers: ConsumerLayers | undefined)</para> /// <para>@emits trace - (trace: ConsumerTraceEventData)</para> /// </summary> /// <param name="loggerFactory"></param> /// <param name="consumerInternalData"></param> /// <param name="kind"></param> /// <param name="rtpParameters"></param> /// <param name="type"></param> /// <param name="channel"></param> /// <param name="appData"></param> /// <param name="paused"></param> /// <param name="producerPaused"></param> /// <param name="score"></param> /// <param name="preferredLayers"></param> public Consumer(ILoggerFactory loggerFactory, ConsumerInternalData consumerInternalData, MediaKind kind, RtpParameters rtpParameters, ConsumerType type, Channel channel, Dictionary <string, object>?appData, bool paused, bool producerPaused, ConsumerScore?score, ConsumerLayers?preferredLayers ) { _logger = loggerFactory.CreateLogger <Consumer>(); // Internal Internal = consumerInternalData; // Data Kind = kind; RtpParameters = rtpParameters; Type = type; _channel = channel; AppData = appData; Paused = paused; ProducerPaused = producerPaused; Score = score; PreferredLayers = preferredLayers; HandleWorkerNotifications(); }
/// <summary> /// Generate RTP parameters for a pipe Consumer. /// /// It keeps all original consumable encodings and removes support for BWE. If /// enableRtx is false, it also removes RTX and NACK support. /// </summary> public static RtpParameters GetPipeConsumerRtpParameters(RtpParameters consumableParams, bool enableRtx = false) { var consumerParams = new RtpParameters { Codecs = new List <RtpCodecParameters>(), HeaderExtensions = new List <RtpHeaderExtensionParameters>(), Encodings = new List <RtpEncodingParameters>(), Rtcp = consumableParams.Rtcp }; var consumableCodecs = consumableParams.Codecs.DeepClone <List <RtpCodecParameters> >(); foreach (var codec in consumableCodecs) { if (!enableRtx && IsRtxMimeType(codec.MimeType)) { continue; } codec.RtcpFeedback = codec.RtcpFeedback .Where(fb => (fb.Type == "nack" && fb.Parameter == "pli") || (fb.Type == "ccm" && fb.Parameter == "fir") || (enableRtx && fb.Type == "nack" && fb.Parameter.IsNullOrWhiteSpace()) ).ToArray(); consumerParams.Codecs.Add(codec); } // Reduce RTP extensions by disabling transport MID and BWE related ones. consumerParams.HeaderExtensions = consumableParams.HeaderExtensions .Where(ext => (ext.Uri != "urn:ietf:parameters:rtp-hdrext:sdes:mid" && ext.Uri != "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time" && ext.Uri != "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01" )).ToList(); var consumableEncodings = consumableParams.Encodings.DeepClone <List <RtpEncodingParameters> >(); foreach (var encoding in consumableEncodings) { if (!enableRtx) { // 在 Node.js 实现中,delete 了 rtx 。 encoding.Rtx = null; } consumerParams.Encodings.Add(encoding); } return(consumerParams); }
/// <summary> /// Validates RtpParameters. It may modify given data by adding missing /// fields with default values. /// It throws if invalid. /// </summary> public static void ValidateRtpParameters(RtpParameters parameters) { if (parameters == null) { throw new ArgumentNullException(nameof(parameters)); } // mid is optional. // 在 Node.js 实现中,判断了 mid 的数据类型。在强类型语言中不需要。 // codecs is mandatory. if (parameters.Codecs == null) { throw new ArgumentException(nameof(parameters.Codecs)); } foreach (var codec in parameters.Codecs) { ValidateRtpCodecParameters(codec); } // headerExtensions is optional. If unset, fill with an empty array. if (parameters.HeaderExtensions == null) { parameters.HeaderExtensions = new List <RtpHeaderExtensionParameters>(); } foreach (var ext in parameters.HeaderExtensions) { ValidateRtpHeaderExtensionParameters(ext); } // encodings is optional. If unset, fill with an empty array. if (parameters.Encodings == null) { parameters.Encodings = new List <RtpEncodingParameters>(); } foreach (var encoding in parameters.Encodings) { ValidateRtpEncodingParameters(encoding); } // rtcp is optional. If unset, fill with an empty object. // TODO: (alby)强类型无法转 Empty 对象,注意对象属性默认值可能导致的问题。 if (parameters.Rtcp == null) { parameters.Rtcp = new RtcpParameters(); } ValidateRtcpParameters(parameters.Rtcp); }
/// <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); }