lesavka/client/src/input/camera.rs

147 lines
4.1 KiB
Rust
Raw Normal View History

// Webcam capture pipeline, quality selection, and launcher preview tap support.
2025-07-03 09:24:57 -05:00
use anyhow::Context;
use gst::prelude::*;
2025-07-03 08:19:59 -05:00
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";
2025-06-08 22:24:14 -05:00
#[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::<u32>().ok())
.unwrap_or(default)
}
2026-01-28 17:52:00 -03:00
#[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,
}
2025-06-08 22:24:14 -05:00
pub struct CameraCapture {
#[allow(dead_code)] // kept alive to hold PLAYING state
2025-07-03 08:19:59 -05:00
pipeline: gst::Pipeline,
sink: gst_app::AppSink,
preview_tap_running: Option<Arc<AtomicBool>>,
pts_rebaser: crate::live_capture_clock::DurationPacedSourcePtsRebaser,
frame_duration_us: u64,
2025-06-08 22:24:14 -05:00
}
include!("camera/capture_pipeline.rs");
include!("camera/device_selection.rs");
include!("camera/encoder_selection.rs");
2025-07-03 08:19:59 -05:00
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_ALLOW_PROFILE_OVERRIDE", None),
],
|| assert_eq!(resolved_capture_profile(Some(cfg)), (1280, 720, 30)),
);
}
#[test]
#[serial]
/// Guards the browser-facing UVC contract against launcher quality selection.
fn negotiated_output_profile_uses_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_ALLOW_PROFILE_OVERRIDE", 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 the explicit lab override available for controlled camera debugging.
fn explicit_profile_override_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_ALLOW_PROFILE_OVERRIDE", 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)
);
},
);
}
}