// Source enumeration for the Discord-style screen-share picker. Split // into two commands on the slow-vs-fast axis: // // - list_screen_sources — metadata only (no thumbnails). Fast; the // picker shows names + placeholders instantly. // - capture_screen_source_thumbnail — one thumbnail at a time, keyed by // the id returned from the list. // // The frontend fans out the thumbnail calls via Promise.all so Tauri's // command thread pool captures them in parallel — wall-clock time ends up // bounded by the *slowest* source rather than the sum of all captures. // The `id` field is emitted in Chromium's internal desktopCapturer format // ("screen::0" / "window::0") so the JS side can try to pass it // straight into getUserMedia's `chromeMediaSourceId` constraint, or use // it as the source key for the native capture pipeline in screen_capture. // // xcap abstracts the platform-specific capture APIs (Windows GDI + DXGI, // macOS CoreGraphics/ScreenCaptureKit, X11) so the code here stays flat. // Thumbnails are captured at native resolution, then letterbox-downscaled // to fit a 320×180 box to keep the base64 payload small. use base64::Engine; use image::{ImageBuffer, Rgba}; use serde::Serialize; const THUMB_MAX_W: u32 = 320; const THUMB_MAX_H: u32 = 180; #[derive(Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct ScreenSource { /// Chromium-format source id — stable within one enumeration call. pub id: String, /// Human-readable label for the picker (monitor name or window title). pub name: String, /// Discriminator for the grid grouping. pub kind: &'static str, /// Base64-encoded PNG, no data-URL prefix. None when the capture /// fails (minimised window, permission-denied surface, transient /// race). The UI renders a name-only card in that case. pub thumbnail_png: Option, /// Native width of the full-res source — mostly informational, used /// by the UI for aspect-ratio styling of the card. pub width: u32, pub height: u32, } // Fast metadata-only enumeration. No image capture happens here — that's // why it returns in tens of milliseconds instead of the multi-second // wait the single-shot enumerate_screen_sources command had. #[tauri::command] pub fn list_screen_sources() -> Result, String> { let mut out: Vec = Vec::new(); append_monitor_metadata(&mut out); append_window_metadata(&mut out); Ok(out) } // One-shot thumbnail capture by source id. Called N times in parallel from // the frontend after the list lands so the total wall-clock is bounded by // the slowest capture rather than the sum. #[tauri::command] pub fn capture_screen_source_thumbnail(source_id: String) -> Result, String> { if let Some(raw) = source_id.strip_prefix("screen:").and_then(strip_zero_suffix) { return Ok(capture_monitor_by_id(raw)); } if let Some(raw) = source_id.strip_prefix("window:").and_then(strip_zero_suffix) { return Ok(capture_window_by_id(raw)); } Err(format!("unknown source id format: {source_id}")) } fn strip_zero_suffix(s: &str) -> Option<&str> { s.strip_suffix(":0") } // Legacy one-shot all-in-one enumeration. Kept around so the frontend can // fall back during rollout if the split pair throws; marked dead_code so // the linker doesn't grumble when only the split variant is wired up. #[allow(dead_code)] #[tauri::command] pub fn enumerate_screen_sources() -> Result, String> { let mut out: Vec = Vec::new(); append_monitors(&mut out); append_windows(&mut out); Ok(out) } // --------------------------------------------------------------------------- // Monitors // --------------------------------------------------------------------------- fn append_monitor_metadata(out: &mut Vec) { let monitors = match xcap::Monitor::all() { Ok(m) => m, Err(err) => { eprintln!("xcap Monitor::all failed: {err}"); return; } }; for (idx, m) in monitors.iter().enumerate() { out.push(ScreenSource { id: format!("screen:{}:0", m.id()), name: monitor_label(m, idx), kind: "screen", thumbnail_png: None, width: m.width(), height: m.height(), }); } } #[allow(dead_code)] fn append_monitors(out: &mut Vec) { let monitors = match xcap::Monitor::all() { Ok(m) => m, Err(err) => { eprintln!("xcap Monitor::all failed: {err}"); return; } }; for (idx, m) in monitors.iter().enumerate() { out.push(ScreenSource { id: format!("screen:{}:0", m.id()), name: monitor_label(m, idx), kind: "screen", thumbnail_png: capture_monitor_thumbnail(m), width: m.width(), height: m.height(), }); } } fn capture_monitor_by_id(raw: &str) -> Option { let raw_id: u32 = raw.parse().ok()?; let monitors = xcap::Monitor::all().ok()?; let target = monitors.into_iter().find(|m| m.id() == raw_id)?; capture_monitor_thumbnail(&target) } fn monitor_label(m: &xcap::Monitor, idx: usize) -> String { let name = m.name(); if name.is_empty() { format!("Bildschirm {}", idx + 1) } else { name.to_string() } } fn capture_monitor_thumbnail(m: &xcap::Monitor) -> Option { let image = m.capture_image().ok()?; encode_scaled_png(image) } // --------------------------------------------------------------------------- // Windows // --------------------------------------------------------------------------- fn append_window_metadata(out: &mut Vec) { let windows = match xcap::Window::all() { Ok(w) => w, Err(err) => { eprintln!("xcap Window::all failed: {err}"); return; } }; for w in windows.iter() { if !is_shareable_window(w) { continue; } let title = w.title(); if title.trim().is_empty() { continue; } out.push(ScreenSource { id: format!("window:{}:0", w.id()), name: title.to_string(), kind: "window", thumbnail_png: None, width: w.width(), height: w.height(), }); } } #[allow(dead_code)] fn append_windows(out: &mut Vec) { let windows = match xcap::Window::all() { Ok(w) => w, Err(err) => { eprintln!("xcap Window::all failed: {err}"); return; } }; for w in windows.iter() { if !is_shareable_window(w) { continue; } let title = w.title(); if title.trim().is_empty() { continue; } out.push(ScreenSource { id: format!("window:{}:0", w.id()), name: title.to_string(), kind: "window", thumbnail_png: capture_window_thumbnail(w), width: w.width(), height: w.height(), }); } } fn capture_window_by_id(raw: &str) -> Option { let raw_id: u32 = raw.parse().ok()?; let windows = xcap::Window::all().ok()?; let target = windows.into_iter().find(|w| w.id() == raw_id)?; capture_window_thumbnail(&target) } fn is_shareable_window(w: &xcap::Window) -> bool { if w.is_minimized() { return false; } let width = w.width(); let height = w.height(); // Tooltips, invisible tray-helpers, etc. sit at or near zero size — // they'd clutter the picker grid and usually can't be captured anyway. if width < 80 || height < 60 { return false; } true } fn capture_window_thumbnail(w: &xcap::Window) -> Option { let image = w.capture_image().ok()?; encode_scaled_png(image) } // --------------------------------------------------------------------------- // Scaling + encoding // --------------------------------------------------------------------------- // Letterbox-shrink the captured image so the long edge is at most // THUMB_MAX_W / THUMB_MAX_H. Keeps aspect ratio, skips upscaling entirely // (tiny windows stay their captured size). Returns a base64 PNG string or // None if encoding fails. fn encode_scaled_png(src: ImageBuffer, Vec>) -> Option { let (w, h) = src.dimensions(); if w == 0 || h == 0 { return None; } let scale = (THUMB_MAX_W as f32 / w as f32) .min(THUMB_MAX_H as f32 / h as f32) .min(1.0); let scaled = if scale < 1.0 { let new_w = ((w as f32) * scale).round().max(1.0) as u32; let new_h = ((h as f32) * scale).round().max(1.0) as u32; image::imageops::resize(&src, new_w, new_h, image::imageops::FilterType::Triangle) } else { src }; let mut buf: Vec = Vec::new(); let encoder = image::codecs::png::PngEncoder::new_with_quality( &mut buf, image::codecs::png::CompressionType::Fast, image::codecs::png::FilterType::Adaptive, ); use image::ImageEncoder; encoder .write_image( scaled.as_raw(), scaled.width(), scaled.height(), image::ExtendedColorType::Rgba8, ) .ok()?; Some(base64::engine::general_purpose::STANDARD.encode(&buf)) }