fix(camera): preserve webcam capture mode
This commit is contained in:
parent
8f319549e1
commit
c12f5bf50c
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.16.8"
|
||||
version = "0.16.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1686,7 +1686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.16.8"
|
||||
version = "0.16.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1698,7 +1698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.16.8"
|
||||
version = "0.16.9"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.16.8"
|
||||
version = "0.16.9"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -64,13 +64,13 @@ include!("camera/bus_and_encoder.rs");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CameraCodec, CameraConfig, resolved_capture_profile};
|
||||
use super::{CameraCodec, CameraConfig, resolved_capture_profile, resolved_output_profile};
|
||||
use serial_test::serial;
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
/// Guards the browser-facing UVC contract against launcher preview overrides.
|
||||
fn negotiated_capture_profile_overrides_launcher_quality_env_by_default() {
|
||||
/// 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,
|
||||
@ -84,7 +84,35 @@ mod tests {
|
||||
("LESAVKA_CAM_FPS", Some("30")),
|
||||
("LESAVKA_CAM_ALLOW_PROFILE_OVERRIDE", None),
|
||||
],
|
||||
|| assert_eq!(resolved_capture_profile(Some(cfg)), (640, 480, 20)),
|
||||
|| 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)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -105,7 +133,14 @@ mod tests {
|
||||
("LESAVKA_CAM_FPS", Some("30")),
|
||||
("LESAVKA_CAM_ALLOW_PROFILE_OVERRIDE", Some("1")),
|
||||
],
|
||||
|| assert_eq!(resolved_capture_profile(Some(cfg)), (1280, 720, 30)),
|
||||
|| {
|
||||
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)
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,10 +40,14 @@ impl CameraCapture {
|
||||
|cfg| matches!(cfg.codec, CameraCodec::Mjpeg),
|
||||
);
|
||||
let jpeg_quality = env_u32("LESAVKA_CAM_JPEG_QUALITY", 85).clamp(1, 100);
|
||||
let (width, height, fps) = resolved_capture_profile(cfg);
|
||||
let capture_profile = resolved_capture_profile(cfg);
|
||||
let (capture_width, capture_height, capture_fps) = capture_profile;
|
||||
let (width, height, fps) = resolved_output_profile(cfg, capture_profile);
|
||||
let keyframe_interval = env_u32("LESAVKA_CAM_KEYFRAME_INTERVAL", fps.min(5)).clamp(1, fps);
|
||||
let source_profile = camera_source_profile(allow_mjpg_source);
|
||||
let use_mjpg_source = source_profile == CameraSourceProfile::Mjpeg;
|
||||
let passthrough_mjpg_source =
|
||||
use_mjpg_source && capture_profile == (width, height, fps);
|
||||
let (enc, kf_prop) = if use_mjpg_source && !output_mjpeg {
|
||||
("x264enc", Some("key-int-max"))
|
||||
} else {
|
||||
@ -66,29 +70,29 @@ impl CameraCapture {
|
||||
}
|
||||
#[cfg(not(coverage))]
|
||||
let have_nvvidconv = gst::ElementFactory::find("nvvidconv").is_some();
|
||||
let (src_caps, preenc) = match enc {
|
||||
let preenc = match enc {
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
// Jetson (has nvvidconv) Desktop (falls back to videoconvert)
|
||||
// ───────────────────────────────────────────────────────────────────
|
||||
#[cfg(not(coverage))]
|
||||
"nvh264enc" if have_nvvidconv =>
|
||||
(format!(
|
||||
"video/x-raw(memory:NVMM),format=NV12,width={width},height={height},framerate={fps}/1"
|
||||
), "nvvidconv !"),
|
||||
format!(
|
||||
"nvvidconv ! video/x-raw(memory:NVMM),format=NV12,width={width},height={height},framerate={fps}/1 !"
|
||||
),
|
||||
#[cfg(not(coverage))]
|
||||
"nvh264enc" /* else */ =>
|
||||
(format!(
|
||||
"video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1"
|
||||
), "videoconvert !"),
|
||||
format!(
|
||||
"videoconvert ! video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1 !"
|
||||
),
|
||||
#[cfg(not(coverage))]
|
||||
"vaapih264enc" =>
|
||||
(format!(
|
||||
"video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1"
|
||||
), "videoconvert !"),
|
||||
format!(
|
||||
"videoconvert ! video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1 !"
|
||||
),
|
||||
_ =>
|
||||
(format!(
|
||||
"video/x-raw,width={width},height={height},framerate={fps}/1"
|
||||
), "videoconvert !"),
|
||||
format!(
|
||||
"videoconvert ! video/x-raw,width={width},height={height},framerate={fps}/1 !"
|
||||
),
|
||||
};
|
||||
|
||||
// let desc = format!(
|
||||
@ -104,11 +108,24 @@ impl CameraCapture {
|
||||
// * x264enc needs the usual raw caps.
|
||||
let preview_tap_path = camera_preview_tap_path();
|
||||
let preview_tap_branch = camera_preview_tap_branch(width, height, fps);
|
||||
let raw_source_chain =
|
||||
camera_raw_source_chain(&src_desc, &src_caps, width, height, fps, source_profile);
|
||||
let source_raw_caps = format!(
|
||||
"video/x-raw,width={capture_width},height={capture_height},framerate={capture_fps}/1"
|
||||
);
|
||||
let raw_source_chain = camera_raw_source_chain(
|
||||
&src_desc,
|
||||
&source_raw_caps,
|
||||
capture_width,
|
||||
capture_height,
|
||||
capture_fps,
|
||||
source_profile,
|
||||
);
|
||||
let normalized_raw_chain = format!(
|
||||
"{raw_source_chain} ! {}",
|
||||
camera_output_raw_chain(width, height, fps)
|
||||
);
|
||||
let desc = if preview_tap_path.is_some() {
|
||||
if output_mjpeg {
|
||||
if use_mjpg_source {
|
||||
if passthrough_mjpg_source {
|
||||
format!(
|
||||
"{src_desc} ! \
|
||||
image/jpeg,width={width},height={height},framerate={fps}/1 ! \
|
||||
@ -120,7 +137,7 @@ impl CameraCapture {
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{raw_source_chain} ! \
|
||||
"{normalized_raw_chain} ! \
|
||||
tee name=t \
|
||||
t. ! queue max-size-buffers=30 leaky=downstream ! \
|
||||
videoconvert ! jpegenc quality={jpeg_quality} ! \
|
||||
@ -129,22 +146,9 @@ impl CameraCapture {
|
||||
{preview_tap_branch}"
|
||||
)
|
||||
}
|
||||
} else if use_mjpg_source {
|
||||
format!(
|
||||
"{src_desc} ! \
|
||||
image/jpeg,width={width},height={height} ! \
|
||||
jpegdec ! videorate ! video/x-raw,framerate={fps}/1 ! \
|
||||
tee name=t \
|
||||
t. ! queue max-size-buffers=30 leaky=downstream ! \
|
||||
videoconvert ! {enc_opts} ! \
|
||||
h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||
appsink name=asink emit-signals=true max-buffers=60 drop=true \
|
||||
t. ! queue max-size-buffers=2 leaky=downstream ! \
|
||||
{preview_tap_branch}"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{raw_source_chain} ! \
|
||||
"{normalized_raw_chain} ! \
|
||||
tee name=t \
|
||||
t. ! queue max-size-buffers=30 leaky=downstream ! \
|
||||
{preenc} {enc_opts} ! \
|
||||
@ -155,7 +159,7 @@ impl CameraCapture {
|
||||
)
|
||||
}
|
||||
} else if output_mjpeg {
|
||||
if use_mjpg_source {
|
||||
if passthrough_mjpg_source {
|
||||
format!(
|
||||
"{src_desc} ! \
|
||||
image/jpeg,width={width},height={height},framerate={fps}/1 ! \
|
||||
@ -164,32 +168,32 @@ impl CameraCapture {
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{raw_source_chain} ! \
|
||||
"{normalized_raw_chain} ! \
|
||||
videoconvert ! jpegenc quality={jpeg_quality} ! \
|
||||
queue max-size-buffers=30 leaky=downstream ! \
|
||||
appsink name=asink emit-signals=true max-buffers=60 drop=true"
|
||||
)
|
||||
}
|
||||
} else if use_mjpg_source {
|
||||
format!(
|
||||
"{src_desc} ! \
|
||||
image/jpeg,width={width},height={height} ! \
|
||||
jpegdec ! videorate ! video/x-raw,framerate={fps}/1 ! \
|
||||
videoconvert ! {enc_opts} ! \
|
||||
h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||
queue max-size-buffers=30 leaky=downstream ! \
|
||||
appsink name=asink emit-signals=true max-buffers=60 drop=true"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{raw_source_chain} ! \
|
||||
"{normalized_raw_chain} ! \
|
||||
{preenc} {enc_opts} ! \
|
||||
h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||
queue max-size-buffers=30 leaky=downstream ! \
|
||||
appsink name=asink emit-signals=true max-buffers=60 drop=true"
|
||||
)
|
||||
};
|
||||
tracing::info!(%enc, width, height, fps, ?desc, "📸 using encoder element");
|
||||
tracing::info!(
|
||||
%enc,
|
||||
capture_width,
|
||||
capture_height,
|
||||
capture_fps,
|
||||
output_width = width,
|
||||
output_height = height,
|
||||
output_fps = fps,
|
||||
?desc,
|
||||
"📸 using encoder element"
|
||||
);
|
||||
|
||||
let pipeline: gst::Pipeline = gst::parse::launch(&desc)
|
||||
.context("gst parse_launch(cam)")?
|
||||
@ -259,15 +263,12 @@ impl CameraCapture {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the exact profile the client sends, preferring the server UVC contract.
|
||||
/// Resolve the profile requested from the local webcam.
|
||||
///
|
||||
/// The server UVC contract is applied after capture. Keeping these separate
|
||||
/// prevents a browser-facing 640x480/20 gadget mode from forcing a local webcam
|
||||
/// to expose that exact mode when the selected camera quality is 720p/30.
|
||||
fn resolved_capture_profile(cfg: Option<CameraConfig>) -> (u32, u32, u32) {
|
||||
match cfg {
|
||||
Some(cfg) if !env_flag_enabled("LESAVKA_CAM_ALLOW_PROFILE_OVERRIDE") => {
|
||||
return (cfg.width, cfg.height, cfg.fps.max(1));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
(
|
||||
env_u32("LESAVKA_CAM_WIDTH", cfg.map_or(1280, |cfg| cfg.width)),
|
||||
env_u32("LESAVKA_CAM_HEIGHT", cfg.map_or(720, |cfg| cfg.height)),
|
||||
@ -275,6 +276,19 @@ fn resolved_capture_profile(cfg: Option<CameraConfig>) -> (u32, u32, u32) {
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve the profile emitted toward the remote UVC gadget.
|
||||
fn resolved_output_profile(
|
||||
cfg: Option<CameraConfig>,
|
||||
capture_profile: (u32, u32, u32),
|
||||
) -> (u32, u32, u32) {
|
||||
match cfg {
|
||||
Some(cfg) if !env_flag_enabled("LESAVKA_CAM_ALLOW_PROFILE_OVERRIDE") => {
|
||||
(cfg.width, cfg.height, cfg.fps.max(1))
|
||||
}
|
||||
_ => capture_profile,
|
||||
}
|
||||
}
|
||||
|
||||
fn env_flag_enabled(name: &str) -> bool {
|
||||
std::env::var(name).ok().is_some_and(|value| {
|
||||
let trimmed = value.trim();
|
||||
|
||||
@ -56,6 +56,14 @@ fn camera_auto_decode_caps(width: u32, height: u32, fps: u32) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
/// Convert local webcam frames into the exact outbound UVC profile.
|
||||
fn camera_output_raw_chain(width: u32, height: u32, fps: u32) -> String {
|
||||
format!(
|
||||
"videoconvert ! videoscale ! videorate ! \
|
||||
video/x-raw,width={width},height={height},framerate={fps}/1,pixel-aspect-ratio=1/1"
|
||||
)
|
||||
}
|
||||
|
||||
fn camera_preview_tap_path() -> Option<PathBuf> {
|
||||
std::env::var(CAMERA_PREVIEW_TAP_ENV)
|
||||
.ok()
|
||||
|
||||
@ -10,12 +10,17 @@
|
||||
state
|
||||
.borrow_mut()
|
||||
.select_microphone(selected_combo_value(µphone_combo_read));
|
||||
if tests.borrow_mut().is_running(DeviceTestKind::Microphone) {
|
||||
let relay_live = child_proc.borrow().is_some();
|
||||
if relay_live {
|
||||
widgets.status_label.set_text(
|
||||
"Microphone selection staged for the next relay launch. Use the Mic toggle to soft-pause or resume the current live feed.",
|
||||
);
|
||||
} else if tests.borrow_mut().is_running(DeviceTestKind::Microphone) {
|
||||
widgets.status_label.set_text(
|
||||
"Microphone selection changed. Restart Monitor Mic to audition the new input.",
|
||||
);
|
||||
}
|
||||
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
|
||||
refresh_launcher_ui(&widgets, &state.borrow(), relay_live);
|
||||
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
||||
});
|
||||
}
|
||||
@ -34,12 +39,17 @@
|
||||
let speaker_running = tests.borrow_mut().is_running(DeviceTestKind::Speaker);
|
||||
let microphone_running =
|
||||
tests.borrow_mut().is_running(DeviceTestKind::Microphone);
|
||||
if speaker_running || microphone_running {
|
||||
let relay_live = child_proc.borrow().is_some();
|
||||
if relay_live {
|
||||
widgets.status_label.set_text(
|
||||
"Speaker selection staged for the next relay launch. Speaker gain still applies live.",
|
||||
);
|
||||
} else if speaker_running || microphone_running {
|
||||
widgets.status_label.set_text(
|
||||
"Speaker selection changed. Restart the local audio tests to hear the new output.",
|
||||
);
|
||||
}
|
||||
refresh_launcher_ui(&widgets, &state.borrow(), child_proc.borrow().is_some());
|
||||
refresh_launcher_ui(&widgets, &state.borrow(), relay_live);
|
||||
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
||||
});
|
||||
}
|
||||
|
||||
@ -30,6 +30,10 @@
|
||||
widgets
|
||||
.status_label
|
||||
.set_text(&format!("Camera quality update failed: {err}"));
|
||||
} else if child_proc.borrow().is_some() {
|
||||
widgets.status_label.set_text(
|
||||
"Camera selection staged for the next relay launch. Use the Camera toggle to soft-pause or resume the current live feed.",
|
||||
);
|
||||
} else if preview_was_running {
|
||||
widgets.status_label.set_text(&format!(
|
||||
"Local camera preview switched to {}{}.",
|
||||
@ -68,6 +72,10 @@
|
||||
widgets
|
||||
.status_label
|
||||
.set_text(&format!("Camera quality update failed: {err}"));
|
||||
} else if child_proc.borrow().is_some() {
|
||||
widgets.status_label.set_text(
|
||||
"Camera quality staged for the next relay launch. The live feed keeps its current capture pipeline.",
|
||||
);
|
||||
} else if preview_was_running {
|
||||
widgets.status_label.set_text(&format!(
|
||||
"Local camera preview switched to {}.",
|
||||
|
||||
@ -235,8 +235,8 @@
|
||||
console_level_combo.set_tooltip_text(Some("Show relay logs at this level or higher."));
|
||||
let console_copy_button = gtk::Button::with_label("Copy");
|
||||
console_copy_button.set_tooltip_text(Some("Copy visible log."));
|
||||
let console_popout_button = gtk::Button::with_label("Pop Out");
|
||||
console_popout_button.set_tooltip_text(Some("Open log window."));
|
||||
let console_popout_button = gtk::Button::with_label("Break Out");
|
||||
console_popout_button.set_tooltip_text(Some("Break out the log window."));
|
||||
let console_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
console_buttons.set_hexpand(true);
|
||||
console_buttons.set_homogeneous(true);
|
||||
|
||||
@ -171,25 +171,24 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
||||
widgets
|
||||
.uvc_recover_button
|
||||
.set_sensitive(state.server_available);
|
||||
widgets.device_refresh_button.set_sensitive(!relay_live);
|
||||
widgets.device_refresh_button.set_sensitive(true);
|
||||
widgets
|
||||
.camera_combo
|
||||
.set_sensitive(!relay_live && state.channels.camera);
|
||||
.set_sensitive(state.channels.camera);
|
||||
widgets.camera_quality_combo.set_sensitive(
|
||||
!relay_live
|
||||
&& state.channels.camera
|
||||
state.channels.camera
|
||||
&& state.devices.camera.is_some()
|
||||
&& state.camera_quality.is_some(),
|
||||
);
|
||||
widgets
|
||||
.microphone_combo
|
||||
.set_sensitive(!relay_live && state.channels.microphone);
|
||||
.set_sensitive(state.channels.microphone);
|
||||
widgets
|
||||
.speaker_combo
|
||||
.set_sensitive(!relay_live && state.channels.audio);
|
||||
.set_sensitive(state.channels.audio);
|
||||
widgets
|
||||
.audio_gain_scale
|
||||
.set_sensitive(!relay_live && state.channels.audio);
|
||||
.set_sensitive(state.channels.audio);
|
||||
widgets.keyboard_combo.set_sensitive(!relay_live);
|
||||
widgets.mouse_combo.set_sensitive(!relay_live);
|
||||
widgets
|
||||
@ -223,7 +222,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
||||
.set_sensitive(!relay_live && state.channels.microphone);
|
||||
widgets
|
||||
.mic_gain_scale
|
||||
.set_sensitive(!relay_live && state.channels.microphone);
|
||||
.set_sensitive(state.channels.microphone);
|
||||
widgets
|
||||
.speaker_test_button
|
||||
.set_sensitive(!relay_live && state.channels.audio);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.16.8"
|
||||
version = "0.16.9"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
"client/src/input/camera.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 111
|
||||
"loc": 146
|
||||
},
|
||||
"client/src/input/camera/bus_and_encoder.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -68,7 +68,7 @@
|
||||
"client/src/input/camera/capture_pipeline.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 4,
|
||||
"loc": 320
|
||||
"loc": 334
|
||||
},
|
||||
"client/src/input/camera/device_selection.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -88,7 +88,7 @@
|
||||
"client/src/input/camera/source_description.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 76
|
||||
"loc": 84
|
||||
},
|
||||
"client/src/input/inputs.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -338,7 +338,7 @@
|
||||
"client/src/launcher/ui/media_device_bindings.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 182
|
||||
"loc": 192
|
||||
},
|
||||
"client/src/launcher/ui/message_and_network_state.rs": {
|
||||
"clippy_warnings": 0,
|
||||
@ -373,7 +373,7 @@
|
||||
"client/src/launcher/ui/stage_device_bindings.rs": {
|
||||
"clippy_warnings": 0,
|
||||
"doc_debt": 0,
|
||||
"loc": 176
|
||||
"loc": 184
|
||||
},
|
||||
"client/src/launcher/ui/startup_window_guard.rs": {
|
||||
"clippy_warnings": 0,
|
||||
|
||||
@ -47,7 +47,18 @@ MEDIA_TESTS=(
|
||||
start_seconds=$(date +%s)
|
||||
status=0
|
||||
set +e
|
||||
cargo test -p lesavka_testing "${MEDIA_TESTS[@]}" --color never 2>&1 | tee "${TEST_LOG}"
|
||||
{
|
||||
echo '==> client camera profile/unit guards'
|
||||
cargo test -p lesavka_client --color never input::camera::tests -- --nocapture
|
||||
camera_status=${PIPESTATUS[0]}
|
||||
echo
|
||||
echo '==> media reliability contract tests'
|
||||
cargo test -p lesavka_testing --color never "${MEDIA_TESTS[@]}"
|
||||
contract_status=${PIPESTATUS[0]}
|
||||
if [[ "${camera_status}" -ne 0 || "${contract_status}" -ne 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
} 2>&1 | tee "${TEST_LOG}"
|
||||
status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
end_seconds=$(date +%s)
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.16.8"
|
||||
version = "0.16.9"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ const UI_SRC: &str = concat!(
|
||||
include_str!("../../client/src/launcher/ui/activation_setup.rs"),
|
||||
include_str!("../../client/src/launcher/ui/device_refresh_binding.rs"),
|
||||
include_str!("../../client/src/launcher/ui/local_test_bindings.rs"),
|
||||
include_str!("../../client/src/launcher/ui/media_device_bindings.rs"),
|
||||
include_str!("../../client/src/launcher/ui/relay_input_bindings.rs"),
|
||||
include_str!("../../client/src/launcher/ui/runtime_poll.rs"),
|
||||
include_str!("../../client/src/launcher/ui/stage_device_bindings.rs"),
|
||||
@ -68,29 +69,26 @@ fn relay_child_starts_safe_parent_watchdog_on_boot() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relay_address_entry_is_locked_while_relay_is_live() {
|
||||
fn relay_address_locks_but_media_staging_stays_available_while_relay_is_live() {
|
||||
assert!(UI_RUNTIME_SRC.contains("widgets.server_entry.set_sensitive(!relay_live);"));
|
||||
assert!(
|
||||
UI_RUNTIME_SRC.contains(
|
||||
".camera_combo\n .set_sensitive(!relay_live && state.channels.camera);"
|
||||
)
|
||||
UI_RUNTIME_SRC.contains(".camera_combo\n .set_sensitive(state.channels.camera);")
|
||||
);
|
||||
assert!(UI_RUNTIME_SRC.contains(".camera_quality_combo"));
|
||||
assert!(UI_RUNTIME_SRC.contains("widgets.camera_quality_combo.set_sensitive("));
|
||||
assert!(UI_RUNTIME_SRC.contains("state.devices.camera.is_some()"));
|
||||
assert!(UI_RUNTIME_SRC.contains("state.camera_quality.is_some()"));
|
||||
assert!(UI_RUNTIME_SRC.contains(
|
||||
".microphone_combo\n .set_sensitive(!relay_live && state.channels.microphone);"
|
||||
));
|
||||
assert!(
|
||||
UI_RUNTIME_SRC.contains(
|
||||
".speaker_combo\n .set_sensitive(!relay_live && state.channels.audio);"
|
||||
)
|
||||
UI_RUNTIME_SRC
|
||||
.contains(".microphone_combo\n .set_sensitive(state.channels.microphone);")
|
||||
);
|
||||
assert!(
|
||||
UI_RUNTIME_SRC.contains(".speaker_combo\n .set_sensitive(state.channels.audio);")
|
||||
);
|
||||
assert!(UI_RUNTIME_SRC.contains(".audio_gain_scale"));
|
||||
assert!(UI_RUNTIME_SRC.contains(".set_sensitive(!relay_live && state.channels.audio);"));
|
||||
assert!(UI_RUNTIME_SRC.contains(".set_sensitive(state.channels.audio);"));
|
||||
assert!(UI_RUNTIME_SRC.contains(".mic_gain_scale"));
|
||||
assert!(UI_RUNTIME_SRC.contains(".set_sensitive(!relay_live && state.channels.microphone);"));
|
||||
assert!(UI_RUNTIME_SRC.contains(".set_sensitive(state.channels.microphone);"));
|
||||
assert!(UI_RUNTIME_SRC.contains("widgets.keyboard_combo.set_sensitive(!relay_live);"));
|
||||
assert!(UI_RUNTIME_SRC.contains("widgets.mouse_combo.set_sensitive(!relay_live);"));
|
||||
assert!(
|
||||
@ -108,6 +106,10 @@ fn relay_address_entry_is_locked_while_relay_is_live() {
|
||||
)
|
||||
);
|
||||
assert!(UI_RUNTIME_SRC.contains("Soft-pause or resume this feed in the running relay"));
|
||||
assert!(UI_SRC.contains("Camera selection staged for the next relay launch"));
|
||||
assert!(UI_SRC.contains("Camera quality staged for the next relay launch"));
|
||||
assert!(UI_SRC.contains("Microphone selection staged for the next relay launch"));
|
||||
assert!(UI_SRC.contains("Speaker selection staged for the next relay launch"));
|
||||
assert!(UI_RUNTIME_SRC.contains("\"Connect\""));
|
||||
assert!(UI_RUNTIME_SRC.contains("\"Disconnect\""));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user