1use crate::{CodecError, ControlEnvelope};
2use commucat_media_types::{
3 AudioCodec, AudioCodecDescriptor, HardwareAcceleration, MediaCapabilities, MediaSourceMode,
4 VideoCodec, VideoCodecDescriptor, VideoResolution,
5};
6use serde::de::DeserializeOwned;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use std::convert::TryFrom;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
12#[serde(rename_all = "snake_case")]
13pub enum CallMode {
14 #[default]
15 FullDuplex,
16 HalfDuplex,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct AudioParameters {
21 pub codec: AudioCodec,
22 pub bitrate: u32,
23 pub sample_rate: u32,
24 pub channels: u8,
25 #[serde(default)]
26 pub fec: bool,
27 #[serde(default)]
28 pub dtx: bool,
29 #[serde(default)]
30 pub source: MediaSourceMode,
31 #[serde(default)]
32 pub preferred_codecs: Vec<AudioCodec>,
33 #[serde(default)]
34 pub available_codecs: Vec<AudioCodecDescriptor>,
35 #[serde(default)]
36 pub allow_passthrough: bool,
37}
38
39impl Default for AudioParameters {
40 fn default() -> Self {
41 AudioParameters {
42 codec: AudioCodec::Opus,
43 bitrate: 16_000,
44 sample_rate: 48_000,
45 channels: 1,
46 fec: true,
47 dtx: true,
48 source: MediaSourceMode::Raw,
49 preferred_codecs: vec![AudioCodec::Opus],
50 available_codecs: vec![AudioCodecDescriptor::default()],
51 allow_passthrough: false,
52 }
53 }
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct VideoParameters {
58 pub codec: VideoCodec,
59 pub max_bitrate: u32,
60 pub max_resolution: VideoResolution,
61 pub frame_rate: u8,
62 #[serde(default)]
63 pub adaptive: bool,
64 #[serde(default)]
65 pub source: MediaSourceMode,
66 #[serde(default)]
67 pub preferred_codecs: Vec<VideoCodec>,
68 #[serde(default)]
69 pub available_codecs: Vec<VideoCodecDescriptor>,
70 #[serde(default)]
71 pub hardware: Vec<HardwareAcceleration>,
72 #[serde(default)]
73 pub allow_passthrough: bool,
74 #[serde(default)]
75 pub capabilities: Option<MediaCapabilities>,
76}
77
78impl Default for VideoParameters {
79 fn default() -> Self {
80 VideoParameters {
81 codec: VideoCodec::Vp8,
82 max_bitrate: 750_000,
83 max_resolution: VideoResolution::default(),
84 frame_rate: 24,
85 adaptive: true,
86 source: MediaSourceMode::Raw,
87 preferred_codecs: vec![VideoCodec::Vp8],
88 available_codecs: vec![VideoCodecDescriptor::default()],
89 hardware: vec![HardwareAcceleration::Cpu],
90 allow_passthrough: true,
91 capabilities: None,
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
97pub struct CallMediaProfile {
98 pub audio: AudioParameters,
99 #[serde(default)]
100 pub video: Option<VideoParameters>,
101 #[serde(default)]
102 pub mode: CallMode,
103 #[serde(default)]
104 pub capabilities: Option<MediaCapabilities>,
105}
106
107impl Default for CallMediaProfile {
108 fn default() -> Self {
109 CallMediaProfile {
110 audio: AudioParameters::default(),
111 video: None,
112 mode: CallMode::FullDuplex,
113 capabilities: None,
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
119#[serde(rename_all = "lowercase")]
120pub enum TransportProtocol {
121 Tcp,
122 #[default]
123 Udp,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "snake_case")]
128pub enum IceCandidateType {
129 Host,
130 Srflx,
131 Prflx,
132 Relay,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
136#[serde(rename_all = "snake_case")]
137pub enum IceTcpCandidateType {
138 Active,
139 Passive,
140 So,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144pub struct IceCredentials {
145 pub username_fragment: String,
146 pub password: String,
147 #[serde(default)]
148 pub expires_at: Option<u64>,
149}
150
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152pub struct TransportCandidate {
153 pub address: String,
154 pub port: u16,
155 #[serde(default)]
156 pub protocol: TransportProtocol,
157 #[serde(default)]
158 pub foundation: Option<String>,
159 #[serde(default)]
160 pub component: Option<u8>,
161 #[serde(default)]
162 pub priority: Option<u32>,
163 #[serde(default)]
164 pub candidate_type: Option<IceCandidateType>,
165 #[serde(default)]
166 pub related_address: Option<String>,
167 #[serde(default)]
168 pub related_port: Option<u16>,
169 #[serde(default)]
170 pub tcp_type: Option<IceTcpCandidateType>,
171 #[serde(default)]
172 pub sdp_mid: Option<String>,
173 #[serde(default)]
174 pub sdp_mline_index: Option<u16>,
175 #[serde(default)]
176 pub url: Option<String>,
177}
178
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
180pub struct CallTransport {
181 #[serde(default)]
182 pub prefer_relay: bool,
183 #[serde(default, alias = "udp_candidates")]
184 pub candidates: Vec<TransportCandidate>,
185 #[serde(default)]
186 pub fingerprints: Vec<String>,
187 #[serde(default)]
188 pub ice_credentials: Option<IceCredentials>,
189 #[serde(default)]
190 pub trickle: bool,
191 #[serde(default)]
192 pub consent_interval_secs: Option<u16>,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196pub struct TransportCandidateRef {
197 pub address: String,
198 pub port: u16,
199 #[serde(default)]
200 pub protocol: TransportProtocol,
201 #[serde(default)]
202 pub candidate_type: Option<IceCandidateType>,
203 #[serde(default)]
204 pub foundation: Option<String>,
205 #[serde(default)]
206 pub priority: Option<u32>,
207}
208
209#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct CallTransportUpdate {
211 pub call_id: String,
212 #[serde(flatten)]
213 pub payload: TransportUpdatePayload,
214}
215
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(tag = "update", rename_all = "snake_case")]
218pub enum TransportUpdatePayload {
219 Candidate {
220 candidate: TransportCandidate,
221 },
222 SelectedCandidatePair {
223 local: TransportCandidateRef,
224 remote: TransportCandidateRef,
225 #[serde(default)]
226 rtt_ms: Option<u32>,
227 },
228 ConsentKeepalive {
229 #[serde(default)]
230 interval_secs: Option<u16>,
231 },
232}
233
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct CallOffer {
236 pub call_id: String,
237 pub from: String,
238 pub to: Vec<String>,
239 #[serde(default)]
240 pub media: CallMediaProfile,
241 #[serde(default = "default_metadata")]
242 pub metadata: Value,
243 #[serde(default)]
244 pub transport: Option<CallTransport>,
245 #[serde(default)]
246 pub expires_at: Option<u64>,
247 #[serde(default)]
248 pub ephemeral_key: Option<String>,
249}
250
251fn default_metadata() -> Value {
252 Value::Null
253}
254
255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
256pub struct CallAnswer {
257 pub call_id: String,
258 pub accept: bool,
259 #[serde(default)]
260 pub media: Option<CallMediaProfile>,
261 #[serde(default)]
262 pub transport: Option<CallTransport>,
263 #[serde(default)]
264 pub reason: Option<CallRejectReason>,
265 #[serde(default = "default_metadata")]
266 pub metadata: Value,
267 #[serde(default)]
268 pub selected_audio_codec: Option<AudioCodec>,
269 #[serde(default)]
270 pub selected_video_codec: Option<VideoCodec>,
271 #[serde(default)]
272 pub audio_source: Option<MediaSourceMode>,
273 #[serde(default)]
274 pub video_source: Option<MediaSourceMode>,
275 #[serde(default)]
276 pub video_hardware: Option<HardwareAcceleration>,
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(rename_all = "snake_case")]
281pub enum CallRejectReason {
282 Busy,
283 Decline,
284 Unsupported,
285 Timeout,
286 Error,
287}
288
289#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
290pub struct CallEnd {
291 pub call_id: String,
292 pub reason: CallEndReason,
293 #[serde(default = "default_metadata")]
294 pub metadata: Value,
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
298#[serde(rename_all = "snake_case")]
299pub enum CallEndReason {
300 Hangup,
301 Cancel,
302 Failure,
303 Timeout,
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(rename_all = "snake_case")]
308pub enum CallMediaDirection {
309 Send,
310 Receive,
311}
312
313#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314pub struct MediaStreamStats {
315 pub bitrate: u32,
316 #[serde(default)]
317 pub packet_loss: f32,
318 #[serde(default)]
319 pub jitter_ms: u32,
320 #[serde(default)]
321 pub rtt_ms: Option<u32>,
322 #[serde(default)]
323 pub frames_per_second: Option<u8>,
324 #[serde(default)]
325 pub key_frames: Option<u32>,
326 #[serde(default)]
327 pub codec: Option<String>,
328 #[serde(default)]
329 pub source: Option<MediaSourceMode>,
330 #[serde(default)]
331 pub hardware: Option<HardwareAcceleration>,
332}
333
334#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
335pub struct CallStats {
336 pub call_id: String,
337 pub direction: CallMediaDirection,
338 #[serde(default)]
339 pub audio: Option<MediaStreamStats>,
340 #[serde(default)]
341 pub video: Option<MediaStreamStats>,
342 #[serde(default)]
343 pub timestamp: Option<u64>,
344}
345
346fn encode_control<T: Serialize>(value: T) -> Result<ControlEnvelope, CodecError> {
347 serde_json::to_value(value)
348 .map(|properties| ControlEnvelope { properties })
349 .map_err(|_| CodecError::InvalidControlJson)
350}
351
352fn decode_control<T: DeserializeOwned>(envelope: &ControlEnvelope) -> Result<T, CodecError> {
353 serde_json::from_value(envelope.properties.clone()).map_err(|_| CodecError::InvalidControlJson)
354}
355
356macro_rules! impl_control_codec {
357 ($ty:ty) => {
358 impl TryFrom<$ty> for ControlEnvelope {
359 type Error = CodecError;
360
361 fn try_from(value: $ty) -> Result<Self, Self::Error> {
362 encode_control(value)
363 }
364 }
365
366 impl TryFrom<&$ty> for ControlEnvelope {
367 type Error = CodecError;
368
369 fn try_from(value: &$ty) -> Result<Self, Self::Error> {
370 encode_control(value)
371 }
372 }
373
374 impl TryFrom<&ControlEnvelope> for $ty {
375 type Error = CodecError;
376
377 fn try_from(envelope: &ControlEnvelope) -> Result<Self, Self::Error> {
378 decode_control::<$ty>(envelope)
379 }
380 }
381 };
382}
383
384impl_control_codec!(CallOffer);
385impl_control_codec!(CallAnswer);
386impl_control_codec!(CallEnd);
387impl_control_codec!(CallStats);
388impl_control_codec!(CallTransportUpdate);
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393 use crate::ControlEnvelope;
394 use commucat_media_types::CodecPriority;
395
396 #[test]
397 fn offer_roundtrip() {
398 let offer = CallOffer {
399 call_id: "call-123".to_string(),
400 from: "alice:device".to_string(),
401 to: vec!["bob:device".to_string()],
402 media: CallMediaProfile {
403 audio: AudioParameters {
404 codec: AudioCodec::Opus,
405 bitrate: 24_000,
406 sample_rate: 48_000,
407 channels: 1,
408 fec: true,
409 dtx: false,
410 source: MediaSourceMode::Raw,
411 preferred_codecs: vec![AudioCodec::Opus, AudioCodec::RawPcm],
412 available_codecs: vec![
413 AudioCodecDescriptor {
414 codec: AudioCodec::Opus,
415 bitrate: Some(32_000),
416 sample_rate: Some(48_000),
417 channels: Some(1),
418 priority: CodecPriority(120),
419 },
420 AudioCodecDescriptor {
421 codec: AudioCodec::RawPcm,
422 bitrate: Some(1_536_000),
423 sample_rate: Some(48_000),
424 channels: Some(2),
425 priority: CodecPriority(60),
426 },
427 ],
428 allow_passthrough: true,
429 },
430 video: Some(VideoParameters {
431 codec: VideoCodec::Vp8,
432 max_bitrate: 350_000,
433 max_resolution: VideoResolution::new(640, 360),
434 frame_rate: 20,
435 adaptive: true,
436 source: MediaSourceMode::Raw,
437 preferred_codecs: vec![VideoCodec::Vp8, VideoCodec::H264Baseline],
438 available_codecs: vec![
439 VideoCodecDescriptor {
440 codec: VideoCodec::Vp8,
441 max_bitrate: Some(350_000),
442 max_resolution: Some(VideoResolution::new(640, 360)),
443 frame_rate: Some(25),
444 hardware: vec![HardwareAcceleration::Cpu],
445 priority: CodecPriority(110),
446 supports_scalability: true,
447 },
448 VideoCodecDescriptor {
449 codec: VideoCodec::H264Baseline,
450 max_bitrate: Some(1_000_000),
451 max_resolution: Some(VideoResolution::new(1280, 720)),
452 frame_rate: Some(30),
453 hardware: vec![
454 HardwareAcceleration::Nvidia,
455 HardwareAcceleration::Intel,
456 ],
457 priority: CodecPriority(80),
458 supports_scalability: false,
459 },
460 ],
461 hardware: vec![HardwareAcceleration::Cpu, HardwareAcceleration::Nvidia],
462 allow_passthrough: true,
463 capabilities: Some(MediaCapabilities {
464 audio: vec![],
465 video: vec![VideoCodecDescriptor::default()],
466 allow_raw_audio: false,
467 allow_raw_video: true,
468 }),
469 }),
470 mode: CallMode::FullDuplex,
471 capabilities: Some(MediaCapabilities {
472 audio: vec![AudioCodecDescriptor::default()],
473 video: vec![VideoCodecDescriptor::default()],
474 allow_raw_audio: true,
475 allow_raw_video: true,
476 }),
477 },
478 metadata: serde_json::json!({"mode": "voice"}),
479 transport: Some(CallTransport {
480 prefer_relay: false,
481 candidates: vec![TransportCandidate {
482 address: "203.0.113.10".to_string(),
483 port: 3478,
484 protocol: TransportProtocol::Udp,
485 foundation: Some("foundation-1".to_string()),
486 component: Some(1),
487 priority: Some(1_234_567),
488 candidate_type: Some(IceCandidateType::Srflx),
489 related_address: Some("10.0.0.5".to_string()),
490 related_port: Some(52_333),
491 tcp_type: None,
492 sdp_mid: Some("0".to_string()),
493 sdp_mline_index: Some(0),
494 url: Some("stun:stun.commucat".to_string()),
495 }],
496 fingerprints: vec!["abc123".to_string()],
497 ice_credentials: Some(IceCredentials {
498 username_fragment: "ufrag".to_string(),
499 password: "secret".to_string(),
500 expires_at: Some(1_700_000_600),
501 }),
502 trickle: true,
503 consent_interval_secs: Some(20),
504 }),
505 expires_at: Some(1_700_000_000),
506 ephemeral_key: Some("feedface".to_string()),
507 };
508 let envelope: ControlEnvelope = (&offer).try_into().expect("encode");
509 let decoded = CallOffer::try_from(&envelope).expect("decode");
510 assert_eq!(decoded.call_id, offer.call_id);
511 let transport = decoded.transport.as_ref().expect("transport");
512 assert_eq!(transport.candidates.len(), 1);
513 assert_eq!(
514 transport
515 .ice_credentials
516 .as_ref()
517 .expect("credentials")
518 .username_fragment,
519 "ufrag"
520 );
521 assert_eq!(
522 decoded
523 .media
524 .audio
525 .available_codecs
526 .iter()
527 .filter(|desc| desc.codec == AudioCodec::RawPcm)
528 .count(),
529 1
530 );
531 assert_eq!(decoded.media.audio.preferred_codecs[0], AudioCodec::Opus);
532 assert_eq!(
533 decoded
534 .media
535 .video
536 .as_ref()
537 .and_then(|video| video.preferred_codecs.first())
538 .copied(),
539 Some(VideoCodec::Vp8)
540 );
541 }
542
543 #[test]
544 fn transport_update_roundtrip() {
545 let candidate = TransportCandidate {
546 address: "198.51.100.12".to_string(),
547 port: 60_000,
548 protocol: TransportProtocol::Udp,
549 foundation: Some("f1".to_string()),
550 component: Some(1),
551 priority: Some(12_345_678),
552 candidate_type: Some(IceCandidateType::Srflx),
553 related_address: Some("10.0.0.5".to_string()),
554 related_port: Some(52_333),
555 tcp_type: None,
556 sdp_mid: Some("0".to_string()),
557 sdp_mline_index: Some(0),
558 url: None,
559 };
560 let update = CallTransportUpdate {
561 call_id: "call-xyz".to_string(),
562 payload: TransportUpdatePayload::Candidate {
563 candidate: candidate.clone(),
564 },
565 };
566 let envelope: ControlEnvelope = (&update).try_into().expect("encode");
567 let decoded = CallTransportUpdate::try_from(&envelope).expect("decode");
568 assert_eq!(decoded.call_id, update.call_id);
569 match decoded.payload {
570 TransportUpdatePayload::Candidate {
571 candidate: ref decoded_candidate,
572 } => {
573 assert_eq!(decoded_candidate.address, candidate.address);
574 assert_eq!(decoded_candidate.priority, candidate.priority);
575 }
576 other => panic!("unexpected payload: {:?}", other),
577 }
578
579 let selected = CallTransportUpdate {
580 call_id: "call-xyz".to_string(),
581 payload: TransportUpdatePayload::SelectedCandidatePair {
582 local: TransportCandidateRef {
583 address: "10.0.0.5".to_string(),
584 port: 52_333,
585 protocol: TransportProtocol::Udp,
586 candidate_type: Some(IceCandidateType::Srflx),
587 foundation: Some("f1".to_string()),
588 priority: Some(7_654_321),
589 },
590 remote: TransportCandidateRef {
591 address: "203.0.113.4".to_string(),
592 port: 60_000,
593 protocol: TransportProtocol::Udp,
594 candidate_type: Some(IceCandidateType::Srflx),
595 foundation: Some("f2".to_string()),
596 priority: Some(9_999_999),
597 },
598 rtt_ms: Some(22),
599 },
600 };
601 let envelope: ControlEnvelope = (&selected).try_into().expect("encode selected");
602 let decoded = CallTransportUpdate::try_from(&envelope).expect("decode selected");
603 assert_eq!(decoded.call_id, selected.call_id);
604 match decoded.payload {
605 TransportUpdatePayload::SelectedCandidatePair { rtt_ms, .. } => {
606 assert_eq!(rtt_ms, Some(22));
607 }
608 _ => panic!("unexpected payload kind"),
609 }
610 }
611
612 #[test]
613 fn answer_reject_roundtrip() {
614 let answer = CallAnswer {
615 call_id: "call-999".to_string(),
616 accept: false,
617 media: None,
618 transport: None,
619 reason: Some(CallRejectReason::Busy),
620 metadata: Value::Null,
621 selected_audio_codec: None,
622 selected_video_codec: None,
623 audio_source: None,
624 video_source: None,
625 video_hardware: None,
626 };
627 let envelope: ControlEnvelope = (&answer).try_into().expect("encode");
628 let decoded = CallAnswer::try_from(&envelope).expect("decode");
629 assert!(!decoded.accept);
630 assert_eq!(decoded.reason, Some(CallRejectReason::Busy));
631 assert!(decoded.selected_audio_codec.is_none());
632 }
633
634 #[test]
635 fn answer_accept_with_selection_roundtrip() {
636 let answer = CallAnswer {
637 call_id: "call-555".to_string(),
638 accept: true,
639 media: None,
640 transport: None,
641 reason: None,
642 metadata: Value::Null,
643 selected_audio_codec: Some(AudioCodec::Opus),
644 selected_video_codec: Some(VideoCodec::Av1Main),
645 audio_source: Some(MediaSourceMode::Encoded),
646 video_source: Some(MediaSourceMode::Raw),
647 video_hardware: Some(HardwareAcceleration::Nvidia),
648 };
649 let envelope: ControlEnvelope = (&answer).try_into().expect("encode");
650 let decoded = CallAnswer::try_from(&envelope).expect("decode");
651 assert!(decoded.accept);
652 assert_eq!(decoded.selected_video_codec, Some(VideoCodec::Av1Main));
653 assert_eq!(decoded.video_hardware, Some(HardwareAcceleration::Nvidia));
654 }
655
656 #[test]
657 fn stats_roundtrip() {
658 let stats = CallStats {
659 call_id: "call-1".to_string(),
660 direction: CallMediaDirection::Send,
661 audio: Some(MediaStreamStats {
662 bitrate: 18_000,
663 packet_loss: 0.012,
664 jitter_ms: 8,
665 rtt_ms: Some(90),
666 frames_per_second: None,
667 key_frames: None,
668 codec: Some("opus".to_string()),
669 source: Some(MediaSourceMode::Encoded),
670 hardware: None,
671 }),
672 video: Some(MediaStreamStats {
673 bitrate: 600_000,
674 packet_loss: 0.03,
675 jitter_ms: 15,
676 rtt_ms: Some(120),
677 frames_per_second: Some(30),
678 key_frames: Some(4),
679 codec: Some("av1".to_string()),
680 source: Some(MediaSourceMode::Raw),
681 hardware: Some(HardwareAcceleration::Nvidia),
682 }),
683 timestamp: Some(1_690_000_000),
684 };
685 let envelope: ControlEnvelope = (&stats).try_into().expect("encode");
686 let decoded = CallStats::try_from(&envelope).expect("decode");
687 assert_eq!(decoded.audio.as_ref().unwrap().bitrate, 18_000);
688 assert_eq!(
689 decoded.video.as_ref().unwrap().hardware,
690 Some(HardwareAcceleration::Nvidia)
691 );
692 }
693}