Skip to main content

iris_chat_core/
desktop_call.rs

1//! Desktop capture/playout. The engine owns no sockets: compressed media is
2//! handed back to the existing authenticated FIPS call actions.
3mod tones;
4use std::sync::Arc;
5#[cfg(not(feature = "desktop-media"))]
6use std::sync::Mutex;
7pub use tones::DesktopCallTone;
8#[cfg(feature = "desktop-media")]
9mod audio;
10#[cfg(feature = "desktop-media")]
11mod devices;
12#[cfg(feature = "desktop-media")]
13mod engine;
14#[cfg(feature = "desktop-media")]
15mod video;
16#[cfg(feature = "desktop-media")]
17mod video_rgb;
18
19#[derive(Clone, Debug, PartialEq, Eq, uniffi::Record)]
20pub struct DesktopAudioDevice {
21    pub id: String,
22    pub name: String,
23}
24
25#[derive(Clone, Debug, Default, PartialEq, Eq, uniffi::Record)]
26pub struct DesktopAudioDevices {
27    pub microphones: Vec<DesktopAudioDevice>,
28    pub speakers: Vec<DesktopAudioDevice>,
29    pub microphone: String,
30    pub speaker: String,
31    pub error: Option<String>,
32}
33
34#[derive(Clone, Debug, uniffi::Enum)]
35pub enum DesktopCallEvent {
36    Encoded {
37        kind: u8,
38        timestamp_us: u64,
39        key_frame: bool,
40        data: Vec<u8>,
41    },
42    Video {
43        local: bool,
44        width: u32,
45        height: u32,
46        rgba: Vec<u8>,
47    },
48    Ready,
49    RequestKeyFrame,
50    Error {
51        message: String,
52    },
53}
54
55#[derive(uniffi::Object)]
56pub struct DesktopCallMedia {
57    #[cfg(feature = "desktop-media")]
58    engine: engine::Engine,
59    #[cfg(not(feature = "desktop-media"))]
60    error: Mutex<bool>,
61}
62#[uniffi::export]
63impl DesktopCallMedia {
64    #[uniffi::constructor]
65    pub fn new() -> Arc<Self> {
66        Arc::new(Self {
67            #[cfg(feature = "desktop-media")]
68            engine: engine::Engine::new(),
69            #[cfg(not(feature = "desktop-media"))]
70            error: Mutex::new(true),
71        })
72    }
73    pub fn configure(&self, muted: bool, video: bool, bitrate: u32, key_generation: u32) {
74        #[cfg(feature = "desktop-media")]
75        self.engine.configure(muted, video, bitrate, key_generation);
76        #[cfg(not(feature = "desktop-media"))]
77        let _ = (muted, video, bitrate, key_generation);
78    }
79    /// Select a platform-owned screen capture source instead of the camera.
80    /// The platform must obtain explicit user consent before enabling this.
81    pub fn set_external_video(&self, enabled: bool) {
82        #[cfg(feature = "desktop-media")]
83        self.engine.set_external_video(enabled);
84        #[cfg(not(feature = "desktop-media"))]
85        let _ = enabled;
86    }
87    /// Supplies RGBA pixels to the existing bounded H.264 video worker.
88    pub fn submit_video_frame(&self, width: u32, height: u32, rgba: Vec<u8>) {
89        #[cfg(feature = "desktop-media")]
90        self.engine.submit_video_frame(width, height, rgba);
91        #[cfg(not(feature = "desktop-media"))]
92        let _ = (width, height, rgba);
93    }
94    /// Device discovery and switching run on the audio worker, never on the UI thread.
95    pub fn audio_devices(&self) -> DesktopAudioDevices {
96        #[cfg(feature = "desktop-media")]
97        {
98            self.engine.audio_devices()
99        }
100        #[cfg(not(feature = "desktop-media"))]
101        {
102            DesktopAudioDevices::default()
103        }
104    }
105    pub fn select_audio_devices(&self, microphone: String, speaker: String) {
106        #[cfg(feature = "desktop-media")]
107        self.engine.select_audio_devices(microphone, speaker);
108        #[cfg(not(feature = "desktop-media"))]
109        let _ = (microphone, speaker);
110    }
111    pub fn receive(
112        &self,
113        kind: u8,
114        sequence: u32,
115        timestamp_us: u64,
116        key_frame: bool,
117        data: Vec<u8>,
118    ) {
119        #[cfg(feature = "desktop-media")]
120        self.engine
121            .receive(kind, sequence, timestamp_us, key_frame, data);
122        #[cfg(not(feature = "desktop-media"))]
123        let _ = (kind, sequence, timestamp_us, key_frame, data);
124    }
125    /// Bounded output; a slow UI cannot accumulate old capture or video frames.
126    pub fn poll(&self) -> Vec<DesktopCallEvent> {
127        #[cfg(feature = "desktop-media")]
128        {
129            self.engine.poll()
130        }
131        #[cfg(not(feature = "desktop-media"))]
132        {
133            if let Ok(mut error) = self.error.lock() {
134                if std::mem::take(&mut *error) {
135                    return vec![DesktopCallEvent::Error {
136                        message: "Calls are unavailable in this build.".into(),
137                    }];
138                }
139            }
140            Vec::new()
141        }
142    }
143    pub fn stop(&self) {
144        #[cfg(feature = "desktop-media")]
145        self.engine.stop();
146    }
147}
148impl Drop for DesktopCallMedia {
149    fn drop(&mut self) {
150        self.stop();
151    }
152}
153
154/// Device-free interoperability fixture. It decodes the peer's real H.264 and
155/// re-encodes it with the exact desktop codecs and current core bitrate target.
156#[cfg(all(feature = "desktop-media", feature = "stack-fixture"))]
157pub struct DesktopVideoTranscoder {
158    decoder: video::VideoDecoder,
159    encoder: video::VideoEncoder,
160    pacer: video::FramePacer,
161    key_generation: u32,
162}
163#[cfg(all(feature = "desktop-media", feature = "stack-fixture"))]
164impl DesktopVideoTranscoder {
165    pub fn new() -> Result<Self, String> {
166        Ok(Self {
167            decoder: video::VideoDecoder::new().map_err(|e| e.to_string())?,
168            encoder: video::VideoEncoder::new(1_200_000).map_err(|e| e.to_string())?,
169            pacer: video::FramePacer::default(),
170            key_generation: 0,
171        })
172    }
173    pub fn receive(
174        &mut self,
175        seq: u32,
176        key: bool,
177        bytes: Vec<u8>,
178        target: u32,
179        generation: u32,
180    ) -> Vec<DesktopCallEvent> {
181        let now = std::time::Instant::now();
182        self.decoder.receive(seq, key, bytes, now);
183        let mut output = Vec::new();
184        for event in self.decoder.drain(now) {
185            match event {
186                DesktopCallEvent::Video {
187                    width,
188                    height,
189                    rgba,
190                    ..
191                } => {
192                    let (w, h, fps) = video::settings(target, width, height);
193                    if !self.pacer.accept(now, fps) {
194                        continue;
195                    }
196                    let Some(image) = image::RgbaImage::from_raw(width, height, rgba) else {
197                        continue;
198                    };
199                    let scaled = image::DynamicImage::ImageRgba8(image)
200                        .resize_exact(w, h, image::imageops::FilterType::Triangle)
201                        .into_rgb8();
202                    if let Ok(Some((data, key_frame))) = self.encoder.encode(
203                        scaled.as_raw(),
204                        w,
205                        h,
206                        target,
207                        self.key_generation != generation,
208                    ) {
209                        self.key_generation = generation;
210                        output.push(DesktopCallEvent::Encoded {
211                            kind: 2,
212                            timestamp_us: 0,
213                            key_frame,
214                            data,
215                        });
216                    }
217                }
218                DesktopCallEvent::RequestKeyFrame => output.push(event),
219                _ => {}
220            }
221        }
222        output
223    }
224}