launcher: expose upstream webcam transport
This commit is contained in:
parent
117323a10a
commit
fb466abdfd
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.21.15"
|
||||
version = "0.21.16"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1686,7 +1686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.21.15"
|
||||
version = "0.21.16"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1698,7 +1698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.21.15"
|
||||
version = "0.21.16"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.21.15"
|
||||
version = "0.21.16"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -22,7 +22,10 @@ pub fn resolve_server_addr(args: &[String], env_addr: Option<&str>) -> String {
|
||||
#[must_use]
|
||||
/// Build local camera capture settings from negotiated peer capabilities.
|
||||
pub fn camera_config_from_caps(caps: &PeerCaps) -> Option<CameraConfig> {
|
||||
let codec = parse_camera_codec(caps.camera_codec.as_deref()?)?;
|
||||
let codec = std::env::var("LESAVKA_CAM_CODEC")
|
||||
.ok()
|
||||
.and_then(|value| parse_camera_codec(&value))
|
||||
.or_else(|| parse_camera_codec(caps.camera_codec.as_deref()?))?;
|
||||
Some(CameraConfig {
|
||||
codec,
|
||||
width: caps.camera_width?,
|
||||
@ -63,6 +66,7 @@ mod tests {
|
||||
};
|
||||
use crate::handshake::PeerCaps;
|
||||
use crate::input::camera::CameraCodec;
|
||||
use serial_test::serial;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
@ -94,14 +98,47 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn camera_config_from_caps_requires_complete_profile() {
|
||||
let mut caps = PeerCaps {
|
||||
temp_env::with_var("LESAVKA_CAM_CODEC", None::<&str>, || {
|
||||
let mut caps = PeerCaps {
|
||||
camera: true,
|
||||
microphone: false,
|
||||
bundled_webcam_media: false,
|
||||
server_version: None,
|
||||
camera_output: Some(String::from("uvc")),
|
||||
camera_codec: Some(String::from("mjpeg")),
|
||||
camera_width: Some(1280),
|
||||
camera_height: Some(720),
|
||||
camera_fps: Some(25),
|
||||
eye_width: None,
|
||||
eye_height: None,
|
||||
eye_fps: None,
|
||||
};
|
||||
|
||||
let config = camera_config_from_caps(&caps).expect("complete caps should map");
|
||||
assert!(matches!(config.codec, CameraCodec::Mjpeg));
|
||||
assert_eq!(config.width, 1280);
|
||||
|
||||
caps.camera_codec = Some(String::from("h265"));
|
||||
let config = camera_config_from_caps(&caps).expect("h265 alias should map");
|
||||
assert!(matches!(config.codec, CameraCodec::Hevc));
|
||||
|
||||
caps.camera_codec = Some(String::from("vp9"));
|
||||
assert!(camera_config_from_caps(&caps).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn camera_config_from_caps_honors_launcher_codec_override() {
|
||||
let caps = PeerCaps {
|
||||
camera: true,
|
||||
microphone: false,
|
||||
bundled_webcam_media: false,
|
||||
server_version: None,
|
||||
camera_output: Some(String::from("uvc")),
|
||||
camera_codec: Some(String::from("mjpeg")),
|
||||
camera_codec: Some(String::from("hevc")),
|
||||
camera_width: Some(1280),
|
||||
camera_height: Some(720),
|
||||
camera_fps: Some(25),
|
||||
@ -110,16 +147,10 @@ mod tests {
|
||||
eye_fps: None,
|
||||
};
|
||||
|
||||
let config = camera_config_from_caps(&caps).expect("complete caps should map");
|
||||
assert!(matches!(config.codec, CameraCodec::Mjpeg));
|
||||
assert_eq!(config.width, 1280);
|
||||
|
||||
caps.camera_codec = Some(String::from("h265"));
|
||||
let config = camera_config_from_caps(&caps).expect("h265 alias should map");
|
||||
assert!(matches!(config.codec, CameraCodec::Hevc));
|
||||
|
||||
caps.camera_codec = Some(String::from("vp9"));
|
||||
assert!(camera_config_from_caps(&caps).is_none());
|
||||
temp_env::with_var("LESAVKA_CAM_CODEC", Some("mjpeg"), || {
|
||||
let config = camera_config_from_caps(&caps).expect("override should map");
|
||||
assert!(matches!(config.codec, CameraCodec::Mjpeg));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -154,6 +154,7 @@ pub struct SnapshotReport {
|
||||
pub right_rendered_caps_label: String,
|
||||
pub selected_camera: Option<String>,
|
||||
pub camera_quality_label: String,
|
||||
pub upstream_camera_transport: String,
|
||||
pub selected_microphone: Option<String>,
|
||||
pub selected_speaker: Option<String>,
|
||||
pub media_channels: MediaChannelState,
|
||||
|
||||
@ -228,6 +228,7 @@ impl SnapshotReport {
|
||||
.camera_quality
|
||||
.map(CameraMode::short_label)
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
upstream_camera_transport: state.webcam_transport.label().to_string(),
|
||||
selected_microphone: state.devices.microphone.clone(),
|
||||
selected_speaker: state.devices.speaker.clone(),
|
||||
media_channels: MediaChannelState {
|
||||
|
||||
@ -90,9 +90,10 @@ impl SnapshotReport {
|
||||
let _ = writeln!(text, "media staging");
|
||||
let _ = writeln!(
|
||||
text,
|
||||
" camera: {} | quality={} | enabled={}",
|
||||
" camera: {} | quality={} | transport={} | enabled={}",
|
||||
self.selected_camera.as_deref().unwrap_or("auto"),
|
||||
self.camera_quality_label,
|
||||
self.upstream_camera_transport,
|
||||
self.media_channels.camera
|
||||
);
|
||||
let _ = writeln!(
|
||||
|
||||
@ -159,6 +159,10 @@ pub fn runtime_env_vars(state: &LauncherState) -> BTreeMap<String, String> {
|
||||
state.audio_gain_env_value(),
|
||||
);
|
||||
envs.insert("LESAVKA_MIC_GAIN".to_string(), state.mic_gain_env_value());
|
||||
envs.insert(
|
||||
"LESAVKA_CAM_CODEC".to_string(),
|
||||
state.webcam_transport.env_value().to_string(),
|
||||
);
|
||||
if matches!(state.view_mode, ViewMode::Unified) {
|
||||
envs.insert("LESAVKA_DISABLE_VIDEO_RENDER".to_string(), "1".to_string());
|
||||
}
|
||||
|
||||
@ -310,6 +310,10 @@ impl LauncherState {
|
||||
self.camera_quality = mode;
|
||||
}
|
||||
|
||||
pub fn select_webcam_transport(&mut self, transport: WebcamTransport) {
|
||||
self.webcam_transport = transport;
|
||||
}
|
||||
|
||||
pub fn camera_quality_options(&self, catalog: &DeviceCatalog) -> Vec<CameraMode> {
|
||||
self.devices
|
||||
.camera
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
impl LauncherState {
|
||||
pub fn status_line(&self) -> String {
|
||||
format!(
|
||||
"server={} mode={} view={} active={} power={} source={}x{} d1={} d2={} s1={} s2={} camera={} camera_quality={} mic={} speaker={} channels=cam:{}/mic:{}/audio:{} remote_caps=cam:{:?}/mic:{:?}/output:{:?}/codec:{:?} cal={}:{:+.1}ms audio_gain={} mic_gain={} kbd={} mouse={} swap={}",
|
||||
"server={} mode={} view={} active={} power={} source={}x{} d1={} d2={} s1={} s2={} camera={} camera_quality={} camera_transport={} mic={} speaker={} channels=cam:{}/mic:{}/audio:{} remote_caps=cam:{:?}/mic:{:?}/output:{:?}/codec:{:?} cal={}:{:+.1}ms audio_gain={} mic_gain={} kbd={} mouse={} swap={}",
|
||||
self.server_available,
|
||||
match self.routing {
|
||||
InputRouting::Local => "local",
|
||||
@ -27,6 +27,7 @@ impl LauncherState {
|
||||
self.camera_quality
|
||||
.map(CameraMode::short_label)
|
||||
.unwrap_or_else(|| "default".to_string()),
|
||||
self.webcam_transport.label(),
|
||||
media_status_label(self.channels.microphone, self.devices.microphone.as_deref()),
|
||||
media_status_label(self.channels.audio, self.devices.speaker.as_deref()),
|
||||
self.channels.camera,
|
||||
|
||||
@ -27,6 +27,66 @@ impl InputRouting {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum WebcamTransport {
|
||||
Hevc,
|
||||
Mjpeg,
|
||||
}
|
||||
|
||||
impl WebcamTransport {
|
||||
/// Return the stable GTK row id for the upstream webcam transport.
|
||||
///
|
||||
/// Inputs: the selected transport. Output: the id stored in the combo box.
|
||||
/// Why: the UI must map operator intent onto the exact codec environment
|
||||
/// variable the relay child consumes when it starts its webcam pipeline.
|
||||
pub const fn as_id(self) -> &'static str {
|
||||
match self {
|
||||
Self::Hevc => "hevc",
|
||||
Self::Mjpeg => "mjpeg",
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a GTK row id back into an upstream webcam transport.
|
||||
///
|
||||
/// Inputs: raw combo id. Output: a supported transport when recognized.
|
||||
/// Why: unsupported codecs should never silently enter launcher state,
|
||||
/// since the server decoder path is calibrated per ingress codec.
|
||||
pub fn from_id(raw: &str) -> Option<Self> {
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"hevc" | "h265" | "h.265" => Some(Self::Hevc),
|
||||
"mjpeg" | "mjpg" | "jpeg" => Some(Self::Mjpeg),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the environment value consumed by the relay child.
|
||||
///
|
||||
/// Inputs: the selected transport. Output: canonical `LESAVKA_CAM_CODEC`.
|
||||
/// Why: the launcher and headless client share the same camera pipeline
|
||||
/// selector, so keeping this value canonical prevents HEVC/MJPEG drift.
|
||||
pub const fn env_value(self) -> &'static str {
|
||||
self.as_id()
|
||||
}
|
||||
|
||||
/// Short label displayed in the launcher dropdown.
|
||||
///
|
||||
/// Inputs: the selected transport. Output: a human-readable label. Why:
|
||||
/// this makes the compact control understandable without implying YUY2 or
|
||||
/// H.264 are ready for the current calibrated upstream path.
|
||||
pub const fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Hevc => "HEVC",
|
||||
Self::Mjpeg => "MJPEG",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WebcamTransport {
|
||||
fn default() -> Self {
|
||||
Self::Hevc
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ViewMode {
|
||||
Unified,
|
||||
|
||||
@ -276,6 +276,7 @@ pub struct LauncherState {
|
||||
pub breakout_sizes: [BreakoutSizePreset; 2],
|
||||
pub devices: DeviceSelection,
|
||||
pub camera_quality: Option<CameraMode>,
|
||||
pub webcam_transport: WebcamTransport,
|
||||
pub channels: ChannelSelection,
|
||||
pub audio_gain_percent: u32,
|
||||
pub mic_gain_percent: u32,
|
||||
@ -313,6 +314,7 @@ impl Default for LauncherState {
|
||||
breakout_sizes: [BreakoutSizePreset::Source, BreakoutSizePreset::Source],
|
||||
devices: DeviceSelection::default(),
|
||||
camera_quality: None,
|
||||
webcam_transport: WebcamTransport::default(),
|
||||
channels: ChannelSelection::default(),
|
||||
audio_gain_percent: DEFAULT_AUDIO_GAIN_PERCENT,
|
||||
mic_gain_percent: DEFAULT_MIC_GAIN_PERCENT,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use super::*;
|
||||
use crate::launcher::state::{
|
||||
CaptureSizePreset, DeviceSelection, DisplaySurface, FeedSourcePreset, LauncherState,
|
||||
WebcamTransport,
|
||||
};
|
||||
use crate::uplink_telemetry::UpstreamStreamTelemetry;
|
||||
|
||||
@ -157,6 +158,7 @@ fn snapshot_report_contains_state_fields_and_samples() {
|
||||
assert!(report.left_stream_caps_label.contains("video/x-h264"));
|
||||
assert!(report.left_decoded_caps_label.contains("video/x-raw"));
|
||||
assert!(report.left_rendered_caps_label.contains("video/x-raw"));
|
||||
assert_eq!(report.upstream_camera_transport, "HEVC");
|
||||
assert_eq!(report.upstream_camera.queue_peak, 7);
|
||||
assert_eq!(report.upstream_microphone.reconnect_count, 1);
|
||||
}
|
||||
@ -240,6 +242,7 @@ fn snapshot_text_reflects_live_media_control_changes() {
|
||||
state.select_camera_quality(Some(crate::launcher::devices::CameraMode::new(
|
||||
1920, 1080, 30,
|
||||
)));
|
||||
state.select_webcam_transport(WebcamTransport::Mjpeg);
|
||||
state.select_microphone(Some("alsa_input.usb".to_string()));
|
||||
state.select_speaker(Some("alsa_output.usb".to_string()));
|
||||
state.set_audio_gain_percent(250);
|
||||
@ -254,7 +257,9 @@ fn snapshot_text_reflects_live_media_control_changes() {
|
||||
);
|
||||
let text = report.to_pretty_text();
|
||||
|
||||
assert!(text.contains("camera: /dev/video9 | quality=1080p@30 | enabled=false"));
|
||||
assert!(
|
||||
text.contains("camera: /dev/video9 | quality=1080p@30 | transport=MJPEG | enabled=false")
|
||||
);
|
||||
assert!(text.contains("speaker: alsa_output.usb | volume=250% | enabled=true"));
|
||||
assert!(text.contains("microphone: alsa_input.usb | gain=125% | enabled=true"));
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::launcher::state::WebcamTransport;
|
||||
use serial_test::serial;
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
@ -161,6 +162,7 @@ fn runtime_env_vars_emit_selected_controls() {
|
||||
);
|
||||
assert_eq!(envs.get("LESAVKA_AUDIO_GAIN"), Some(&"2.000".to_string()));
|
||||
assert_eq!(envs.get("LESAVKA_MIC_GAIN"), Some(&"1.000".to_string()));
|
||||
assert_eq!(envs.get("LESAVKA_CAM_CODEC"), Some(&"hevc".to_string()));
|
||||
assert_eq!(envs.get("LESAVKA_CAM_WIDTH"), Some(&"1920".to_string()));
|
||||
assert_eq!(envs.get("LESAVKA_CAM_HEIGHT"), Some(&"1080".to_string()));
|
||||
assert_eq!(envs.get("LESAVKA_CAM_FPS"), Some(&"30".to_string()));
|
||||
@ -252,6 +254,21 @@ fn runtime_env_vars_keeps_remote_failsafe_disabled_for_invalid_launch_option() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_env_vars_emit_selected_webcam_transport() {
|
||||
let mut state = LauncherState::new();
|
||||
assert_eq!(
|
||||
runtime_env_vars(&state).get("LESAVKA_CAM_CODEC"),
|
||||
Some(&"hevc".to_string())
|
||||
);
|
||||
|
||||
state.select_webcam_transport(WebcamTransport::Mjpeg);
|
||||
assert_eq!(
|
||||
runtime_env_vars(&state).get("LESAVKA_CAM_CODEC"),
|
||||
Some(&"mjpeg".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_env_vars_do_not_disable_breakout_video_windows() {
|
||||
let mut state = LauncherState::new();
|
||||
|
||||
@ -8,6 +8,20 @@ fn routing_and_view_env_values_are_stable() {
|
||||
assert_eq!(ViewMode::Breakout.as_env(), "breakout");
|
||||
assert_eq!(DisplaySurface::Preview.label(), "preview");
|
||||
assert_eq!(DisplaySurface::Window.label(), "window");
|
||||
assert_eq!(WebcamTransport::default(), WebcamTransport::Hevc);
|
||||
assert_eq!(WebcamTransport::Hevc.as_id(), "hevc");
|
||||
assert_eq!(WebcamTransport::Mjpeg.as_id(), "mjpeg");
|
||||
assert_eq!(WebcamTransport::Hevc.env_value(), "hevc");
|
||||
assert_eq!(WebcamTransport::Mjpeg.label(), "MJPEG");
|
||||
assert_eq!(
|
||||
WebcamTransport::from_id("h265"),
|
||||
Some(WebcamTransport::Hevc)
|
||||
);
|
||||
assert_eq!(
|
||||
WebcamTransport::from_id("jpeg"),
|
||||
Some(WebcamTransport::Mjpeg)
|
||||
);
|
||||
assert_eq!(WebcamTransport::from_id("yuy2"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -2,7 +2,7 @@ use super::*;
|
||||
use crate::launcher::{
|
||||
devices::{CameraMode, DeviceCatalog},
|
||||
preview::PreviewBinding,
|
||||
state::{BreakoutSizePreset, LauncherState, PreviewSourceSize},
|
||||
state::{BreakoutSizePreset, LauncherState, PreviewSourceSize, WebcamTransport},
|
||||
ui_components::build_launcher_view,
|
||||
};
|
||||
use crate::uplink_telemetry::UpstreamStreamTelemetry;
|
||||
@ -322,6 +322,59 @@ fn recovery_buttons_use_upstream_device_labels() {
|
||||
);
|
||||
}
|
||||
|
||||
#[gtk::test]
|
||||
#[serial]
|
||||
fn webcam_transport_combo_tracks_selected_upstream_codec() {
|
||||
if gtk::gdk::Display::default().is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let app = gtk::Application::builder()
|
||||
.application_id("dev.lesavka.test-webcam-transport")
|
||||
.build();
|
||||
let _ = app.register(None::<>k::gio::Cancellable>);
|
||||
|
||||
let mut state = LauncherState::new();
|
||||
state.set_camera_channel_enabled(true);
|
||||
let view = build_launcher_view(
|
||||
&app,
|
||||
"http://127.0.0.1:50051",
|
||||
&DeviceCatalog::default(),
|
||||
&state,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
view.device_stage
|
||||
.webcam_transport_combo
|
||||
.active_id()
|
||||
.as_deref(),
|
||||
Some("hevc")
|
||||
);
|
||||
assert!(view.device_stage.webcam_transport_combo.is_sensitive());
|
||||
|
||||
state.select_webcam_transport(WebcamTransport::Mjpeg);
|
||||
refresh_launcher_ui(&view.widgets, &state, false);
|
||||
assert_eq!(
|
||||
view.device_stage
|
||||
.webcam_transport_combo
|
||||
.active_id()
|
||||
.as_deref(),
|
||||
Some("mjpeg")
|
||||
);
|
||||
assert!(view.device_stage.webcam_transport_combo.is_sensitive());
|
||||
|
||||
refresh_launcher_ui(&view.widgets, &state, true);
|
||||
assert!(!view.device_stage.webcam_transport_combo.is_sensitive());
|
||||
assert!(
|
||||
view.device_stage
|
||||
.webcam_transport_combo
|
||||
.tooltip_text()
|
||||
.as_deref()
|
||||
.unwrap_or_default()
|
||||
.contains("Reconnect")
|
||||
);
|
||||
}
|
||||
|
||||
#[gtk::test]
|
||||
#[serial]
|
||||
fn diagnostics_and_log_popouts_install_native_window_chrome() {
|
||||
@ -515,7 +568,7 @@ fn uvc_chip_degrades_when_live_camera_frames_are_not_flowing() {
|
||||
|
||||
assert_eq!(
|
||||
recovery_uvc_health(&state, false, None),
|
||||
(StatusLightState::Live, "MJPEG".to_string())
|
||||
(StatusLightState::Live, "HEVC".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
recovery_uvc_health(&state, true, None),
|
||||
@ -531,6 +584,12 @@ fn uvc_chip_degrades_when_live_camera_frames_are_not_flowing() {
|
||||
queue_depth: 3,
|
||||
..UpstreamStreamTelemetry::default()
|
||||
};
|
||||
assert_eq!(
|
||||
recovery_uvc_health(&state, true, Some(&healthy)),
|
||||
(StatusLightState::Live, "HEVC".to_string())
|
||||
);
|
||||
|
||||
state.select_webcam_transport(WebcamTransport::Mjpeg);
|
||||
assert_eq!(
|
||||
recovery_uvc_health(&state, true, Some(&healthy)),
|
||||
(StatusLightState::Live, "MJPEG".to_string())
|
||||
|
||||
@ -20,7 +20,7 @@ use {
|
||||
super::state::{
|
||||
BreakoutSizePreset, CalibrationStatus, CapturePowerStatus, CaptureSizePreset,
|
||||
DisplaySurface, FeedSourcePreset, InputRouting, LauncherState, MAX_AUDIO_GAIN_PERCENT,
|
||||
MAX_MIC_GAIN_PERCENT, UpstreamSyncStatus,
|
||||
MAX_MIC_GAIN_PERCENT, UpstreamSyncStatus, WebcamTransport,
|
||||
},
|
||||
super::ui_components::{
|
||||
ConsoleLogLevel, build_launcher_view, sync_camera_quality_combo, sync_input_device_combo,
|
||||
|
||||
@ -51,6 +51,35 @@
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let state = Rc::clone(&state);
|
||||
let widgets = widgets.clone();
|
||||
let child_proc = Rc::clone(&child_proc);
|
||||
let transport_combo = widgets.webcam_transport_combo.clone();
|
||||
let transport_combo_read = transport_combo.clone();
|
||||
transport_combo.connect_changed(move |_| {
|
||||
let selected = transport_combo_read
|
||||
.active_id()
|
||||
.as_deref()
|
||||
.and_then(WebcamTransport::from_id)
|
||||
.unwrap_or_default();
|
||||
state.borrow_mut().select_webcam_transport(selected);
|
||||
let relay_live = child_proc.borrow().is_some();
|
||||
if relay_live {
|
||||
widgets.status_label.set_text(&format!(
|
||||
"Webcam transport changed to {} for the next reconnect; keeping the live decoder path stable.",
|
||||
selected.label()
|
||||
));
|
||||
} else {
|
||||
widgets.status_label.set_text(&format!(
|
||||
"Webcam transport set to {} for the next relay launch.",
|
||||
selected.label()
|
||||
));
|
||||
}
|
||||
refresh_launcher_ui(&widgets, &state.borrow(), relay_live);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let state = Rc::clone(&state);
|
||||
let widgets = widgets.clone();
|
||||
|
||||
@ -9,7 +9,7 @@ use super::{
|
||||
preview::{LauncherPreview, PreviewBinding, PreviewSurface},
|
||||
state::{
|
||||
BreakoutSizeChoice, BreakoutSizePreset, CaptureSizeChoice, CaptureSizePreset,
|
||||
FeedSourceChoice, FeedSourcePreset, LauncherState,
|
||||
FeedSourceChoice, FeedSourcePreset, LauncherState, WebcamTransport,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -330,14 +330,14 @@
|
||||
camera_preview_shell.append(&camera_preview_stack);
|
||||
let webcam_transport_combo = gtk::ComboBoxText::new();
|
||||
webcam_transport_combo.add_css_class("compact-combo");
|
||||
webcam_transport_combo.append(Some("mjpeg"), "MJPEG");
|
||||
webcam_transport_combo.append(Some("yuy2"), "YUY2");
|
||||
webcam_transport_combo.append(Some("h264"), "H.264");
|
||||
webcam_transport_combo.set_active_id(Some("mjpeg"));
|
||||
webcam_transport_combo.set_sensitive(false);
|
||||
for transport in [WebcamTransport::Hevc, WebcamTransport::Mjpeg] {
|
||||
webcam_transport_combo.append(Some(transport.as_id()), transport.label());
|
||||
}
|
||||
webcam_transport_combo.set_active_id(Some(state.webcam_transport.as_id()));
|
||||
webcam_transport_combo.set_sensitive(true);
|
||||
webcam_transport_combo.set_size_request(112, -1);
|
||||
webcam_transport_combo.set_tooltip_text(Some(
|
||||
"Upstream transport format. MJPEG is pinned while sync hardening is in progress.",
|
||||
"Upstream webcam transport for the next relay connection. HEVC is the low-latency default; MJPEG is the calibrated fallback.",
|
||||
));
|
||||
let webcam_group =
|
||||
build_subgroup_with_action("Webcam Preview", Some(webcam_transport_combo.upcast_ref()));
|
||||
|
||||
@ -162,7 +162,7 @@ fn recovery_uac_health(
|
||||
media_stream_health(stream, MediaStreamKind::Microphone)
|
||||
}
|
||||
|
||||
/// Summarize whether the UVC camera function is advertised with the expected codec.
|
||||
/// Summarize whether the UVC camera function is advertised with the selected upstream codec.
|
||||
fn recovery_uvc_health(
|
||||
state: &LauncherState,
|
||||
relay_live: bool,
|
||||
@ -171,11 +171,7 @@ fn recovery_uvc_health(
|
||||
if !state.server_available {
|
||||
return (StatusLightState::Idle, "Offline".to_string());
|
||||
}
|
||||
let codec = state
|
||||
.server_camera_codec
|
||||
.as_deref()
|
||||
.map(|value| value.to_ascii_uppercase())
|
||||
.unwrap_or_else(|| "READY".to_string());
|
||||
let codec = state.webcam_transport.label().to_string();
|
||||
if state.server_camera == Some(false) {
|
||||
return (StatusLightState::Warning, "Missing".to_string());
|
||||
}
|
||||
|
||||
@ -226,7 +226,23 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
||||
widgets
|
||||
.speaker_test_button
|
||||
.set_sensitive(!relay_live && state.channels.audio);
|
||||
widgets.webcam_transport_combo.set_sensitive(false);
|
||||
if widgets.webcam_transport_combo.active_id().as_deref()
|
||||
!= Some(state.webcam_transport.as_id())
|
||||
{
|
||||
widgets
|
||||
.webcam_transport_combo
|
||||
.set_active_id(Some(state.webcam_transport.as_id()));
|
||||
}
|
||||
widgets
|
||||
.webcam_transport_combo
|
||||
.set_sensitive(!relay_live && state.channels.camera);
|
||||
widgets
|
||||
.webcam_transport_combo
|
||||
.set_tooltip_text(Some(if relay_live {
|
||||
"Reconnect before changing the upstream webcam transport; the server decoder is calibrated per ingress codec."
|
||||
} else {
|
||||
"Choose HEVC for low-latency upstream video or MJPEG as the calibrated fallback for the next relay launch."
|
||||
}));
|
||||
widgets.input_toggle_button.set_label(match state.routing {
|
||||
InputRouting::Remote => "Route Local",
|
||||
InputRouting::Local => "Route Remote",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.21.15"
|
||||
version = "0.21.16"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.21.15"
|
||||
version = "0.21.16"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user