use crate::rtp_parameters::{
MediaKind, MimeType, MimeTypeVideo, RtcpFeedback, RtcpParameters, RtpCapabilities,
RtpCapabilitiesFinalized, RtpCodecCapability, RtpCodecCapabilityFinalized, RtpCodecParameters,
RtpCodecParametersParameters, RtpCodecParametersParametersValue, RtpEncodingParameters,
RtpEncodingParametersRtx, RtpHeaderExtensionDirection, RtpHeaderExtensionParameters,
RtpHeaderExtensionUri, RtpParameters,
};
use crate::scalability_modes::ScalabilityMode;
use crate::supported_rtp_capabilities;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::BTreeMap;
use std::mem;
use std::num::{NonZeroU32, NonZeroU8};
use std::ops::Deref;
use thiserror::Error;
const DYNAMIC_PAYLOAD_TYPES: &[u8] = &[
100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118,
119, 120, 121, 122, 123, 124, 125, 126, 127, 96, 97, 98, 99,
];
#[derive(Debug, Default, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RtpMappingCodec {
pub payload_type: u8,
pub mapped_payload_type: u8,
}
#[derive(Debug, Default, Clone, Ord, PartialOrd, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RtpMappingEncoding {
#[serde(skip_serializing_if = "Option::is_none")]
pub ssrc: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scalability_mode: Option<String>,
pub mapped_ssrc: u32,
}
#[derive(Debug, Default, Clone, Ord, PartialOrd, Eq, PartialEq, Deserialize, Serialize)]
pub struct RtpMapping {
pub codecs: Vec<RtpMappingCodec>,
pub encodings: Vec<RtpMappingEncoding>,
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum RtpParametersError {
#[error("invalid codec apt parameter {0}")]
InvalidAptParameter(String),
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum RtpCapabilitiesError {
#[error("media codec not supported [mime_type:{mime_type:?}")]
UnsupportedCodec { mime_type: MimeType },
#[error("cannot allocate more dynamic codec payload types")]
CannotAllocate,
#[error("invalid codec apt parameter {0}")]
InvalidAptParameter(String),
#[error("duplicated {0}")]
DuplicatedPreferredPayloadType(u8),
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum RtpParametersMappingError {
#[error("unsupported codec [mime_type:{mime_type:?}, payloadType:{payload_type}]")]
UnsupportedCodec {
mime_type: MimeType,
payload_type: u8,
},
#[error("no RTX codec for capability codec PT {preferred_payload_type}")]
UnsupportedRTXCodec { preferred_payload_type: u8 },
#[error("missing media codec found for RTX PT {payload_type}")]
MissingMediaCodecForRTX { payload_type: u8 },
}
#[derive(Debug, Error, Eq, PartialEq)]
pub enum ConsumerRtpParametersError {
#[error("invalid capabilities: {0}")]
InvalidCapabilities(RtpCapabilitiesError),
#[error("no compatible media codecs")]
NoCompatibleMediaCodecs,
}
fn generate_ssrc() -> u32 {
fastrand::u32(100000000..999999999)
}
pub(crate) fn validate_rtp_parameters(
rtp_parameters: &RtpParameters,
) -> Result<(), RtpParametersError> {
for codec in rtp_parameters.codecs.iter() {
validate_rtp_codec_parameters(codec)?;
}
Ok(())
}
fn validate_rtp_codec_parameters(codec: &RtpCodecParameters) -> Result<(), RtpParametersError> {
for (key, value) in codec.parameters().iter() {
if key.as_str() == "apt" {
match value {
RtpCodecParametersParametersValue::Number(_) => {
}
RtpCodecParametersParametersValue::String(string) => {
return Err(RtpParametersError::InvalidAptParameter(string.clone()));
}
}
}
}
Ok(())
}
fn validate_rtp_codec_capability(codec: &RtpCodecCapability) -> Result<(), RtpCapabilitiesError> {
for (key, value) in codec.parameters().iter() {
if key.as_str() == "apt" {
match value {
RtpCodecParametersParametersValue::Number(_) => {
}
RtpCodecParametersParametersValue::String(string) => {
return Err(RtpCapabilitiesError::InvalidAptParameter(string.clone()));
}
}
}
}
Ok(())
}
pub(crate) fn validate_rtp_capabilities(
caps: &RtpCapabilities,
) -> Result<(), RtpCapabilitiesError> {
for codec in caps.codecs.iter() {
validate_rtp_codec_capability(codec)?;
}
Ok(())
}
pub(crate) fn generate_router_rtp_capabilities(
mut media_codecs: Vec<RtpCodecCapability>,
) -> Result<RtpCapabilitiesFinalized, RtpCapabilitiesError> {
let supported_rtp_capabilities = supported_rtp_capabilities::get_supported_rtp_capabilities();
validate_rtp_capabilities(&supported_rtp_capabilities)?;
let mut dynamic_payload_types = Vec::from(DYNAMIC_PAYLOAD_TYPES);
let mut caps = RtpCapabilitiesFinalized {
codecs: vec![],
header_extensions: supported_rtp_capabilities.header_extensions,
fec_mechanisms: vec![],
};
for media_codec in media_codecs.iter_mut() {
validate_rtp_codec_capability(media_codec)?;
let codec = match supported_rtp_capabilities
.codecs
.iter()
.find(|supported_codec| {
match_codecs(media_codec.deref().into(), (*supported_codec).into(), false).is_ok()
}) {
Some(codec) => codec,
None => {
return Err(RtpCapabilitiesError::UnsupportedCodec {
mime_type: media_codec.mime_type(),
});
}
};
let preferred_payload_type = match media_codec.preferred_payload_type() {
Some(preferred_payload_type) => {
dynamic_payload_types.retain(|&pt| pt != preferred_payload_type);
preferred_payload_type
}
None => {
match codec.preferred_payload_type() {
Some(preferred_payload_type) => {
preferred_payload_type
}
None => {
if dynamic_payload_types.is_empty() {
return Err(RtpCapabilitiesError::CannotAllocate);
}
dynamic_payload_types.remove(0)
}
}
}
};
for codec in caps.codecs.iter() {
if codec.preferred_payload_type() == preferred_payload_type {
return Err(RtpCapabilitiesError::DuplicatedPreferredPayloadType(
preferred_payload_type,
));
}
}
let codec_finalized = match codec {
RtpCodecCapability::Audio {
mime_type,
preferred_payload_type: _,
clock_rate,
channels,
parameters,
rtcp_feedback,
} => RtpCodecCapabilityFinalized::Audio {
mime_type: *mime_type,
preferred_payload_type,
clock_rate: *clock_rate,
channels: *channels,
parameters: {
let mut parameters = parameters.clone();
parameters.extend(mem::take(media_codec.parameters_mut()));
parameters
},
rtcp_feedback: rtcp_feedback.clone(),
},
RtpCodecCapability::Video {
mime_type,
preferred_payload_type: _,
clock_rate,
parameters,
rtcp_feedback,
} => RtpCodecCapabilityFinalized::Video {
mime_type: *mime_type,
preferred_payload_type,
clock_rate: *clock_rate,
parameters: {
let mut parameters = parameters.clone();
parameters.extend(mem::take(media_codec.parameters_mut()));
parameters
},
rtcp_feedback: rtcp_feedback.clone(),
},
};
if matches!(codec_finalized, RtpCodecCapabilityFinalized::Video {..}) {
if dynamic_payload_types.is_empty() {
return Err(RtpCapabilitiesError::CannotAllocate);
}
let payload_type = dynamic_payload_types.remove(0);
let rtx_codec = RtpCodecCapabilityFinalized::Video {
mime_type: MimeTypeVideo::RTX,
preferred_payload_type: payload_type,
clock_rate: codec_finalized.clock_rate(),
parameters: RtpCodecParametersParameters::from([(
"apt",
codec_finalized.preferred_payload_type().into(),
)]),
rtcp_feedback: vec![],
};
caps.codecs.push(codec_finalized);
caps.codecs.push(rtx_codec);
} else {
caps.codecs.push(codec_finalized);
}
}
Ok(caps)
}
pub(crate) fn get_producer_rtp_parameters_mapping(
rtp_parameters: &RtpParameters,
rtp_capabilities: &RtpCapabilitiesFinalized,
) -> Result<RtpMapping, RtpParametersMappingError> {
let mut rtp_mapping = RtpMapping::default();
let mut codec_to_cap_codec =
BTreeMap::<&RtpCodecParameters, Cow<RtpCodecCapabilityFinalized>>::new();
for codec in rtp_parameters.codecs.iter() {
if codec.is_rtx() {
continue;
}
match rtp_capabilities.codecs.iter().find_map(|cap_codec| {
match_codecs(codec.into(), cap_codec.into(), true)
.ok()
.map(|profile_level_id| {
if let Some(profile_level_id) = profile_level_id {
let mut cap_codec = cap_codec.clone();
cap_codec
.parameters_mut()
.insert("profile-level-id", profile_level_id);
Cow::Owned(cap_codec)
} else {
Cow::Borrowed(cap_codec)
}
})
}) {
Some(matched_codec_capability) => {
codec_to_cap_codec.insert(codec, matched_codec_capability);
}
None => {
return Err(RtpParametersMappingError::UnsupportedCodec {
mime_type: codec.mime_type(),
payload_type: codec.payload_type(),
});
}
}
}
for codec in rtp_parameters.codecs.iter() {
if !codec.is_rtx() {
continue;
}
let associated_media_codec = rtp_parameters.codecs.iter().find(|media_codec| {
let media_codec_payload_type = media_codec.payload_type();
let codec_parameters_apt = codec.parameters().get(&"apt".to_string());
match codec_parameters_apt {
Some(RtpCodecParametersParametersValue::Number(apt)) => {
media_codec_payload_type as u32 == *apt
}
_ => false,
}
});
match associated_media_codec {
Some(associated_media_codec) => {
let cap_media_codec = codec_to_cap_codec.get(associated_media_codec).unwrap();
let associated_cap_rtx_codec = rtp_capabilities.codecs.iter().find(|cap_codec| {
if !cap_codec.is_rtx() {
return false;
}
let cap_codec_parameters_apt = cap_codec.parameters().get(&"apt".to_string());
match cap_codec_parameters_apt {
Some(RtpCodecParametersParametersValue::Number(apt)) => {
cap_media_codec.preferred_payload_type() as u32 == *apt
}
_ => false,
}
});
match associated_cap_rtx_codec {
Some(associated_cap_rtx_codec) => {
codec_to_cap_codec.insert(codec, Cow::Borrowed(associated_cap_rtx_codec));
}
None => {
return Err(RtpParametersMappingError::UnsupportedRTXCodec {
preferred_payload_type: cap_media_codec.preferred_payload_type(),
});
}
}
}
None => {
return Err(RtpParametersMappingError::MissingMediaCodecForRTX {
payload_type: codec.payload_type(),
});
}
}
}
for (codec, cap_codec) in codec_to_cap_codec {
rtp_mapping.codecs.push(RtpMappingCodec {
payload_type: codec.payload_type(),
mapped_payload_type: cap_codec.preferred_payload_type(),
});
}
let mut mapped_ssrc: u32 = generate_ssrc();
for encoding in rtp_parameters.encodings.iter() {
rtp_mapping.encodings.push(RtpMappingEncoding {
ssrc: encoding.ssrc,
rid: encoding.rid.clone(),
scalability_mode: encoding.scalability_mode.clone(),
mapped_ssrc,
});
mapped_ssrc += 1;
}
Ok(rtp_mapping)
}
pub(crate) fn get_consumable_rtp_parameters(
kind: MediaKind,
params: &RtpParameters,
caps: &RtpCapabilitiesFinalized,
rtp_mapping: &RtpMapping,
) -> RtpParameters {
let mut consumable_params = RtpParameters::default();
for codec in params.codecs.iter() {
if codec.is_rtx() {
continue;
}
let consumable_codec_pt = rtp_mapping
.codecs
.iter()
.find(|entry| entry.payload_type == codec.payload_type())
.unwrap()
.mapped_payload_type;
let consumable_codec = match caps
.codecs
.iter()
.find(|cap_codec| cap_codec.preferred_payload_type() == consumable_codec_pt)
.unwrap()
{
RtpCodecCapabilityFinalized::Audio {
mime_type,
preferred_payload_type,
clock_rate,
channels,
parameters: _,
rtcp_feedback,
} => {
RtpCodecParameters::Audio {
mime_type: *mime_type,
payload_type: *preferred_payload_type,
clock_rate: *clock_rate,
channels: *channels,
parameters: codec.parameters().clone(),
rtcp_feedback: rtcp_feedback.clone(),
}
}
RtpCodecCapabilityFinalized::Video {
mime_type,
preferred_payload_type,
clock_rate,
parameters: _,
rtcp_feedback,
} => {
RtpCodecParameters::Video {
mime_type: *mime_type,
payload_type: *preferred_payload_type,
clock_rate: *clock_rate,
parameters: codec.parameters().clone(),
rtcp_feedback: rtcp_feedback.clone(),
}
}
};
let consumable_cap_rtx_codec = caps.codecs.iter().find(|cap_rtx_codec| {
if !cap_rtx_codec.is_rtx() {
return false;
}
let cap_rtx_codec_parameters_apt = cap_rtx_codec.parameters().get(&"apt".to_string());
match cap_rtx_codec_parameters_apt {
Some(RtpCodecParametersParametersValue::Number(apt)) => {
*apt as u8 == consumable_codec.payload_type()
}
_ => false,
}
});
consumable_params.codecs.push(consumable_codec);
if let Some(consumable_cap_rtx_codec) = consumable_cap_rtx_codec {
let consumable_rtx_codec = match consumable_cap_rtx_codec {
RtpCodecCapabilityFinalized::Audio {
mime_type,
preferred_payload_type,
clock_rate,
channels,
parameters,
rtcp_feedback,
} => RtpCodecParameters::Audio {
mime_type: *mime_type,
payload_type: *preferred_payload_type,
clock_rate: *clock_rate,
channels: *channels,
parameters: parameters.clone(),
rtcp_feedback: rtcp_feedback.clone(),
},
RtpCodecCapabilityFinalized::Video {
mime_type,
preferred_payload_type,
clock_rate,
parameters,
rtcp_feedback,
} => RtpCodecParameters::Video {
mime_type: *mime_type,
payload_type: *preferred_payload_type,
clock_rate: *clock_rate,
parameters: parameters.clone(),
rtcp_feedback: rtcp_feedback.clone(),
},
};
consumable_params.codecs.push(consumable_rtx_codec);
}
}
for cap_ext in caps.header_extensions.iter() {
match cap_ext.kind {
Some(cap_ext_kind) => {
if cap_ext_kind != kind {
continue;
}
}
None => {
continue;
}
}
if !matches!(
cap_ext.direction,
RtpHeaderExtensionDirection::SendRecv | RtpHeaderExtensionDirection::SendOnly
) {
continue;
}
let consumable_ext = RtpHeaderExtensionParameters {
uri: cap_ext.uri,
id: cap_ext.preferred_id,
encrypt: cap_ext.preferred_encrypt,
};
consumable_params.header_extensions.push(consumable_ext);
}
for (consumable_encoding, mapped_ssrc) in params.encodings.iter().zip(
rtp_mapping
.encodings
.iter()
.map(|encoding| encoding.mapped_ssrc),
) {
let mut consumable_encoding = consumable_encoding.clone();
consumable_encoding.rid.take();
consumable_encoding.rtx.take();
consumable_encoding.codec_payload_type.take();
consumable_encoding.ssrc = Some(mapped_ssrc);
consumable_params.encodings.push(consumable_encoding);
}
consumable_params.rtcp = RtcpParameters {
cname: params.rtcp.cname.clone(),
reduced_size: true,
mux: Some(true),
};
consumable_params
}
pub(crate) fn can_consume(
consumable_params: &RtpParameters,
caps: &RtpCapabilities,
) -> Result<bool, RtpCapabilitiesError> {
validate_rtp_capabilities(&caps)?;
let mut matching_codecs = Vec::<&RtpCodecParameters>::new();
for codec in consumable_params.codecs.iter() {
if caps
.codecs
.iter()
.any(|cap_codec| match_codecs(cap_codec.deref().into(), codec.into(), true).is_ok())
{
matching_codecs.push(codec);
}
}
Ok(matching_codecs
.get(0)
.map(|codec| !codec.is_rtx())
.unwrap_or_default())
}
pub(crate) fn get_consumer_rtp_parameters(
consumable_params: &RtpParameters,
caps: RtpCapabilities,
) -> Result<RtpParameters, ConsumerRtpParametersError> {
let mut consumer_params = RtpParameters::default();
consumer_params.rtcp = consumable_params.rtcp.clone();
for cap_codec in caps.codecs.iter() {
validate_rtp_codec_capability(cap_codec)
.map_err(ConsumerRtpParametersError::InvalidCapabilities)?;
}
let mut rtx_supported = false;
for mut codec in consumable_params.codecs.clone() {
if let Some(matched_cap_codec) = caps
.codecs
.iter()
.find(|cap_codec| match_codecs(cap_codec.deref().into(), (&codec).into(), true).is_ok())
{
*codec.rtcp_feedback_mut() = matched_cap_codec.rtcp_feedback().clone();
consumer_params.codecs.push(codec);
}
}
let mut remove_codecs = Vec::new();
for (idx, codec) in consumer_params.codecs.iter().enumerate() {
if codec.is_rtx() {
let associated_media_codec = consumer_params.codecs.iter().find(|media_codec| {
match codec.parameters().get("apt") {
Some(RtpCodecParametersParametersValue::Number(apt)) => {
media_codec.payload_type() as u32 == *apt
}
_ => false,
}
});
if associated_media_codec.is_some() {
rtx_supported = true;
} else {
remove_codecs.push(idx);
}
}
}
for idx in remove_codecs.into_iter().rev() {
consumer_params.codecs.remove(idx);
}
if consumer_params.codecs.is_empty() || consumer_params.codecs[0].is_rtx() {
return Err(ConsumerRtpParametersError::NoCompatibleMediaCodecs);
}
consumer_params.header_extensions = consumable_params
.header_extensions
.iter()
.filter(|ext| {
caps.header_extensions
.iter()
.any(|cap_ext| cap_ext.preferred_id == ext.id && cap_ext.uri == ext.uri)
})
.cloned()
.collect();
if consumer_params
.header_extensions
.iter()
.any(|ext| ext.uri == RtpHeaderExtensionUri::TransportWideCCDraft01)
{
for codec in consumer_params.codecs.iter_mut() {
codec
.rtcp_feedback_mut()
.retain(|fb| fb != &RtcpFeedback::GoogRemb);
}
} else if consumer_params
.header_extensions
.iter()
.any(|ext| ext.uri == RtpHeaderExtensionUri::AbsSendTime)
{
for codec in consumer_params.codecs.iter_mut() {
codec
.rtcp_feedback_mut()
.retain(|fb| fb != &RtcpFeedback::TransportCC);
}
} else {
for codec in consumer_params.codecs.iter_mut() {
codec
.rtcp_feedback_mut()
.retain(|fb| !matches!(fb, RtcpFeedback::GoogRemb | RtcpFeedback::TransportCC));
}
}
let mut consumer_encoding = RtpEncodingParameters {
ssrc: Some(generate_ssrc()),
..RtpEncodingParameters::default()
};
if rtx_supported {
consumer_encoding.rtx = Some(RtpEncodingParametersRtx {
ssrc: generate_ssrc(),
});
}
let mut scalability_mode = consumable_params
.encodings
.iter()
.find_map(|encoding| encoding.scalability_mode.clone());
if consumable_params.encodings.len() > 1 {
scalability_mode = Some(format!(
"S{}T{}",
consumable_params.encodings.len(),
scalability_mode
.as_ref()
.map(|s| s.parse::<ScalabilityMode>().ok())
.flatten()
.unwrap_or_default()
.temporal_layers
));
}
consumer_encoding.scalability_mode = scalability_mode;
consumer_encoding.max_bitrate = consumable_params
.encodings
.iter()
.map(|encoding| encoding.max_bitrate)
.max()
.flatten();
consumer_params.encodings.push(consumer_encoding);
consumer_params.rtcp = consumable_params.rtcp.clone();
Ok(consumer_params)
}
pub(crate) fn get_pipe_consumer_rtp_parameters(
consumable_params: &RtpParameters,
enable_rtx: bool,
) -> RtpParameters {
let mut consumer_params = RtpParameters {
mid: None,
codecs: vec![],
header_extensions: vec![],
encodings: vec![],
rtcp: consumable_params.rtcp.clone(),
};
for codec in consumable_params.codecs.iter() {
if !enable_rtx && codec.is_rtx() {
continue;
}
let mut codec = codec.clone();
codec.rtcp_feedback_mut().retain(|fb| {
matches!(fb, RtcpFeedback::NackPli | RtcpFeedback::CcmFir)
|| (enable_rtx && fb == &RtcpFeedback::Nack)
});
consumer_params.codecs.push(codec);
}
consumer_params.header_extensions = consumable_params
.header_extensions
.iter()
.filter(|ext| {
!matches!(
ext.uri,
RtpHeaderExtensionUri::MID
| RtpHeaderExtensionUri::AbsSendTime
| RtpHeaderExtensionUri::TransportWideCCDraft01
)
})
.cloned()
.collect();
for ((encoding, ssrc), rtx_ssrc) in consumable_params
.encodings
.iter()
.zip(generate_ssrc()..)
.zip(generate_ssrc()..)
{
consumer_params.encodings.push(RtpEncodingParameters {
ssrc: Some(ssrc),
rtx: if enable_rtx {
Some(RtpEncodingParametersRtx { ssrc: rtx_ssrc })
} else {
None
},
..encoding.clone()
});
}
consumer_params
}
struct CodecToMatch<'a> {
channels: Option<NonZeroU8>,
clock_rate: NonZeroU32,
mime_type: MimeType,
parameters: &'a RtpCodecParametersParameters,
}
impl<'a> From<&'a RtpCodecCapability> for CodecToMatch<'a> {
fn from(rtp_codec_capability: &'a RtpCodecCapability) -> Self {
match rtp_codec_capability {
RtpCodecCapability::Audio {
mime_type,
channels,
clock_rate,
parameters,
..
} => Self {
channels: Some(*channels),
clock_rate: *clock_rate,
mime_type: MimeType::Audio(*mime_type),
parameters,
},
RtpCodecCapability::Video {
mime_type,
clock_rate,
parameters,
..
} => Self {
channels: None,
clock_rate: *clock_rate,
mime_type: MimeType::Video(*mime_type),
parameters,
},
}
}
}
impl<'a> From<&'a RtpCodecCapabilityFinalized> for CodecToMatch<'a> {
fn from(rtp_codec_capability: &'a RtpCodecCapabilityFinalized) -> Self {
match rtp_codec_capability {
RtpCodecCapabilityFinalized::Audio {
mime_type,
channels,
clock_rate,
parameters,
..
} => Self {
channels: Some(*channels),
clock_rate: *clock_rate,
mime_type: MimeType::Audio(*mime_type),
parameters,
},
RtpCodecCapabilityFinalized::Video {
mime_type,
clock_rate,
parameters,
..
} => Self {
channels: None,
clock_rate: *clock_rate,
mime_type: MimeType::Video(*mime_type),
parameters,
},
}
}
}
impl<'a> From<&'a RtpCodecParameters> for CodecToMatch<'a> {
fn from(rtp_codec_parameters: &'a RtpCodecParameters) -> Self {
match rtp_codec_parameters {
RtpCodecParameters::Audio {
mime_type,
channels,
clock_rate,
parameters,
..
} => Self {
channels: Some(*channels),
clock_rate: *clock_rate,
mime_type: MimeType::Audio(*mime_type),
parameters,
},
RtpCodecParameters::Video {
mime_type,
clock_rate,
parameters,
..
} => Self {
channels: None,
clock_rate: *clock_rate,
mime_type: MimeType::Video(*mime_type),
parameters,
},
}
}
}
fn match_codecs(
codec_a: CodecToMatch,
codec_b: CodecToMatch,
strict: bool,
) -> Result<Option<String>, ()> {
if codec_a.mime_type != codec_b.mime_type {
return Err(());
}
if codec_a.channels != codec_b.channels {
return Err(());
}
if codec_a.clock_rate != codec_b.clock_rate {
return Err(());
}
match codec_a.mime_type {
MimeType::Video(MimeTypeVideo::H264) => {
let packetization_mode_a = codec_a
.parameters
.get("packetization-mode")
.unwrap_or(&RtpCodecParametersParametersValue::Number(0));
let packetization_mode_b = codec_b
.parameters
.get("packetization-mode")
.unwrap_or(&RtpCodecParametersParametersValue::Number(0));
if packetization_mode_a != packetization_mode_b {
return Err(());
}
if strict {
let profile_level_id_a =
codec_a
.parameters
.get("profile-level-id")
.and_then(|p| match p {
RtpCodecParametersParametersValue::String(s) => Some(s.as_str()),
RtpCodecParametersParametersValue::Number(_) => None,
});
let profile_level_id_b =
codec_b
.parameters
.get("profile-level-id")
.and_then(|p| match p {
RtpCodecParametersParametersValue::String(s) => Some(s.as_str()),
RtpCodecParametersParametersValue::Number(_) => None,
});
let (profile_level_id_a, profile_level_id_b) =
match h264_profile_level_id::is_same_profile(
profile_level_id_a,
profile_level_id_b,
) {
Some((profile_level_id_a, profile_level_id_b)) => {
(profile_level_id_a, profile_level_id_b)
}
None => {
return Err(());
}
};
let selected_profile_level_id =
h264_profile_level_id::generate_profile_level_id_for_answer(
Some(profile_level_id_a),
codec_a
.parameters
.get("level-asymmetry-allowed")
.map(|p| p == &RtpCodecParametersParametersValue::Number(1))
.unwrap_or_default(),
Some(profile_level_id_b),
codec_b
.parameters
.get("level-asymmetry-allowed")
.map(|p| p == &RtpCodecParametersParametersValue::Number(1))
.unwrap_or_default(),
);
return match selected_profile_level_id {
Ok(selected_profile_level_id) => {
Ok(Some(selected_profile_level_id.to_string()))
}
Err(_) => Err(()),
};
}
}
MimeType::Video(MimeTypeVideo::VP9) => {
if strict {
let profile_id_a = codec_a
.parameters
.get("profile-id")
.unwrap_or(&RtpCodecParametersParametersValue::Number(0));
let profile_id_b = codec_b
.parameters
.get("profile-id")
.unwrap_or(&RtpCodecParametersParametersValue::Number(0));
if profile_id_a != profile_id_b {
return Err(());
}
}
}
_ => {}
}
Ok(None)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rtp_parameters::{MimeTypeAudio, RtpHeaderExtension};
use std::iter;
#[test]
fn generate_router_rtp_capabilities_succeeds() {
let media_codecs = vec![
RtpCodecCapability::Audio {
mime_type: MimeTypeAudio::Opus,
preferred_payload_type: None,
clock_rate: NonZeroU32::new(48000).unwrap(),
channels: NonZeroU8::new(2).unwrap(),
parameters: RtpCodecParametersParameters::from([
("useinbandfec", 1u32.into()),
("foo", "bar".into()),
]),
rtcp_feedback: vec![],
},
RtpCodecCapability::Video {
mime_type: MimeTypeVideo::VP8,
preferred_payload_type: Some(125), clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::new(),
rtcp_feedback: vec![],
},
RtpCodecCapability::Video {
mime_type: MimeTypeVideo::H264,
preferred_payload_type: None,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([
("level-asymmetry-allowed", 1u32.into()),
("profile-level-id", "42e01f".into()),
("foo", "bar".into()),
]),
rtcp_feedback: vec![], },
];
let rtp_capabilities = generate_router_rtp_capabilities(media_codecs)
.expect("Failed to generate router RTP capabilities");
assert_eq!(
rtp_capabilities.codecs,
vec![
RtpCodecCapabilityFinalized::Audio {
mime_type: MimeTypeAudio::Opus,
preferred_payload_type: 100, clock_rate: NonZeroU32::new(48000).unwrap(),
channels: NonZeroU8::new(2).unwrap(),
parameters: RtpCodecParametersParameters::from([
("useinbandfec", 1u32.into()),
("foo", "bar".into()),
]),
rtcp_feedback: vec![RtcpFeedback::TransportCC],
},
RtpCodecCapabilityFinalized::Video {
mime_type: MimeTypeVideo::VP8,
preferred_payload_type: 125,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::new(),
rtcp_feedback: vec![
RtcpFeedback::Nack,
RtcpFeedback::NackPli,
RtcpFeedback::CcmFir,
RtcpFeedback::GoogRemb,
RtcpFeedback::TransportCC
],
},
RtpCodecCapabilityFinalized::Video {
mime_type: MimeTypeVideo::RTX,
preferred_payload_type: 101, clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([("apt", 125u32.into())]),
rtcp_feedback: vec![],
},
RtpCodecCapabilityFinalized::Video {
mime_type: MimeTypeVideo::H264,
preferred_payload_type: 102, clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([
("packetization-mode", 0u32.into()),
("level-asymmetry-allowed", 1u32.into()),
("profile-level-id", "42e01f".into()),
("foo", "bar".into()),
]),
rtcp_feedback: vec![
RtcpFeedback::Nack,
RtcpFeedback::NackPli,
RtcpFeedback::CcmFir,
RtcpFeedback::GoogRemb,
RtcpFeedback::TransportCC,
],
},
RtpCodecCapabilityFinalized::Video {
mime_type: MimeTypeVideo::RTX,
preferred_payload_type: 103,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([("apt", 102u32.into())]),
rtcp_feedback: vec![],
},
]
);
}
#[test]
fn generate_router_rtp_capabilities_unsupported() {
assert!(matches!(
generate_router_rtp_capabilities(vec![RtpCodecCapability::Audio {
mime_type: MimeTypeAudio::Opus,
preferred_payload_type: None,
clock_rate: NonZeroU32::new(48000).unwrap(),
channels: NonZeroU8::new(1).unwrap(),
parameters: RtpCodecParametersParameters::new(),
rtcp_feedback: vec![],
}]),
Err(RtpCapabilitiesError::UnsupportedCodec { .. })
));
assert!(matches!(
generate_router_rtp_capabilities(vec![RtpCodecCapability::Video {
mime_type: MimeTypeVideo::H264,
preferred_payload_type: None,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([(
"packetization-mode",
5u32.into()
)]),
rtcp_feedback: vec![],
}]),
Err(RtpCapabilitiesError::UnsupportedCodec { .. })
));
}
#[test]
fn generate_router_rtp_capabilities_too_many_codecs() {
assert!(matches!(
generate_router_rtp_capabilities(
iter::repeat(RtpCodecCapability::Audio {
mime_type: MimeTypeAudio::Opus,
preferred_payload_type: None,
clock_rate: NonZeroU32::new(48000).unwrap(),
channels: NonZeroU8::new(2).unwrap(),
parameters: RtpCodecParametersParameters::new(),
rtcp_feedback: vec![],
})
.take(100)
.collect::<Vec<_>>()
),
Err(RtpCapabilitiesError::CannotAllocate)
));
}
#[test]
fn get_producer_rtp_parameters_mapping_get_consumable_rtp_parameters_get_consumer_rtp_parameters_get_pipe_consumer_rtp_parameters_succeeds(
) {
let media_codecs = vec![
RtpCodecCapability::Audio {
mime_type: MimeTypeAudio::Opus,
preferred_payload_type: None,
clock_rate: NonZeroU32::new(48000).unwrap(),
channels: NonZeroU8::new(2).unwrap(),
parameters: RtpCodecParametersParameters::from([
("useinbandfec", 1u32.into()),
("foo", "bar".into()),
]),
rtcp_feedback: vec![],
},
RtpCodecCapability::Video {
mime_type: MimeTypeVideo::H264,
preferred_payload_type: None,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([
("level-asymmetry-allowed", 1u32.into()),
("packetization-mode", 1u32.into()),
("profile-level-id", "4d0032".into()),
("foo", "lalala".into()),
]),
rtcp_feedback: vec![],
},
];
let router_rtp_capabilities = generate_router_rtp_capabilities(media_codecs)
.expect("Failed to generate router RTP capabilities");
let rtp_parameters = RtpParameters {
mid: None,
codecs: vec![
RtpCodecParameters::Video {
mime_type: MimeTypeVideo::H264,
payload_type: 111,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([
("foo", 1234u32.into()),
("packetization-mode", 1u32.into()),
("profile-level-id", "4d0032".into()),
]),
rtcp_feedback: vec![
RtcpFeedback::Nack,
RtcpFeedback::NackPli,
RtcpFeedback::GoogRemb,
],
},
RtpCodecParameters::Video {
mime_type: MimeTypeVideo::RTX,
payload_type: 112,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([("apt", 111u32.into())]),
rtcp_feedback: vec![],
},
],
header_extensions: vec![
RtpHeaderExtensionParameters {
uri: RtpHeaderExtensionUri::MID,
id: 1,
encrypt: false,
},
RtpHeaderExtensionParameters {
uri: RtpHeaderExtensionUri::VideoOrientation,
id: 2,
encrypt: false,
},
],
encodings: vec![
RtpEncodingParameters {
ssrc: Some(11111111),
rtx: Some(RtpEncodingParametersRtx { ssrc: 11111112 }),
scalability_mode: Some("L1T3".to_string()),
max_bitrate: Some(111111),
..RtpEncodingParameters::default()
},
RtpEncodingParameters {
ssrc: Some(21111111),
rtx: Some(RtpEncodingParametersRtx { ssrc: 21111112 }),
scalability_mode: Some("L1T3".to_string()),
max_bitrate: Some(222222),
..RtpEncodingParameters::default()
},
RtpEncodingParameters {
rid: Some("high".to_string()),
scalability_mode: Some("L1T3".to_string()),
max_bitrate: Some(333333),
..RtpEncodingParameters::default()
},
],
rtcp: RtcpParameters {
cname: Some("qwerty1234".to_string()),
..RtcpParameters::default()
},
};
let rtp_mapping =
get_producer_rtp_parameters_mapping(&rtp_parameters, &router_rtp_capabilities)
.expect("Failed to get producer RTP parameters mapping");
assert_eq!(
rtp_mapping.codecs,
vec![
RtpMappingCodec {
payload_type: 111,
mapped_payload_type: 101
},
RtpMappingCodec {
payload_type: 112,
mapped_payload_type: 102
},
]
);
assert_eq!(rtp_mapping.encodings.get(0).unwrap().ssrc, Some(11111111));
assert_eq!(rtp_mapping.encodings.get(0).unwrap().rid, None);
assert_eq!(rtp_mapping.encodings.get(1).unwrap().ssrc, Some(21111111));
assert_eq!(rtp_mapping.encodings.get(1).unwrap().rid, None);
assert_eq!(rtp_mapping.encodings.get(2).unwrap().ssrc, None);
assert_eq!(
rtp_mapping.encodings.get(2).unwrap().rid,
Some("high".to_string())
);
let consumable_rtp_parameters = get_consumable_rtp_parameters(
MediaKind::Video,
&rtp_parameters,
&router_rtp_capabilities,
&rtp_mapping,
);
assert_eq!(
consumable_rtp_parameters.codecs,
vec![
RtpCodecParameters::Video {
mime_type: MimeTypeVideo::H264,
payload_type: 101,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([
("foo", 1234u32.into()),
("packetization-mode", 1u32.into()),
("profile-level-id", "4d0032".into()),
]),
rtcp_feedback: vec![
RtcpFeedback::Nack,
RtcpFeedback::NackPli,
RtcpFeedback::CcmFir,
RtcpFeedback::GoogRemb,
RtcpFeedback::TransportCC,
],
},
RtpCodecParameters::Video {
mime_type: MimeTypeVideo::RTX,
payload_type: 102,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([("apt", 101u32.into())]),
rtcp_feedback: vec![],
},
]
);
assert_eq!(
consumable_rtp_parameters.encodings.get(0).unwrap().ssrc,
Some(rtp_mapping.encodings.get(0).unwrap().mapped_ssrc),
);
assert_eq!(
consumable_rtp_parameters
.encodings
.get(0)
.unwrap()
.max_bitrate,
Some(111111),
);
assert_eq!(
consumable_rtp_parameters
.encodings
.get(0)
.unwrap()
.scalability_mode,
Some("L1T3".to_string()),
);
assert_eq!(
consumable_rtp_parameters.encodings.get(1).unwrap().ssrc,
Some(rtp_mapping.encodings.get(1).unwrap().mapped_ssrc),
);
assert_eq!(
consumable_rtp_parameters
.encodings
.get(1)
.unwrap()
.max_bitrate,
Some(222222),
);
assert_eq!(
consumable_rtp_parameters
.encodings
.get(1)
.unwrap()
.scalability_mode,
Some("L1T3".to_string()),
);
assert_eq!(
consumable_rtp_parameters.encodings.get(2).unwrap().ssrc,
Some(rtp_mapping.encodings.get(2).unwrap().mapped_ssrc),
);
assert_eq!(
consumable_rtp_parameters
.encodings
.get(2)
.unwrap()
.max_bitrate,
Some(333333),
);
assert_eq!(
consumable_rtp_parameters
.encodings
.get(2)
.unwrap()
.scalability_mode,
Some("L1T3".to_string()),
);
assert_eq!(
consumable_rtp_parameters.rtcp,
RtcpParameters {
cname: rtp_parameters.rtcp.cname.clone(),
reduced_size: true,
mux: Some(true),
}
);
let remote_rtp_capabilities = RtpCapabilities {
codecs: vec![
RtpCodecCapability::Audio {
mime_type: MimeTypeAudio::Opus,
preferred_payload_type: Some(100),
clock_rate: NonZeroU32::new(48000).unwrap(),
channels: NonZeroU8::new(2).unwrap(),
parameters: RtpCodecParametersParameters::new(),
rtcp_feedback: vec![],
},
RtpCodecCapability::Video {
mime_type: MimeTypeVideo::H264,
preferred_payload_type: Some(101),
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([
("packetization-mode", 1u32.into()),
("profile-level-id", "4d0032".into()),
("baz", "LOLOLO".into()),
]),
rtcp_feedback: vec![
RtcpFeedback::Nack,
RtcpFeedback::NackPli,
RtcpFeedback::Unsupported,
],
},
RtpCodecCapability::Video {
mime_type: MimeTypeVideo::RTX,
preferred_payload_type: Some(102),
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([("apt", 101u32.into())]),
rtcp_feedback: vec![],
},
],
header_extensions: vec![
RtpHeaderExtension {
kind: Some(MediaKind::Audio),
uri: RtpHeaderExtensionUri::MID,
preferred_id: 1,
preferred_encrypt: false,
direction: RtpHeaderExtensionDirection::SendRecv,
},
RtpHeaderExtension {
kind: Some(MediaKind::Video),
uri: RtpHeaderExtensionUri::MID,
preferred_id: 1,
preferred_encrypt: false,
direction: RtpHeaderExtensionDirection::SendRecv,
},
RtpHeaderExtension {
kind: Some(MediaKind::Video),
uri: RtpHeaderExtensionUri::RtpStreamId,
preferred_id: 2,
preferred_encrypt: false,
direction: RtpHeaderExtensionDirection::SendRecv,
},
RtpHeaderExtension {
kind: Some(MediaKind::Audio),
uri: RtpHeaderExtensionUri::AudioLevel,
preferred_id: 8,
preferred_encrypt: false,
direction: RtpHeaderExtensionDirection::SendRecv,
},
RtpHeaderExtension {
kind: Some(MediaKind::Video),
uri: RtpHeaderExtensionUri::VideoOrientation,
preferred_id: 11,
preferred_encrypt: false,
direction: RtpHeaderExtensionDirection::SendRecv,
},
RtpHeaderExtension {
kind: Some(MediaKind::Video),
uri: RtpHeaderExtensionUri::TimeOffset,
preferred_id: 12,
preferred_encrypt: false,
direction: RtpHeaderExtensionDirection::SendRecv,
},
],
fec_mechanisms: vec![],
};
let consumer_rtp_parameters =
get_consumer_rtp_parameters(&consumable_rtp_parameters, remote_rtp_capabilities)
.expect("Failed to get consumer RTP parameters");
assert_eq!(
consumer_rtp_parameters.codecs,
vec![
RtpCodecParameters::Video {
mime_type: MimeTypeVideo::H264,
payload_type: 101,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([
("foo", 1234u32.into()),
("packetization-mode", 1u32.into()),
("profile-level-id", "4d0032".into()),
]),
rtcp_feedback: vec![
RtcpFeedback::Nack,
RtcpFeedback::NackPli,
RtcpFeedback::Unsupported,
],
},
RtpCodecParameters::Video {
mime_type: MimeTypeVideo::RTX,
payload_type: 102,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([("apt", 101u32.into())]),
rtcp_feedback: vec![],
},
]
);
assert_eq!(consumer_rtp_parameters.encodings.len(), 1);
assert!(consumer_rtp_parameters
.encodings
.get(0)
.unwrap()
.ssrc
.is_some());
assert!(consumer_rtp_parameters
.encodings
.get(0)
.unwrap()
.rtx
.is_some());
assert_eq!(
consumer_rtp_parameters
.encodings
.get(0)
.unwrap()
.scalability_mode,
Some("S3T3".to_string()),
);
assert_eq!(
consumer_rtp_parameters
.encodings
.get(0)
.unwrap()
.max_bitrate,
Some(333333),
);
assert_eq!(
consumer_rtp_parameters.header_extensions,
vec![
RtpHeaderExtensionParameters {
uri: RtpHeaderExtensionUri::MID,
id: 1,
encrypt: false,
},
RtpHeaderExtensionParameters {
uri: RtpHeaderExtensionUri::VideoOrientation,
id: 11,
encrypt: false,
},
RtpHeaderExtensionParameters {
uri: RtpHeaderExtensionUri::TimeOffset,
id: 12,
encrypt: false,
},
],
);
assert_eq!(
consumer_rtp_parameters.rtcp,
RtcpParameters {
cname: rtp_parameters.rtcp.cname.clone(),
reduced_size: true,
mux: Some(true),
},
);
let pipe_consumer_rtp_parameters =
get_pipe_consumer_rtp_parameters(&consumable_rtp_parameters, false);
assert_eq!(
pipe_consumer_rtp_parameters.codecs,
vec![RtpCodecParameters::Video {
mime_type: MimeTypeVideo::H264,
payload_type: 101,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([
("foo", 1234u32.into()),
("packetization-mode", 1u32.into()),
("profile-level-id", "4d0032".into()),
]),
rtcp_feedback: vec![RtcpFeedback::NackPli, RtcpFeedback::CcmFir],
}],
);
assert_eq!(pipe_consumer_rtp_parameters.encodings.len(), 3);
assert!(pipe_consumer_rtp_parameters
.encodings
.get(0)
.unwrap()
.ssrc
.is_some());
assert!(pipe_consumer_rtp_parameters
.encodings
.get(0)
.unwrap()
.rtx
.is_none());
assert!(pipe_consumer_rtp_parameters
.encodings
.get(0)
.unwrap()
.max_bitrate
.is_some());
assert_eq!(
pipe_consumer_rtp_parameters
.encodings
.get(0)
.unwrap()
.scalability_mode,
Some("L1T3".to_string()),
);
assert!(pipe_consumer_rtp_parameters
.encodings
.get(1)
.unwrap()
.ssrc
.is_some());
assert!(pipe_consumer_rtp_parameters
.encodings
.get(1)
.unwrap()
.rtx
.is_none());
assert!(pipe_consumer_rtp_parameters
.encodings
.get(1)
.unwrap()
.max_bitrate
.is_some());
assert_eq!(
pipe_consumer_rtp_parameters
.encodings
.get(1)
.unwrap()
.scalability_mode,
Some("L1T3".to_string()),
);
assert!(pipe_consumer_rtp_parameters
.encodings
.get(2)
.unwrap()
.ssrc
.is_some());
assert!(pipe_consumer_rtp_parameters
.encodings
.get(2)
.unwrap()
.rtx
.is_none());
assert!(pipe_consumer_rtp_parameters
.encodings
.get(2)
.unwrap()
.max_bitrate
.is_some());
assert_eq!(
pipe_consumer_rtp_parameters
.encodings
.get(2)
.unwrap()
.scalability_mode,
Some("L1T3".to_string()),
);
assert_eq!(
pipe_consumer_rtp_parameters.rtcp,
RtcpParameters {
cname: rtp_parameters.rtcp.cname.clone(),
reduced_size: true,
mux: Some(true),
},
);
}
#[test]
fn get_producer_rtp_parameters_mapping_unsupported() {
let media_codecs = vec![
RtpCodecCapability::Audio {
mime_type: MimeTypeAudio::Opus,
preferred_payload_type: None,
clock_rate: NonZeroU32::new(48000).unwrap(),
channels: NonZeroU8::new(2).unwrap(),
parameters: RtpCodecParametersParameters::default(),
rtcp_feedback: vec![],
},
RtpCodecCapability::Video {
mime_type: MimeTypeVideo::H264,
preferred_payload_type: None,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::from([
("packetization-mode", 1u32.into()),
("profile-level-id", "640032".into()),
]),
rtcp_feedback: vec![],
},
];
let router_rtp_capabilities = generate_router_rtp_capabilities(media_codecs)
.expect("Failed to generate router RTP capabilities");
let rtp_parameters = RtpParameters {
mid: None,
codecs: vec![RtpCodecParameters::Video {
mime_type: MimeTypeVideo::VP8,
payload_type: 120,
clock_rate: NonZeroU32::new(90000).unwrap(),
parameters: RtpCodecParametersParameters::default(),
rtcp_feedback: vec![RtcpFeedback::Nack, RtcpFeedback::Unsupported],
}],
header_extensions: vec![],
encodings: vec![RtpEncodingParameters {
ssrc: Some(11111111),
..RtpEncodingParameters::default()
}],
rtcp: RtcpParameters {
cname: Some("qwerty1234".to_string()),
..RtcpParameters::default()
},
};
assert!(matches!(
get_producer_rtp_parameters_mapping(&rtp_parameters, &router_rtp_capabilities),
Err(RtpParametersMappingError::UnsupportedCodec { .. }),
));
}
}