1use std::collections::BTreeMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5
6use crate::identity::IdentityAssurance;
7
8#[derive(Clone, Eq, PartialEq, Serialize, Deserialize)]
16pub struct CodecInfo {
17 pub name: String,
18 pub clock_rate_hz: u32,
19 pub channels: u8,
20 pub fmtp: Option<String>,
21}
22
23impl fmt::Debug for CodecInfo {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 formatter
26 .debug_struct("CodecInfo")
27 .field("name_present", &!self.name.is_empty())
28 .field("name_bytes", &self.name.len())
29 .field("clock_rate_hz", &self.clock_rate_hz)
30 .field("channels", &self.channels)
31 .field("fmtp_present", &self.fmtp.is_some())
32 .field(
33 "fmtp_bytes",
34 &self.fmtp.as_ref().map_or(0, std::string::String::len),
35 )
36 .finish()
37 }
38}
39
40pub fn default_audio_codec() -> CodecInfo {
46 CodecInfo {
47 name: "opus".into(),
48 clock_rate_hz: 48_000,
49 channels: 1,
50 fmtp: None,
51 }
52}
53
54impl CodecInfo {
55 pub fn from_name_with_defaults(name: &str) -> Self {
65 let (clock_rate_hz, channels) = match name {
66 "opus" => (48_000, 1),
67 "g.711-mu" | "PCMU" | "pcmu" => (8_000, 1),
68 "g.711-a" | "PCMA" | "pcma" => (8_000, 1),
69 "g.722" => (16_000, 1),
70 "g.729" => (8_000, 1),
71 "pcm_s16le" | "PCM_S16LE" => (16_000, 1),
72 _ => (48_000, 1),
73 };
74 Self {
75 name: name.to_string(),
76 clock_rate_hz,
77 channels,
78 fmtp: None,
79 }
80 }
81}
82
83#[derive(Clone, Serialize, Deserialize)]
88pub struct Codec {
89 pub name: String,
90 #[serde(default)]
91 pub params: BTreeMap<String, serde_json::Value>,
92}
93
94impl fmt::Debug for Codec {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 formatter
97 .debug_struct("Codec")
98 .field("name_present", &!self.name.is_empty())
99 .field("name_bytes", &self.name.len())
100 .field("parameter_count", &self.params.len())
101 .finish()
102 }
103}
104
105impl Codec {
106 pub fn new(name: impl Into<String>) -> Self {
107 Self {
108 name: name.into(),
109 params: BTreeMap::new(),
110 }
111 }
112}
113
114impl From<CodecInfo> for Codec {
115 fn from(c: CodecInfo) -> Self {
116 let mut params = BTreeMap::new();
117 params.insert("sample_rate".into(), serde_json::json!(c.clock_rate_hz));
118 params.insert("channels".into(), serde_json::json!(c.channels));
119 if let Some(fmtp) = c.fmtp {
120 params.insert("fmtp".into(), serde_json::Value::String(fmtp));
121 }
122 Self {
123 name: c.name,
124 params,
125 }
126 }
127}
128
129impl TryFrom<Codec> for CodecInfo {
130 type Error = &'static str;
131 fn try_from(c: Codec) -> Result<Self, Self::Error> {
132 let clock_rate_hz = c
133 .params
134 .get("sample_rate")
135 .and_then(|v| v.as_u64())
136 .ok_or("missing or invalid sample_rate")? as u32;
137 let channels = c
138 .params
139 .get("channels")
140 .and_then(|v| v.as_u64())
141 .unwrap_or(1) as u8;
142 let fmtp = c
143 .params
144 .get("fmtp")
145 .and_then(|v| v.as_str())
146 .map(String::from);
147 Ok(Self {
148 name: c.name,
149 clock_rate_hz,
150 channels,
151 fmtp,
152 })
153 }
154}
155
156#[derive(Clone, Default, Serialize, Deserialize)]
168pub struct CapabilityDescriptor {
169 #[serde(default)]
170 pub audio_codecs: Vec<CodecInfo>,
171
172 #[serde(default)]
173 pub video_codecs: Vec<CodecInfo>,
174
175 #[serde(default)]
176 pub data_protocols: Vec<DataProtocol>,
177
178 #[serde(default)]
179 pub dtmf_modes: Vec<DtmfMode>,
180
181 #[serde(default)]
182 pub max_streams_per_connection: u16,
183
184 #[serde(default)]
185 pub transport_features: Vec<TransportFeature>,
186
187 #[serde(default)]
190 pub interop: Vec<InteropTarget>,
191
192 #[serde(default = "default_assurance_offered")]
195 pub identity_assurance_offered: AssuranceLevel,
196
197 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub identity_assurance_required: Option<IdentityAssuranceRequirement>,
201
202 #[serde(default)]
206 pub supports_message_text: bool,
207
208 #[serde(default)]
211 pub supports_srtp: bool,
212}
213
214impl fmt::Debug for CapabilityDescriptor {
215 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
216 formatter
217 .debug_struct("CapabilityDescriptor")
218 .field("audio_codec_count", &self.audio_codecs.len())
219 .field("video_codec_count", &self.video_codecs.len())
220 .field("data_protocols", &self.data_protocols)
221 .field("dtmf_modes", &self.dtmf_modes)
222 .field(
223 "max_streams_per_connection",
224 &self.max_streams_per_connection,
225 )
226 .field("transport_features", &self.transport_features)
227 .field("interop", &self.interop)
228 .field(
229 "identity_assurance_offered",
230 &self.identity_assurance_offered,
231 )
232 .field(
233 "identity_assurance_required",
234 &self.identity_assurance_required,
235 )
236 .field("supports_message_text", &self.supports_message_text)
237 .field("supports_srtp", &self.supports_srtp)
238 .finish()
239 }
240}
241
242fn default_assurance_offered() -> AssuranceLevel {
243 AssuranceLevel::Anonymous
244}
245
246impl CapabilityDescriptor {
247 pub fn supports_dtmf_rfc4733(&self) -> bool {
250 self.dtmf_modes.contains(&DtmfMode::Rfc4733)
251 }
252}
253
254#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
260#[serde(rename_all = "lowercase")]
261pub enum DataProtocol {
262 Text,
263 Json,
264 Binary,
265}
266
267#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
269pub enum DtmfMode {
270 #[serde(rename = "rfc4733")]
271 Rfc4733,
272 #[serde(rename = "info")]
273 Info,
274}
275
276#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
278#[serde(rename_all = "kebab-case")]
279pub enum TransportFeature {
280 MediaDatagrams,
281 ConnectionMigration,
282 SessionResumption,
283 #[serde(rename = "0rtt")]
284 ZeroRtt,
285 #[serde(rename = "transcode-g711-opus")]
286 TranscodeG711Opus,
287 #[serde(other)]
289 Unknown,
290}
291
292#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
294#[serde(rename_all = "kebab-case")]
295pub enum IdentityAssuranceRequirement {
296 None,
297 Pseudonymous,
298 Identified,
299 TaskScoped,
300 UserAuthorized,
301}
302
303#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
309#[serde(rename_all = "lowercase")]
310pub enum InteropTarget {
311 Sip,
312 Webrtc,
313 Quic,
314 Webtransport,
315 Websocket,
316}
317
318#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename_all = "kebab-case")]
323pub enum AssuranceLevel {
324 #[default]
325 Anonymous,
326 Pseudonymous,
327 Identified,
328 TaskScoped,
329 UserAuthorized,
330}
331
332impl AssuranceLevel {
333 pub fn to_core(self) -> Option<&'static str> {
335 Some(match self {
336 AssuranceLevel::Anonymous => "anonymous",
337 AssuranceLevel::Pseudonymous => "pseudonymous",
338 AssuranceLevel::Identified => "identified",
339 AssuranceLevel::TaskScoped => "task-scoped",
340 AssuranceLevel::UserAuthorized => "user-authorized",
341 })
342 }
343
344 pub fn from_core(assurance: &IdentityAssurance) -> Self {
346 match assurance {
347 IdentityAssurance::Anonymous => AssuranceLevel::Anonymous,
348 IdentityAssurance::Pseudonymous { .. } => AssuranceLevel::Pseudonymous,
349 IdentityAssurance::Identified { .. } => AssuranceLevel::Identified,
350 IdentityAssurance::TaskScoped { .. } => AssuranceLevel::TaskScoped,
351 IdentityAssurance::UserAuthorized { .. } => AssuranceLevel::UserAuthorized,
352 IdentityAssurance::DtlsFingerprint { .. } => AssuranceLevel::Pseudonymous,
355 }
356 }
357}
358
359#[derive(Clone, Default, Serialize, Deserialize)]
365pub struct CapabilityIntersection {
366 pub audio: Option<CodecInfo>,
367 pub video: Option<CodecInfo>,
368 pub dtmf_method: Option<DtmfMethod>,
369 pub messaging_enabled: bool,
370}
371
372impl fmt::Debug for CapabilityIntersection {
373 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
374 formatter
375 .debug_struct("CapabilityIntersection")
376 .field("audio_present", &self.audio.is_some())
377 .field("video_present", &self.video.is_some())
378 .field("dtmf_method", &self.dtmf_method)
379 .field("messaging_enabled", &self.messaging_enabled)
380 .finish()
381 }
382}
383
384#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
385pub enum DtmfMethod {
386 Rfc4733,
387 SipInfo,
388}
389
390#[derive(Clone, Default, Serialize, Deserialize)]
391pub struct NegotiatedCodecs {
392 pub audio: Option<CodecInfo>,
393 pub video: Option<CodecInfo>,
394}
395
396impl fmt::Debug for NegotiatedCodecs {
397 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
398 formatter
399 .debug_struct("NegotiatedCodecs")
400 .field("audio_present", &self.audio.is_some())
401 .field("video_present", &self.video.is_some())
402 .finish()
403 }
404}
405
406#[derive(Clone)]
412pub enum NegotiationOutcome {
413 Ok(Vec<NegotiatedStream>),
415 NotAcceptable488,
417}
418
419impl fmt::Debug for NegotiationOutcome {
420 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
421 match self {
422 Self::Ok(streams) => formatter
423 .debug_struct("Ok")
424 .field("stream_count", &streams.len())
425 .finish(),
426 Self::NotAcceptable488 => formatter.write_str("NotAcceptable488"),
427 }
428 }
429}
430
431#[derive(Clone)]
433pub struct NegotiatedStream {
434 pub stream_id: String,
435 pub kind: String,
436 pub direction: String,
437 pub chosen_codec: Option<String>,
441}
442
443impl fmt::Debug for NegotiatedStream {
444 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
445 formatter
446 .debug_struct("NegotiatedStream")
447 .field("stream_id_present", &!self.stream_id.is_empty())
448 .field("stream_id_bytes", &self.stream_id.len())
449 .field("kind_present", &!self.kind.is_empty())
450 .field("kind_bytes", &self.kind.len())
451 .field("direction_present", &!self.direction.is_empty())
452 .field("direction_bytes", &self.direction.len())
453 .field("chosen_codec_present", &self.chosen_codec.is_some())
454 .finish()
455 }
456}
457
458#[derive(Clone)]
460pub struct StreamOffer<'a> {
461 pub id: &'a str,
462 pub kind: &'a str,
463 pub direction: &'a str,
464 pub codec_preferences: &'a [String],
465}
466
467impl fmt::Debug for StreamOffer<'_> {
468 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
469 formatter
470 .debug_struct("StreamOffer")
471 .field("id_present", &!self.id.is_empty())
472 .field("id_bytes", &self.id.len())
473 .field("kind_present", &!self.kind.is_empty())
474 .field("kind_bytes", &self.kind.len())
475 .field("direction_present", &!self.direction.is_empty())
476 .field("direction_bytes", &self.direction.len())
477 .field("codec_preference_count", &self.codec_preferences.len())
478 .finish()
479 }
480}
481
482#[cfg(test)]
483mod diagnostic_tests {
484 use super::*;
485
486 #[test]
487 fn internal_pcm_codec_uses_wideband_mono_defaults() {
488 let codec = CodecInfo::from_name_with_defaults("pcm_s16le");
489 assert_eq!(codec.clock_rate_hz, 16_000);
490 assert_eq!(codec.channels, 1);
491 assert!(codec.fmtp.is_none());
492 }
493
494 #[test]
495 fn capability_diagnostics_never_render_peer_strings() {
496 const CANARY: &str = "capability-canary\r\nAuthorization: exposed";
497 let codec = CodecInfo {
498 name: CANARY.into(),
499 clock_rate_hz: 48_000,
500 channels: 1,
501 fmtp: Some(CANARY.into()),
502 };
503 let descriptor = CapabilityDescriptor {
504 audio_codecs: vec![codec.clone()],
505 ..CapabilityDescriptor::default()
506 };
507 let negotiated = NegotiatedStream {
508 stream_id: CANARY.into(),
509 kind: CANARY.into(),
510 direction: CANARY.into(),
511 chosen_codec: Some(CANARY.into()),
512 };
513 for debug in [
514 format!("{codec:?}"),
515 format!("{:?}", Codec::new(CANARY)),
516 format!("{descriptor:?}"),
517 format!("{negotiated:?}"),
518 format!("{:?}", NegotiationOutcome::Ok(vec![negotiated])),
519 ] {
520 assert!(!debug.contains(CANARY));
521 }
522 }
523}
524
525pub fn negotiate_streams<'a, I>(
532 streams_offered: I,
533 answerer: &CapabilityDescriptor,
534) -> NegotiationOutcome
535where
536 I: IntoIterator<Item = StreamOffer<'a>>,
537{
538 let answerer_codecs: std::collections::HashSet<&str> = answerer
539 .audio_codecs
540 .iter()
541 .chain(answerer.video_codecs.iter())
542 .map(|c| c.name.as_str())
543 .collect();
544
545 let mut results = Vec::new();
546 let mut any_match = false;
547
548 for offer in streams_offered {
549 let chosen = offer
550 .codec_preferences
551 .iter()
552 .find(|c| answerer_codecs.contains(c.as_str()))
553 .cloned();
554 if chosen.is_some() {
555 any_match = true;
556 }
557 results.push(NegotiatedStream {
558 stream_id: offer.id.to_string(),
559 kind: offer.kind.to_string(),
560 direction: offer.direction.to_string(),
561 chosen_codec: chosen,
562 });
563 }
564
565 if any_match {
566 NegotiationOutcome::Ok(results)
567 } else {
568 NegotiationOutcome::NotAcceptable488
569 }
570}