media: keep upstream codec aligned with server

This commit is contained in:
Brad Stein 2026-05-13 03:32:31 -03:00
parent 0ef34da971
commit ea0ca9f744
19 changed files with 109 additions and 34 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.22.20" version = "0.22.21"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.22.20" version = "0.22.21"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.22.20" version = "0.22.21"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package] [package]
name = "lesavka_client" name = "lesavka_client"
version = "0.22.20" version = "0.22.21"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -228,7 +228,7 @@ impl SnapshotReport {
.camera_quality .camera_quality
.map(CameraMode::short_label) .map(CameraMode::short_label)
.unwrap_or_else(|| "default".to_string()), .unwrap_or_else(|| "default".to_string()),
upstream_camera_transport: state.webcam_transport.label().to_string(), upstream_camera_transport: state.effective_webcam_transport().label().to_string(),
selected_microphone: state.devices.microphone.clone(), selected_microphone: state.devices.microphone.clone(),
selected_speaker: state.devices.speaker.clone(), selected_speaker: state.devices.speaker.clone(),
media_channels: MediaChannelState { media_channels: MediaChannelState {

View File

@ -161,7 +161,7 @@ pub fn runtime_env_vars(state: &LauncherState) -> BTreeMap<String, String> {
envs.insert("LESAVKA_MIC_GAIN".to_string(), state.mic_gain_env_value()); envs.insert("LESAVKA_MIC_GAIN".to_string(), state.mic_gain_env_value());
envs.insert( envs.insert(
"LESAVKA_CAM_CODEC".to_string(), "LESAVKA_CAM_CODEC".to_string(),
state.webcam_transport.env_value().to_string(), state.effective_webcam_transport().env_value().to_string(),
); );
envs.insert( envs.insert(
"LESAVKA_UPLINK_AUDIO_CODEC".to_string(), "LESAVKA_UPLINK_AUDIO_CODEC".to_string(),

View File

@ -47,6 +47,13 @@ impl LauncherState {
Some(trimmed.to_string()) Some(trimmed.to_string())
} }
}); });
if let Some(transport) = self
.server_camera_codec
.as_deref()
.and_then(WebcamTransport::from_server_codec)
{
self.webcam_transport = transport;
}
} }
pub fn set_view_mode(&mut self, view_mode: ViewMode) { pub fn set_view_mode(&mut self, view_mode: ViewMode) {
@ -314,6 +321,13 @@ impl LauncherState {
self.webcam_transport = transport; self.webcam_transport = transport;
} }
pub fn effective_webcam_transport(&self) -> WebcamTransport {
self.server_camera_codec
.as_deref()
.and_then(WebcamTransport::from_server_codec)
.unwrap_or(self.webcam_transport)
}
pub fn select_upstream_audio_transport(&mut self, transport: UpstreamAudioTransport) { pub fn select_upstream_audio_transport(&mut self, transport: UpstreamAudioTransport) {
self.upstream_audio_transport = transport; self.upstream_audio_transport = transport;
} }

View File

@ -27,7 +27,7 @@ impl LauncherState {
self.camera_quality self.camera_quality
.map(CameraMode::short_label) .map(CameraMode::short_label)
.unwrap_or_else(|| "default".to_string()), .unwrap_or_else(|| "default".to_string()),
self.webcam_transport.label(), self.effective_webcam_transport().label(),
media_status_label(self.channels.microphone, self.devices.microphone.as_deref()), media_status_label(self.channels.microphone, self.devices.microphone.as_deref()),
self.upstream_audio_transport.label(), self.upstream_audio_transport.label(),
self.mic_noise_suppression, self.mic_noise_suppression,

View File

@ -29,8 +29,8 @@ impl InputRouting {
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum WebcamTransport { pub enum WebcamTransport {
#[default]
Hevc, Hevc,
#[default]
Mjpeg, Mjpeg,
} }
@ -60,6 +60,16 @@ impl WebcamTransport {
} }
} }
/// Parse the camera codec advertised by the server caps response.
///
/// Inputs: raw server codec string. Output: matching launcher transport.
/// Why: the relay child must send the codec the server is currently
/// configured to consume, otherwise MJPEG UVC can receive HEVC bytes and
/// browsers show a black webcam preview.
pub fn from_server_codec(raw: &str) -> Option<Self> {
Self::from_id(raw)
}
/// Return the environment value consumed by the relay child. /// Return the environment value consumed by the relay child.
/// ///
/// Inputs: the selected transport. Output: canonical `LESAVKA_CAM_CODEC`. /// Inputs: the selected transport. Output: canonical `LESAVKA_CAM_CODEC`.

