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]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.16.8"
|
version = "0.16.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.16.8"
|
version = "0.16.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.16.8"
|
version = "0.16.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.16.8"
|
version = "0.16.9"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -64,13 +64,13 @@ include!("camera/bus_and_encoder.rs");
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{CameraCodec, CameraConfig, resolved_capture_profile};
|
use super::{CameraCodec, CameraConfig, resolved_capture_profile, resolved_output_profile};
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
/// Guards the browser-facing UVC contract against launcher preview overrides.
|
/// Keeps the selected local webcam mode independent from the UVC gadget mode.
|
||||||
fn negotiated_capture_profile_overrides_launcher_quality_env_by_default() {
|
fn local_capture_profile_keeps_launcher_quality_env_by_default() {
|
||||||
let cfg = CameraConfig {
|
let cfg = CameraConfig {
|
||||||
codec: CameraCodec::Mjpeg,
|
codec: CameraCodec::Mjpeg,
|
||||||
width: 640,
|
width: 640,
|
||||||
@ -84,7 +84,35 @@ mod tests {
|
|||||||
("LESAVKA_CAM_FPS", Some("30")),
|
("LESAVKA_CAM_FPS", Some("30")),
|
||||||
("LESAVKA_CAM_ALLOW_PROFILE_OVERRIDE", None),
|
("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_FPS", Some("30")),
|
||||||
("LESAVKA_CAM_ALLOW_PROFILE_OVERRIDE", Some("1")),
|
("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),
|
|cfg| matches!(cfg.codec, CameraCodec::Mjpeg),
|
||||||
);
|
);
|
||||||
let jpeg_quality = env_u32("LESAVKA_CAM_JPEG_QUALITY", 85).clamp(1, 100);
|
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 keyframe_interval = env_u32("LESAVKA_CAM_KEYFRAME_INTERVAL", fps.min(5)).clamp(1, fps);
|
||||||
let source_profile = camera_source_profile(allow_mjpg_source);
|
let source_profile = camera_source_profile(allow_mjpg_source);
|
||||||
let use_mjpg_source = source_profile == CameraSourceProfile::Mjpeg;
|
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 {
|
let (enc, kf_prop) = if use_mjpg_source && !output_mjpeg {
|
||||||
("x264enc", Some("key-int-max"))
|
("x264enc", Some("key-int-max"))
|
||||||
} else {
|
} else {
|
||||||
@ -66,29 +70,29 @@ impl CameraCapture {
|
|||||||
}
|
}
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
let have_nvvidconv = gst::ElementFactory::find("nvvidconv").is_some();
|
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)
|
// Jetson (has nvvidconv) Desktop (falls back to videoconvert)
|
||||||
// ───────────────────────────────────────────────────────────────────
|
// ───────────────────────────────────────────────────────────────────
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
"nvh264enc" if have_nvvidconv =>
|
"nvh264enc" if have_nvvidconv =>
|
||||||
(format!(
|
format!(
|
||||||
"video/x-raw(memory:NVMM),format=NV12,width={width},height={height},framerate={fps}/1"
|
"nvvidconv ! video/x-raw(memory:NVMM),format=NV12,width={width},height={height},framerate={fps}/1 !"
|
||||||
), "nvvidconv !"),
|
),
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
"nvh264enc" /* else */ =>
|
"nvh264enc" /* else */ =>
|
||||||
(format!(
|
format!(
|
||||||
"video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1"
|
"videoconvert ! video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1 !"
|
||||||
), "videoconvert !"),
|
),
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
"vaapih264enc" =>
|
"vaapih264enc" =>
|
||||||
(format!(
|
format!(
|
||||||
"video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1"
|
"videoconvert ! video/x-raw,format=NV12,width={width},height={height},framerate={fps}/1 !"
|
||||||
), "videoconvert !"),
|
),
|
||||||
_ =>
|
_ =>
|
||||||
(format!(
|
format!(
|
||||||
"video/x-raw,width={width},height={height},framerate={fps}/1"
|
"videoconvert ! video/x-raw,width={width},height={height},framerate={fps}/1 !"
|
||||||
), "videoconvert !"),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// let desc = format!(
|
// let desc = format!(
|
||||||
@ -104,11 +108,24 @@ impl CameraCapture {
|
|||||||
// * x264enc needs the usual raw caps.
|
// * x264enc needs the usual raw caps.
|
||||||
let preview_tap_path = camera_preview_tap_path();
|
let preview_tap_path = camera_preview_tap_path();
|
||||||
let preview_tap_branch = camera_preview_tap_branch(width, height, fps);
|
let preview_tap_branch = camera_preview_tap_branch(width, height, fps);
|
||||||
let raw_source_chain =
|
let source_raw_caps = format!(
|
||||||
camera_raw_source_chain(&src_desc, &src_caps, width, height, fps, source_profile);
|
"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() {
|
let desc = if preview_tap_path.is_some() {
|
||||||
if output_mjpeg {
|
if output_mjpeg {
|
||||||
if use_mjpg_source {
|
if passthrough_mjpg_source {
|
||||||
format!(
|
format!(
|
||||||
"{src_desc} ! \
|
"{src_desc} ! \
|
||||||
image/jpeg,width={width},height={height},framerate={fps}/1 ! \
|
image/jpeg,width={width},height={height},framerate={fps}/1 ! \
|
||||||
@ -120,7 +137,7 @@ impl CameraCapture {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"{raw_source_chain} ! \
|
"{normalized_raw_chain} ! \
|
||||||
tee name=t \
|
tee name=t \
|
||||||
t. ! queue max-size-buffers=30 leaky=downstream ! \
|
t. ! queue max-size-buffers=30 leaky=downstream ! \
|
||||||
videoconvert ! jpegenc quality={jpeg_quality} ! \
|
videoconvert ! jpegenc quality={jpeg_quality} ! \
|
||||||
@ -129,22 +146,9 @@ impl CameraCapture {
|
|||||||
{preview_tap_branch}"
|
{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 {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"{raw_source_chain} ! \
|
"{normalized_raw_chain} ! \
|
||||||
tee name=t \
|
tee name=t \
|
||||||
t. ! queue max-size-buffers=30 leaky=downstream ! \
|
t. ! queue max-size-buffers=30 leaky=downstream ! \
|
||||||
{preenc} {enc_opts} ! \
|
{preenc} {enc_opts} ! \
|
||||||
@ -155,7 +159,7 @@ impl CameraCapture {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else if output_mjpeg {
|
} else if output_mjpeg {
|
||||||
if use_mjpg_source {
|
if passthrough_mjpg_source {
|
||||||
format!(
|
format!(
|
||||||
"{src_desc} ! \
|
"{src_desc} ! \
|
||||||
image/jpeg,width={width},height={height},framerate={fps}/1 ! \
|
image/jpeg,width={width},height={height},framerate={fps}/1 ! \
|
||||||
@ -164,32 +168,32 @@ impl CameraCapture {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"{raw_source_chain} ! \
|
"{normalized_raw_chain} ! \
|
||||||
videoconvert ! jpegenc quality={jpeg_quality} ! \
|
videoconvert ! jpegenc quality={jpeg_quality} ! \
|
||||||
queue max-size-buffers=30 leaky=downstream ! \
|
queue max-size-buffers=30 leaky=downstream ! \
|
||||||
appsink name=asink emit-signals=true max-buffers=60 drop=true"
|
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 {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"{raw_source_chain} ! \
|
"{normalized_raw_chain} ! \
|
||||||
{preenc} {enc_opts} ! \
|
{preenc} {enc_opts} ! \
|
||||||
h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au ! \
|
h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||||
queue max-size-buffers=30 leaky=downstream ! \
|
queue max-size-buffers=30 leaky=downstream ! \
|
||||||
appsink name=asink emit-signals=true max-buffers=60 drop=true"
|
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)
|
let pipeline: gst::Pipeline = gst::parse::launch(&desc)
|
||||||
.context("gst parse_launch(cam)")?
|
.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) {
|
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_WIDTH", cfg.map_or(1280, |cfg| cfg.width)),
|
||||||
env_u32("LESAVKA_CAM_HEIGHT", cfg.map_or(720, |cfg| cfg.height)),
|
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 {
|
fn env_flag_enabled(name: &str) -> bool {
|
||||||
std::env::var(name).ok().is_some_and(|value| {
|
std::env::var(name).ok().is_some_and(|value| {
|
||||||
let trimmed = value.trim();
|
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> {
|
fn camera_preview_tap_path() -> Option<PathBuf> {
|
||||||
std::env::var(CAMERA_PREVIEW_TAP_ENV)
|
std::env::var(CAMERA_PREVIEW_TAP_ENV)
|
||||||
.ok()
|
.ok()
|
||||||
|
|||||||
@ -10,12 +10,17 @@
|
|||||||
state
|
state
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.select_microphone(selected_combo_value(µphone_combo_read));
|
.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(
|
widgets.status_label.set_text(
|
||||||
"Microphone selection changed. Restart Monitor Mic to audition the new input.",
|
"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());
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -34,12 +39,17 @@
|
|||||||
let speaker_running = tests.borrow_mut().is_running(DeviceTestKind::Speaker);
|
let speaker_running = tests.borrow_mut().is_running(DeviceTestKind::Speaker);
|
||||||
let microphone_running =
|
let microphone_running =
|
||||||
tests.borrow_mut().is_running(DeviceTestKind::Microphone);
|
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(
|
widgets.status_label.set_text(
|
||||||
"Speaker selection changed. Restart the local audio tests to hear the new output.",
|
"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());
|
refresh_test_buttons(&widgets, &mut tests.borrow_mut());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,10 @@
|
|||||||
widgets
|
widgets
|
||||||
.status_label
|
.status_label
|
||||||
.set_text(&format!("Camera quality update failed: {err}"));
|
.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 {
|
} else if preview_was_running {
|
||||||
widgets.status_label.set_text(&format!(
|
widgets.status_label.set_text(&format!(
|
||||||
"Local camera preview switched to {}{}.",
|
"Local camera preview switched to {}{}.",
|
||||||
@ -68,6 +72,10 @@
|
|||||||
widgets
|
widgets
|
||||||
.status_label
|
.status_label
|
||||||
.set_text(&format!("Camera quality update failed: {err}"));
|
.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 {
|
} else if preview_was_running {
|
||||||
widgets.status_label.set_text(&format!(
|
widgets.status_label.set_text(&format!(
|
||||||
"Local camera preview switched to {}.",
|
"Local camera preview switched to {}.",
|
||||||
|
|||||||
@ -235,8 +235,8 @@
|
|||||||
console_level_combo.set_tooltip_text(Some("Show relay logs at this level or higher."));
|
console_level_combo.set_tooltip_text(Some("Show relay logs at this level or higher."));
|
||||||
let console_copy_button = gtk::Button::with_label("Copy");
|
let console_copy_button = gtk::Button::with_label("Copy");
|
||||||
console_copy_button.set_tooltip_text(Some("Copy visible log."));
|
console_copy_button.set_tooltip_text(Some("Copy visible log."));
|
||||||
let console_popout_button = gtk::Button::with_label("Pop Out");
|
let console_popout_button = gtk::Button::with_label("Break Out");
|
||||||
console_popout_button.set_tooltip_text(Some("Open log window."));
|
console_popout_button.set_tooltip_text(Some("Break out the log window."));
|
||||||
let console_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let console_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
console_buttons.set_hexpand(true);
|
console_buttons.set_hexpand(true);
|
||||||
console_buttons.set_homogeneous(true);
|
console_buttons.set_homogeneous(true);
|
||||||
|
|||||||
@ -171,25 +171,24 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
widgets
|
widgets
|
||||||
.uvc_recover_button
|
.uvc_recover_button
|
||||||
.set_sensitive(state.server_available);
|
.set_sensitive(state.server_available);
|
||||||
widgets.device_refresh_button.set_sensitive(!relay_live);
|
widgets.device_refresh_button.set_sensitive(true);
|
||||||
widgets
|
widgets
|
||||||
.camera_combo
|
.camera_combo
|
||||||
.set_sensitive(!relay_live && state.channels.camera);
|
.set_sensitive(state.channels.camera);
|
||||||
widgets.camera_quality_combo.set_sensitive(
|
widgets.camera_quality_combo.set_sensitive(
|
||||||
!relay_live
|
state.channels.camera
|
||||||
&& state.channels.camera
|
|
||||||
&& state.devices.camera.is_some()
|
&& state.devices.camera.is_some()
|
||||||
&& state.camera_quality.is_some(),
|
&& state.camera_quality.is_some(),
|
||||||
);
|
);
|
||||||
widgets
|
widgets
|
||||||
.microphone_combo
|
.microphone_combo
|
||||||
.set_sensitive(!relay_live && state.channels.microphone);
|
.set_sensitive(state.channels.microphone);
|
||||||
widgets
|
widgets
|
||||||
.speaker_combo
|
.speaker_combo
|
||||||
.set_sensitive(!relay_live && state.channels.audio);
|
.set_sensitive(state.channels.audio);
|
||||||
widgets
|
widgets
|
||||||
.audio_gain_scale
|
.audio_gain_scale
|
||||||
.set_sensitive(!relay_live && state.channels.audio);
|
.set_sensitive(state.channels.audio);
|
||||||
widgets.keyboard_combo.set_sensitive(!relay_live);
|
widgets.keyboard_combo.set_sensitive(!relay_live);
|
||||||
widgets.mouse_combo.set_sensitive(!relay_live);
|
widgets.mouse_combo.set_sensitive(!relay_live);
|
||||||
widgets
|
widgets
|
||||||
@ -223,7 +222,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
.set_sensitive(!relay_live && state.channels.microphone);
|
.set_sensitive(!relay_live && state.channels.microphone);
|
||||||
widgets
|
widgets
|
||||||
.mic_gain_scale
|
.mic_gain_scale
|
||||||
.set_sensitive(!relay_live && state.channels.microphone);
|
.set_sensitive(state.channels.microphone);
|
||||||
widgets
|
widgets
|
||||||
.speaker_test_button
|
.speaker_test_button
|
||||||
.set_sensitive(!relay_live && state.channels.audio);
|
.set_sensitive(!relay_live && state.channels.audio);
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.16.8"
|
version = "0.16.9"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -58,7 +58,7 @@
|
|||||||
"client/src/input/camera.rs": {
|
"client/src/input/camera.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 111
|
"loc": 146
|
||||||
},
|
},
|
||||||
"client/src/input/camera/bus_and_encoder.rs": {
|
"client/src/input/camera/bus_and_encoder.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -68,7 +68,7 @@
|
|||||||
"client/src/input/camera/capture_pipeline.rs": {
|
"client/src/input/camera/capture_pipeline.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 4,
|
"doc_debt": 4,
|
||||||
"loc": 320
|
"loc": 334
|
||||||
},
|
},
|
||||||
"client/src/input/camera/device_selection.rs": {
|
"client/src/input/camera/device_selection.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -88,7 +88,7 @@
|
|||||||
"client/src/input/camera/source_description.rs": {
|
"client/src/input/camera/source_description.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 76
|
"loc": 84
|
||||||
},
|
},
|
||||||
"client/src/input/inputs.rs": {
|
"client/src/input/inputs.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -338,7 +338,7 @@
|
|||||||
"client/src/launcher/ui/media_device_bindings.rs": {
|
"client/src/launcher/ui/media_device_bindings.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 182
|
"loc": 192
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui/message_and_network_state.rs": {
|
"client/src/launcher/ui/message_and_network_state.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
@ -373,7 +373,7 @@
|
|||||||
"client/src/launcher/ui/stage_device_bindings.rs": {
|
"client/src/launcher/ui/stage_device_bindings.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
"doc_debt": 0,
|
"doc_debt": 0,
|
||||||
"loc": 176
|
"loc": 184
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui/startup_window_guard.rs": {
|
"client/src/launcher/ui/startup_window_guard.rs": {
|
||||||
"clippy_warnings": 0,
|
"clippy_warnings": 0,
|
||||||
|
|||||||
@ -47,7 +47,18 @@ MEDIA_TESTS=(
|
|||||||
start_seconds=$(date +%s)
|
start_seconds=$(date +%s)
|
||||||
status=0
|
status=0
|
||||||
set +e
|
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]}
|
status=${PIPESTATUS[0]}
|
||||||
set -e
|
set -e
|
||||||
end_seconds=$(date +%s)
|
end_seconds=$(date +%s)
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.16.8"
|
version = "0.16.9"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
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/activation_setup.rs"),
|
||||||
include_str!("../../client/src/launcher/ui/device_refresh_binding.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/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/relay_input_bindings.rs"),
|
||||||
include_str!("../../client/src/launcher/ui/runtime_poll.rs"),
|
include_str!("../../client/src/launcher/ui/runtime_poll.rs"),
|
||||||
include_str!("../../client/src/launcher/ui/stage_device_bindings.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]
|
#[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("widgets.server_entry.set_sensitive(!relay_live);"));
|
||||||
assert!(
|
assert!(
|
||||||
UI_RUNTIME_SRC.contains(
|
UI_RUNTIME_SRC.contains(".camera_combo\n .set_sensitive(state.channels.camera);")
|
||||||
".camera_combo\n .set_sensitive(!relay_live && state.channels.camera);"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
assert!(UI_RUNTIME_SRC.contains(".camera_quality_combo"));
|
assert!(UI_RUNTIME_SRC.contains(".camera_quality_combo"));
|
||||||
assert!(UI_RUNTIME_SRC.contains("widgets.camera_quality_combo.set_sensitive("));
|
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.devices.camera.is_some()"));
|
||||||
assert!(UI_RUNTIME_SRC.contains("state.camera_quality.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!(
|
assert!(
|
||||||
UI_RUNTIME_SRC.contains(
|
UI_RUNTIME_SRC
|
||||||
".speaker_combo\n .set_sensitive(!relay_live && state.channels.audio);"
|
.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(".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(".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.keyboard_combo.set_sensitive(!relay_live);"));
|
||||||
assert!(UI_RUNTIME_SRC.contains("widgets.mouse_combo.set_sensitive(!relay_live);"));
|
assert!(UI_RUNTIME_SRC.contains("widgets.mouse_combo.set_sensitive(!relay_live);"));
|
||||||
assert!(
|
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_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("\"Connect\""));
|
||||||
assert!(UI_RUNTIME_SRC.contains("\"Disconnect\""));
|
assert!(UI_RUNTIME_SRC.contains("\"Disconnect\""));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user