commucat_proto/
call.rs

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}