View File

@ -158,7 +158,7 @@ fn snapshot_report_contains_state_fields_and_samples() {
assert!(report.left_stream_caps_label.contains("video/x-h264")); assert!(report.left_stream_caps_label.contains("video/x-h264"));
assert!(report.left_decoded_caps_label.contains("video/x-raw")); assert!(report.left_decoded_caps_label.contains("video/x-raw"));
assert!(report.left_rendered_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_transport, "MJPEG");
assert_eq!(report.upstream_camera.queue_peak, 7); assert_eq!(report.upstream_camera.queue_peak, 7);
assert_eq!(report.upstream_microphone.reconnect_count, 1); assert_eq!(report.upstream_microphone.reconnect_count, 1);
} }

View File

@ -162,7 +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_AUDIO_GAIN"), Some(&"2.000".to_string()));
assert_eq!(envs.get("LESAVKA_MIC_GAIN"), Some(&"1.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_CODEC"), Some(&"mjpeg".to_string()));
assert_eq!( assert_eq!(
envs.get("LESAVKA_UPLINK_AUDIO_CODEC"), envs.get("LESAVKA_UPLINK_AUDIO_CODEC"),
Some(&"opus".to_string()) Some(&"opus".to_string())
@ -265,12 +265,23 @@ fn runtime_env_vars_keeps_remote_failsafe_disabled_for_invalid_launch_option() {
#[test] #[test]
fn runtime_env_vars_emit_selected_webcam_transport() { fn runtime_env_vars_emit_selected_webcam_transport() {
let mut state = LauncherState::new(); let mut state = LauncherState::new();
assert_eq!(
runtime_env_vars(&state).get("LESAVKA_CAM_CODEC"),
Some(&"mjpeg".to_string())
);
state.select_webcam_transport(WebcamTransport::Hevc);
assert_eq!( assert_eq!(
runtime_env_vars(&state).get("LESAVKA_CAM_CODEC"), runtime_env_vars(&state).get("LESAVKA_CAM_CODEC"),
Some(&"hevc".to_string()) Some(&"hevc".to_string())
); );
state.select_webcam_transport(WebcamTransport::Mjpeg); state.set_server_media_caps(
None,
None,
Some("uvc".to_string()),
Some("mjpeg".to_string()),
);
assert_eq!( assert_eq!(
runtime_env_vars(&state).get("LESAVKA_CAM_CODEC"), runtime_env_vars(&state).get("LESAVKA_CAM_CODEC"),
Some(&"mjpeg".to_string()) Some(&"mjpeg".to_string())

View File

@ -8,7 +8,7 @@ fn routing_and_view_env_values_are_stable() {
assert_eq!(ViewMode::Breakout.as_env(), "breakout"); assert_eq!(ViewMode::Breakout.as_env(), "breakout");
assert_eq!(DisplaySurface::Preview.label(), "preview"); assert_eq!(DisplaySurface::Preview.label(), "preview");
assert_eq!(DisplaySurface::Window.label(), "window"); assert_eq!(DisplaySurface::Window.label(), "window");
assert_eq!(WebcamTransport::default(), WebcamTransport::Hevc); assert_eq!(WebcamTransport::default(), WebcamTransport::Mjpeg);
assert_eq!(WebcamTransport::Hevc.as_id(), "hevc"); assert_eq!(WebcamTransport::Hevc.as_id(), "hevc");
assert_eq!(WebcamTransport::Mjpeg.as_id(), "mjpeg"); assert_eq!(WebcamTransport::Mjpeg.as_id(), "mjpeg");
assert_eq!(WebcamTransport::Hevc.env_value(), "hevc"); assert_eq!(WebcamTransport::Hevc.env_value(), "hevc");
@ -17,6 +17,10 @@ fn routing_and_view_env_values_are_stable() {
WebcamTransport::from_id("h265"), WebcamTransport::from_id("h265"),
Some(WebcamTransport::Hevc) Some(WebcamTransport::Hevc)
); );
assert_eq!(
WebcamTransport::from_server_codec("mjpeg"),
Some(WebcamTransport::Mjpeg)
);
assert_eq!( assert_eq!(
WebcamTransport::from_id("jpeg"), WebcamTransport::from_id("jpeg"),
Some(WebcamTransport::Mjpeg) Some(WebcamTransport::Mjpeg)

View File

@ -411,18 +411,18 @@ fn webcam_transport_combo_tracks_selected_upstream_codec() {
.webcam_transport_combo .webcam_transport_combo
.active_id() .active_id()
.as_deref(), .as_deref(),
Some("hevc") Some("mjpeg")
); );
assert!(view.device_stage.webcam_transport_combo.is_sensitive()); assert!(view.device_stage.webcam_transport_combo.is_sensitive());
state.select_webcam_transport(WebcamTransport::Mjpeg); state.select_webcam_transport(WebcamTransport::Hevc);
refresh_launcher_ui(&view.widgets, &state, false); refresh_launcher_ui(&view.widgets, &state, false);
assert_eq!( assert_eq!(
view.device_stage view.device_stage
.webcam_transport_combo .webcam_transport_combo
.active_id() .active_id()
.as_deref(), .as_deref(),
Some("mjpeg") Some("hevc")
); );
assert!(view.device_stage.webcam_transport_combo.is_sensitive()); assert!(view.device_stage.webcam_transport_combo.is_sensitive());
@ -682,7 +682,7 @@ fn uvc_chip_degrades_when_live_camera_frames_are_not_flowing() {
assert_eq!( assert_eq!(
recovery_uvc_health(&state, false, None), recovery_uvc_health(&state, false, None),
(StatusLightState::Live, "HEVC".to_string()) (StatusLightState::Live, "MJPEG".to_string())
); );
assert_eq!( assert_eq!(
recovery_uvc_health(&state, true, None), recovery_uvc_health(&state, true, None),
@ -700,7 +700,7 @@ fn uvc_chip_degrades_when_live_camera_frames_are_not_flowing() {
}; };
assert_eq!( assert_eq!(
recovery_uvc_health(&state, true, Some(&healthy)), recovery_uvc_health(&state, true, Some(&healthy)),
(StatusLightState::Live, "HEVC".to_string()) (StatusLightState::Live, "MJPEG".to_string())
); );
state.set_server_media_caps(None, None, None, None); state.set_server_media_caps(None, None, None, None);
@ -710,7 +710,7 @@ fn uvc_chip_degrades_when_live_camera_frames_are_not_flowing() {
); );
assert_eq!( assert_eq!(
recovery_uvc_health(&state, true, Some(&healthy)), recovery_uvc_health(&state, true, Some(&healthy)),
(StatusLightState::Live, "HEVC".to_string()) (StatusLightState::Live, "MJPEG".to_string())
); );
state.select_webcam_transport(WebcamTransport::Mjpeg); state.select_webcam_transport(WebcamTransport::Mjpeg);

View File

@ -227,11 +227,11 @@
for transport in [WebcamTransport::Hevc, WebcamTransport::Mjpeg] { for transport in [WebcamTransport::Hevc, WebcamTransport::Mjpeg] {
webcam_transport_combo.append(Some(transport.as_id()), transport.label()); 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_active_id(Some(state.effective_webcam_transport().as_id()));
webcam_transport_combo.set_sensitive(true); webcam_transport_combo.set_sensitive(true);
webcam_transport_combo.set_size_request(98, -1); webcam_transport_combo.set_size_request(98, -1);
webcam_transport_combo.set_tooltip_text(Some( webcam_transport_combo.set_tooltip_text(Some(
"Upstream webcam transport for the next relay connection. HEVC is the low-latency default; MJPEG is the calibrated fallback.", "Upstream webcam transport for the next relay connection. MJPEG is the safe calibrated default; HEVC is used only when the server advertises it.",
)); ));
let upstream_audio_transport_combo = gtk::ComboBoxText::new(); let upstream_audio_transport_combo = gtk::ComboBoxText::new();

View File

@ -190,7 +190,7 @@ fn recovery_uvc_health(
if !state.server_available { if !state.server_available {
return (StatusLightState::Idle, "Offline".to_string()); return (StatusLightState::Idle, "Offline".to_string());
} }
let codec = state.webcam_transport.label().to_string(); let codec = state.effective_webcam_transport().label().to_string();
if state.server_camera == Some(false) { if state.server_camera == Some(false) {
return (StatusLightState::Warning, "Missing".to_string()); return (StatusLightState::Warning, "Missing".to_string());
} }

View File

@ -118,7 +118,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
widgets.summary.uvc_value.set_text(&uvc_value); widgets.summary.uvc_value.set_text(&uvc_value);
widgets.summary.uvc_value.set_tooltip_text(Some(&format!( widgets.summary.uvc_value.set_tooltip_text(Some(&format!(
"Upstream webcam transport: {}. Server calibration is profile-specific.", "Upstream webcam transport: {}. Server calibration is profile-specific.",
state.webcam_transport.label() state.effective_webcam_transport().label()
))); )));
let power_detail = if state.server_available { let power_detail = if state.server_available {
@ -253,11 +253,11 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
.speaker_test_button .speaker_test_button
.set_sensitive(!relay_live && state.channels.audio); .set_sensitive(!relay_live && state.channels.audio);
if widgets.webcam_transport_combo.active_id().as_deref() if widgets.webcam_transport_combo.active_id().as_deref()
!= Some(state.webcam_transport.as_id()) != Some(state.effective_webcam_transport().as_id())
{ {
widgets widgets
.webcam_transport_combo .webcam_transport_combo
.set_active_id(Some(state.webcam_transport.as_id())); .set_active_id(Some(state.effective_webcam_transport().as_id()));
} }
widgets widgets
.webcam_transport_combo .webcam_transport_combo
@ -267,7 +267,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
.set_tooltip_text(Some(if relay_live { .set_tooltip_text(Some(if relay_live {
"Reconnect before changing the upstream webcam transport; the server decoder is calibrated per ingress codec." "Reconnect before changing the upstream webcam transport; the server decoder is calibrated per ingress codec."
} else { } else {
"Choose HEVC for low-latency upstream video or MJPEG as the calibrated fallback for the next relay launch." "Use the server-advertised upstream webcam transport for the next relay launch; MJPEG is the safe calibrated default."
})); }));
if widgets if widgets
.upstream_audio_transport_combo .upstream_audio_transport_combo

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lesavka_common" name = "lesavka_common"
version = "0.22.20" version = "0.22.21"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"

View File

@ -10,7 +10,7 @@ bench = false
[package] [package]
name = "lesavka_server" name = "lesavka_server"
version = "0.22.20" version = "0.22.21"
edition = "2024" edition = "2024"
autobins = false autobins = false

View File

@ -103,6 +103,14 @@ fn uvc_appsrc_leaky_type() -> String {
.unwrap_or_else(|| "downstream".to_string()) .unwrap_or_else(|| "downstream".to_string())
} }
fn looks_like_mjpeg_frame(data: &[u8]) -> bool {
data.len() >= 4 && data.starts_with(&[0xff, 0xd8, 0xff])
}
fn looks_like_annex_b_hevc(data: &[u8]) -> bool {
data.starts_with(&[0, 0, 0, 1]) || data.starts_with(&[0, 0, 1])
}
fn uvc_hevc_freshness_queue_buffers() -> u32 { fn uvc_hevc_freshness_queue_buffers() -> u32 {
positive_u64_env("LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS", 2) positive_u64_env("LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS", 2)
.min(4) .min(4)
@ -579,6 +587,15 @@ impl WebcamSink {
if let Some(path) = &self.mjpeg_spool_path if let Some(path) = &self.mjpeg_spool_path
&& self.decoded_mjpeg_sink.is_none() && self.decoded_mjpeg_sink.is_none()
{ {
if !looks_like_mjpeg_frame(&pkt.data) {
warn!(
target:"lesavka_server::video",
bytes = pkt.data.len(),
hevc_annex_b = looks_like_annex_b_hevc(&pkt.data),
"📸⚠️ dropping non-MJPEG packet before UVC spool; client/server camera codec mismatch would black-screen the browser webcam"
);
return;
}
let timing = MjpegSpoolTiming::mjpeg_passthrough(pkt.pts); let timing = MjpegSpoolTiming::mjpeg_passthrough(pkt.pts);
if let Err(err) = spool_mjpeg_frame_with_timing(path, &pkt.data, Some(timing)) { if let Err(err) = spool_mjpeg_frame_with_timing(path, &pkt.data, Some(timing)) {
warn!(target:"lesavka_server::video", %err, "📸⚠️ failed to spool MJPEG frame for UVC helper"); warn!(target:"lesavka_server::video", %err, "📸⚠️ failed to spool MJPEG frame for UVC helper");
@ -668,6 +685,25 @@ impl Drop for WebcamSink {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#[test]
fn mjpeg_spool_byte_guard_accepts_jpeg_and_identifies_hevc_annex_b() {
assert!(super::looks_like_mjpeg_frame(&[
0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10
]));
assert!(!super::looks_like_mjpeg_frame(&[
0x00, 0x00, 0x00, 0x01, 0x46, 0x01
]));
assert!(super::looks_like_annex_b_hevc(&[
0x00, 0x00, 0x00, 0x01, 0x46, 0x01
]));
assert!(super::looks_like_annex_b_hevc(&[
0x00, 0x00, 0x01, 0x26
]));
assert!(!super::looks_like_annex_b_hevc(&[
0xff, 0xd8, 0xff, 0xdb
]));
}
#[test] #[test]
fn uvc_session_clock_alignment_defaults_on_and_accepts_disable_overrides() { fn uvc_session_clock_alignment_defaults_on_and_accepts_disable_overrides() {
temp_env::with_var_unset("LESAVKA_UVC_SESSION_CLOCK_ALIGN", || { temp_env::with_var_unset("LESAVKA_UVC_SESSION_CLOCK_ALIGN", || {

View File

@ -70,7 +70,7 @@ fn report_builder_populates_schema_from_live_state_and_recent_samples() {
for marker in [ for marker in [
"crate::VERSION.to_string()", "crate::VERSION.to_string()",
"state.server_version.clone()", "state.server_version.clone()",
"state.webcam_transport.label().to_string()", "state.effective_webcam_transport().label().to_string()",
"state.upstream_sync.live_lag_ms", "state.upstream_sync.live_lag_ms",
"state.upstream_sync.planner_skew_ms", "state.upstream_sync.planner_skew_ms",
"state.upstream_sync.stale_audio_drops", "state.upstream_sync.stale_audio_drops",

View File

@ -37,13 +37,13 @@ const STAGE_DEVICE_BINDINGS_SRC: &str = include_str!(concat!(
fn webcam_transport_selector_exposes_real_hevc_and_mjpeg_choices() { fn webcam_transport_selector_exposes_real_hevc_and_mjpeg_choices() {
for marker in [ for marker in [
"pub enum WebcamTransport", "pub enum WebcamTransport",
"#[default]\n Hevc", "#[default]\n Mjpeg",
"Self::Hevc => \"hevc\"", "Self::Hevc => \"hevc\"",
"Self::Mjpeg => \"mjpeg\"", "Self::Mjpeg => \"mjpeg\"",
"Self::Hevc => \"HEVC\"", "Self::Hevc => \"HEVC\"",
"Self::Mjpeg => \"MJPEG\"", "Self::Mjpeg => \"MJPEG\"",
"LESAVKA_CAM_CODEC", "LESAVKA_CAM_CODEC",
"state.webcam_transport.env_value().to_string()", "state.effective_webcam_transport().env_value().to_string()",
] { ] {
assert!( assert!(
SELECTION_MODELS_SRC.contains(marker) || LAUNCHER_MOD_SRC.contains(marker), SELECTION_MODELS_SRC.contains(marker) || LAUNCHER_MOD_SRC.contains(marker),
@ -54,9 +54,9 @@ fn webcam_transport_selector_exposes_real_hevc_and_mjpeg_choices() {
for marker in [ for marker in [
"for transport in [WebcamTransport::Hevc, WebcamTransport::Mjpeg]", "for transport in [WebcamTransport::Hevc, WebcamTransport::Mjpeg]",
"webcam_transport_combo.append(Some(transport.as_id()), transport.label());", "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_active_id(Some(state.effective_webcam_transport().as_id()));",
"webcam_transport_combo.set_sensitive(true);", "webcam_transport_combo.set_sensitive(true);",
"HEVC is the low-latency default; MJPEG is the calibrated fallback", "MJPEG is the safe calibrated default; HEVC is used only when the server advertises it",
] { ] {
assert!( assert!(
BUILD_DEVICE_CONTROLS_SRC.contains(marker), BUILD_DEVICE_CONTROLS_SRC.contains(marker),
@ -82,7 +82,7 @@ fn webcam_transport_changes_are_staged_when_relay_is_live() {
for marker in [ for marker in [
".webcam_transport_combo\n .set_sensitive(!relay_live && state.channels.camera);", ".webcam_transport_combo\n .set_sensitive(!relay_live && state.channels.camera);",
"Reconnect before changing the upstream webcam transport; the server decoder is calibrated per ingress codec.", "Reconnect before changing the upstream webcam transport; the server decoder is calibrated per ingress codec.",
"Choose HEVC for low-latency upstream video or MJPEG as the calibrated fallback for the next relay launch.", "Use the server-advertised upstream webcam transport for the next relay launch; MJPEG is the safe calibrated default.",
] { ] {
assert!( assert!(
STATUS_REFRESH_SRC.contains(marker), STATUS_REFRESH_SRC.contains(marker),
@ -94,7 +94,7 @@ fn webcam_transport_changes_are_staged_when_relay_is_live() {
#[test] #[test]
fn uvc_chip_reports_selected_transport_not_stale_server_codec() { fn uvc_chip_reports_selected_transport_not_stale_server_codec() {
for marker in [ for marker in [
"let codec = state.webcam_transport.label().to_string();", "let codec = state.effective_webcam_transport().label().to_string();",
"if !relay_live {\n return (StatusLightState::Live, codec);", "if !relay_live {\n return (StatusLightState::Live, codec);",
"if matches!(health, StatusLightState::Live) {\n (health, codec)", "if matches!(health, StatusLightState::Live) {\n (health, codec)",
"state.select_webcam_transport(WebcamTransport::Mjpeg);", "state.select_webcam_transport(WebcamTransport::Mjpeg);",