2026-04-23 07:00:06 -03:00
|
|
|
// Webcam capture pipeline, quality selection, and launcher preview tap support.
|
2025-07-03 09:24:57 -05:00
|
|
|
use anyhow::Context;
|
2025-12-01 11:38:51 -03:00
|
|
|
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;
|
2026-04-22 05:46:31 -03:00
|
|
|
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
|
|
|
|
2026-04-22 08:07:09 -03:00
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
|
|
|
enum CameraSourceProfile {
|
|
|
|
|
Raw,
|
|
|
|
|
Mjpeg,
|
|
|
|
|
AutoDecode,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 09:47:41 -03:00
|
|
|
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 {
|
2025-12-01 11:38:51 -03:00
|
|
|
#[allow(dead_code)] // kept alive to hold PLAYING state
|
2025-07-03 08:19:59 -05:00
|
|
|
pipeline: gst::Pipeline,
|
2025-12-01 11:38:51 -03:00
|
|
|
sink: gst_app::AppSink,
|
2026-04-22 05:46:31 -03:00
|
|
|
preview_tap_running: Option<Arc<AtomicBool>>,
|
2026-04-26 13:22:52 -03:00
|
|
|
pts_rebaser: crate::live_capture_clock::DurationPacedSourcePtsRebaser,
|
|
|
|
|
frame_duration_us: u64,
|
2025-06-08 22:24:14 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 07:00:06 -03:00
|
|
|
include!("camera/capture_pipeline.rs");
|
|
|
|
|
include!("camera/device_selection.rs");
|
|
|
|
|
include!("camera/encoder_selection.rs");
|
2025-07-03 08:19:59 -05:00
|
|
|
|
2026-04-23 07:00:06 -03:00
|
|
|
include!("camera/source_description.rs");
|
|
|
|
|
include!("camera/preview_tap.rs");
|
|
|
|
|
include!("camera/bus_and_encoder.rs");
|
2026-04-30 18:38:34 -03:00
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
2026-04-30 19:40:23 -03:00
|
|
|
use super::{CameraCodec, CameraConfig, resolved_capture_profile, resolved_output_profile};
|
2026-04-30 18:38:34 -03:00
|
|
|
use serial_test::serial;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
2026-04-30 19:40:23 -03:00
|
|
|
/// Keeps the selected local webcam mode independent from the UVC gadget mode.
|
|
|
|
|
fn local_capture_profile_keeps_launcher_quality_env_by_default() {
|
2026-04-30 18:38:34 -03:00
|
|
|
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),
|
|
|
|
|
],
|
2026-04-30 19:40:23 -03:00
|
|
|
|| 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)
|
|
|
|
|
);
|
|
|
|
|
},
|
2026-04-30 18:38:34 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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")),
|
|
|
|
|
],
|
2026-04-30 19:40:23 -03:00
|
|
|
|| {
|
|
|
|
|
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)
|
|
|
|
|
);
|
|
|
|
|
},
|
2026-04-30 18:38:34 -03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|