// Webcam capture pipeline, quality selection, and launcher preview tap support. use anyhow::Context; use gst::prelude::*; use gstreamer as gst; use gstreamer_app as gst_app; use lesavka_common::lesavka::VideoPacket; use std::{ io::Write, path::{Path, PathBuf}, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, thread, time::Duration, }; const CAMERA_PREVIEW_TAP_ENV: &str = "LESAVKA_UPLINK_CAMERA_PREVIEW"; #[derive(Clone, Copy, Debug, PartialEq, Eq)] enum CameraSourceProfile { Raw, Mjpeg, AutoDecode, } fn env_u32(name: &str, default: u32) -> u32 { std::env::var(name) .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(default) } #[derive(Clone, Copy, Debug)] pub enum CameraCodec { H264, Mjpeg, } #[derive(Clone, Copy, Debug)] pub struct CameraConfig { pub codec: CameraCodec, pub width: u32, pub height: u32, pub fps: u32, } pub struct CameraCapture { #[allow(dead_code)] // kept alive to hold PLAYING state pipeline: gst::Pipeline, sink: gst_app::AppSink, preview_tap_running: Option>, pts_rebaser: crate::live_capture_clock::DurationPacedSourcePtsRebaser, frame_duration_us: u64, } include!("camera/capture_pipeline.rs"); include!("camera/device_selection.rs"); include!("camera/encoder_selection.rs"); include!("camera/source_description.rs"); include!("camera/preview_tap.rs"); include!("camera/bus_and_encoder.rs"); #[cfg(test)] mod tests { use super::{CameraCodec, CameraConfig, resolved_capture_profile, resolved_output_profile}; use serial_test::serial; #[test] #[serial] /// Keeps the selected local webcam mode independent from the UVC gadget mode. fn local_capture_profile_keeps_launcher_quality_env_by_default() { let cfg = CameraConfig { codec: CameraCodec::Mjpeg, width: 640, height: 480, fps: 20, }; temp_env::with_vars( [ ("LESAVKA_CAM_WIDTH", Some("1280")), ("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_FPS", Some("30")), ("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None), ("LESAVKA_CAM_EMIT_UI_PROFILE", None), ], || assert_eq!(resolved_capture_profile(Some(cfg)), (1280, 720, 30)), ); } #[test] #[serial] /// UVC output must match the gadget profile that browsers negotiate. fn negotiated_output_profile_matches_server_uvc_contract_by_default() { let cfg = CameraConfig { codec: CameraCodec::Mjpeg, width: 640, height: 480, fps: 20, }; temp_env::with_vars( [ ("LESAVKA_CAM_WIDTH", Some("1280")), ("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_FPS", Some("30")), ("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None), ("LESAVKA_CAM_EMIT_UI_PROFILE", None), ], || { let capture_profile = resolved_capture_profile(Some(cfg)); assert_eq!(capture_profile, (1280, 720, 30)); assert_eq!( resolved_output_profile(Some(cfg), capture_profile), (640, 480, 20) ); }, ); } #[test] #[serial] /// Keeps UI-profile emission explicit until the server can reconfigure UVC. fn explicit_ui_profile_emission_keeps_lab_mode_available() { let cfg = CameraConfig { codec: CameraCodec::Mjpeg, width: 640, height: 480, fps: 20, }; temp_env::with_vars( [ ("LESAVKA_CAM_WIDTH", Some("1280")), ("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_FPS", Some("30")), ("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None), ("LESAVKA_CAM_EMIT_UI_PROFILE", Some("1")), ], || { let capture_profile = resolved_capture_profile(Some(cfg)); assert_eq!(capture_profile, (1280, 720, 30)); assert_eq!( resolved_output_profile(Some(cfg), capture_profile), (1280, 720, 30) ); }, ); } #[test] #[serial] /// The safety lock wins if both experimental flags are set. fn explicit_server_profile_lock_wins_over_ui_emission() { let cfg = CameraConfig { codec: CameraCodec::Mjpeg, width: 640, height: 480, fps: 20, }; temp_env::with_vars( [ ("LESAVKA_CAM_WIDTH", Some("1280")), ("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_FPS", Some("30")), ("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", Some("1")), ("LESAVKA_CAM_EMIT_UI_PROFILE", Some("1")), ], || { let capture_profile = resolved_capture_profile(Some(cfg)); assert_eq!(capture_profile, (1280, 720, 30)); assert_eq!( resolved_output_profile(Some(cfg), capture_profile), (640, 480, 20) ); }, ); } }