use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::identity::IdentityAssurance;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CodecInfo {
pub name: String,
pub clock_rate_hz: u32,
pub channels: u8,
pub fmtp: Option<String>,
}
pub fn default_audio_codec() -> CodecInfo {
CodecInfo {
name: "opus".into(),
clock_rate_hz: 48_000,
channels: 1,
fmtp: None,
}
}
impl CodecInfo {
pub fn from_name_with_defaults(name: &str) -> Self {
let (clock_rate_hz, channels) = match name {
"opus" => (48_000, 1),
"g.711-mu" | "PCMU" | "pcmu" => (8_000, 1),
"g.711-a" | "PCMA" | "pcma" => (8_000, 1),
"g.722" => (16_000, 1),
"g.729" => (8_000, 1),
_ => (48_000, 1),
};
Self {
name: name.to_string(),
clock_rate_hz,
channels,
fmtp: None,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Codec {
pub name: String,
#[serde(default)]
pub params: BTreeMap<String, serde_json::Value>,
}
impl Codec {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
params: BTreeMap::new(),
}
}
}
impl From<CodecInfo> for Codec {
fn from(c: CodecInfo) -> Self {
let mut params = BTreeMap::new();
params.insert("sample_rate".into(), serde_json::json!(c.clock_rate_hz));
params.insert("channels".into(), serde_json::json!(c.channels));
if let Some(fmtp) = c.fmtp {
params.insert("fmtp".into(), serde_json::Value::String(fmtp));
}
Self {
name: c.name,
params,
}
}
}
impl TryFrom<Codec> for CodecInfo {
type Error = &'static str;
fn try_from(c: Codec) -> Result<Self, Self::Error> {
let clock_rate_hz = c
.params
.get("sample_rate")
.and_then(|v| v.as_u64())
.ok_or("missing or invalid sample_rate")? as u32;
let channels = c
.params
.get("channels")
.and_then(|v| v.as_u64())
.unwrap_or(1) as u8;
let fmtp = c
.params
.get("fmtp")
.and_then(|v| v.as_str())
.map(String::from);
Ok(Self {
name: c.name,
clock_rate_hz,
channels,
fmtp,
})
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CapabilityDescriptor {
#[serde(default)]
pub audio_codecs: Vec<CodecInfo>,
#[serde(default)]
pub video_codecs: Vec<CodecInfo>,
#[serde(default)]
pub data_protocols: Vec<DataProtocol>,
#[serde(default)]
pub dtmf_modes: Vec<DtmfMode>,
#[serde(default)]
pub max_streams_per_connection: u16,
#[serde(default)]
pub transport_features: Vec<TransportFeature>,
#[serde(default)]
pub interop: Vec<InteropTarget>,
#[serde(default = "default_assurance_offered")]
pub identity_assurance_offered: AssuranceLevel,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub identity_assurance_required: Option<IdentityAssuranceRequirement>,
#[serde(default)]
pub supports_message_text: bool,
#[serde(default)]
pub supports_srtp: bool,
}
fn default_assurance_offered() -> AssuranceLevel {
AssuranceLevel::Anonymous
}
impl CapabilityDescriptor {
pub fn supports_dtmf_rfc4733(&self) -> bool {
self.dtmf_modes.contains(&DtmfMode::Rfc4733)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DataProtocol {
Text,
Json,
Binary,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DtmfMode {
#[serde(rename = "rfc4733")]
Rfc4733,
#[serde(rename = "info")]
Info,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum TransportFeature {
MediaDatagrams,
ConnectionMigration,
SessionResumption,
#[serde(rename = "0rtt")]
ZeroRtt,
#[serde(rename = "transcode-g711-opus")]
TranscodeG711Opus,
#[serde(other)]
Unknown,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IdentityAssuranceRequirement {
None,
Pseudonymous,
Identified,
TaskScoped,
UserAuthorized,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InteropTarget {
Sip,
Webrtc,
Quic,
Webtransport,
Websocket,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum AssuranceLevel {
#[default]
Anonymous,
Pseudonymous,
Identified,
TaskScoped,
UserAuthorized,
}
impl AssuranceLevel {
pub fn to_core(self) -> Option<&'static str> {
Some(match self {
AssuranceLevel::Anonymous => "anonymous",
AssuranceLevel::Pseudonymous => "pseudonymous",
AssuranceLevel::Identified => "identified",
AssuranceLevel::TaskScoped => "task-scoped",
AssuranceLevel::UserAuthorized => "user-authorized",
})
}
pub fn from_core(assurance: &IdentityAssurance) -> Self {
match assurance {
IdentityAssurance::Anonymous => AssuranceLevel::Anonymous,
IdentityAssurance::Pseudonymous { .. } => AssuranceLevel::Pseudonymous,
IdentityAssurance::Identified { .. } => AssuranceLevel::Identified,
IdentityAssurance::TaskScoped { .. } => AssuranceLevel::TaskScoped,
IdentityAssurance::UserAuthorized { .. } => AssuranceLevel::UserAuthorized,
IdentityAssurance::DtlsFingerprint { .. } => AssuranceLevel::Pseudonymous,
}
}
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct CapabilityIntersection {
pub audio: Option<CodecInfo>,
pub video: Option<CodecInfo>,
pub dtmf_method: Option<DtmfMethod>,
pub messaging_enabled: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum DtmfMethod {
Rfc4733,
SipInfo,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct NegotiatedCodecs {
pub audio: Option<CodecInfo>,
pub video: Option<CodecInfo>,
}
#[derive(Clone, Debug)]
pub enum NegotiationOutcome {
Ok(Vec<NegotiatedStream>),
NotAcceptable488,
}
#[derive(Clone, Debug)]
pub struct NegotiatedStream {
pub stream_id: String,
pub kind: String,
pub direction: String,
pub chosen_codec: Option<String>,
}
#[derive(Clone, Debug)]
pub struct StreamOffer<'a> {
pub id: &'a str,
pub kind: &'a str,
pub direction: &'a str,
pub codec_preferences: &'a [String],
}
pub fn negotiate_streams<'a, I>(
streams_offered: I,
answerer: &CapabilityDescriptor,
) -> NegotiationOutcome
where
I: IntoIterator<Item = StreamOffer<'a>>,
{
let answerer_codecs: std::collections::HashSet<&str> = answerer
.audio_codecs
.iter()
.chain(answerer.video_codecs.iter())
.map(|c| c.name.as_str())
.collect();
let mut results = Vec::new();
let mut any_match = false;
for offer in streams_offered {
let chosen = offer
.codec_preferences
.iter()
.find(|c| answerer_codecs.contains(c.as_str()))
.cloned();
if chosen.is_some() {
any_match = true;
}
results.push(NegotiatedStream {
stream_id: offer.id.to_string(),
kind: offer.kind.to_string(),
direction: offer.direction.to_string(),
chosen_codec: chosen,
});
}
if any_match {
NegotiationOutcome::Ok(results)
} else {
NegotiationOutcome::NotAcceptable488
}
}