From 692c3a65456119e56ededd3ba7f07afce665276f Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Wed, 13 May 2026 19:12:11 -0300 Subject: [PATCH] media: reset bundled upstream sessions --- Cargo.lock | 6 +- client/Cargo.toml | 2 +- client/src/input/inputs/routing_state.rs | 4 +- .../src/launcher/ui/stage_device_bindings.rs | 56 +++++++++++++------ client/src/launcher/ui_components.rs | 5 +- .../launcher/ui_components/assemble_view.rs | 2 + client/src/launcher/ui_components/types.rs | 2 + .../src/launcher/ui_runtime/status_refresh.rs | 4 ++ common/Cargo.toml | 2 +- server/Cargo.toml | 2 +- .../relay_service/output_delay_probe_rpc.rs | 5 +- .../main/relay_service/upstream_media_rpc.rs | 5 +- .../relay_trait_impl.rs | 5 +- server/src/upstream_media_runtime.rs | 6 ++ .../playout_planning_methods.rs | 30 ++++++++++ .../stream_lifecycle_methods.rs | 5 ++ .../src/upstream_media_runtime/tests/mod.rs | 52 +++++++++++++++++ .../server_upstream_media_bundle_contract.rs | 11 ++++ .../inputs/client_inputs_routing_contract.rs | 24 ++++++++ .../client_codec_transport_ui_contract.rs | 6 +- .../client_launcher_runtime_contract.rs | 12 ++++ 21 files changed, 213 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5397366..0af3611 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.22.26" +version = "0.22.27" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.22.26" +version = "0.22.27" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.22.26" +version = "0.22.27" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index f328b5e..a4f1ad1 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.22.26" +version = "0.22.27" edition = "2024" [dependencies] diff --git a/client/src/input/inputs/routing_state.rs b/client/src/input/inputs/routing_state.rs index fe1f5a2..65310dc 100644 --- a/client/src/input/inputs/routing_state.rs +++ b/client/src/input/inputs/routing_state.rs @@ -64,6 +64,8 @@ impl InputAggregator { self.pending_release_started_at = Some(Instant::now()); self.last_keyboard_report = [0; 8]; self.capture_pending_keys(); + #[cfg(not(coverage))] + self.publish_routing_state_if_changed(); } fn finish_local_release(&mut self, focus_launcher: bool) { @@ -271,7 +273,7 @@ impl InputAggregator { #[cfg(not(coverage))] fn publish_routing_state_if_changed(&mut self) { - let remote_capture = !self.released; + let remote_capture = self.remote_capture_active(); if self.published_remote_capture == Some(remote_capture) { return; } diff --git a/client/src/launcher/ui/stage_device_bindings.rs b/client/src/launcher/ui/stage_device_bindings.rs index 6a49b21..75991fd 100644 --- a/client/src/launcher/ui/stage_device_bindings.rs +++ b/client/src/launcher/ui/stage_device_bindings.rs @@ -58,27 +58,38 @@ let transport_combo = widgets.webcam_transport_combo.clone(); let transport_combo_read = transport_combo.clone(); transport_combo.connect_changed(move |_| { + if widgets.webcam_transport_syncing.get() { + return; + } 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 Ok(mut state_mut) = state.try_borrow_mut() else { + return; + }; + state_mut.select_webcam_transport(selected); + drop(state_mut); let relay_live = child_proc.borrow().is_some(); if relay_live { - apply_live_media_device_change( - &state.borrow(), - &widgets, - &child_proc, - "Webcam transport", - ); + if let Ok(state_ref) = state.try_borrow() { + apply_live_media_device_change( + &state_ref, + &widgets, + &child_proc, + "Webcam transport", + ); + } } 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); + if let Ok(state_ref) = state.try_borrow() { + refresh_launcher_ui(&widgets, &state_ref, relay_live); + } }); } @@ -89,29 +100,38 @@ let audio_combo = widgets.upstream_audio_transport_combo.clone(); let audio_combo_read = audio_combo.clone(); audio_combo.connect_changed(move |_| { + if widgets.upstream_audio_transport_syncing.get() { + return; + } let selected = audio_combo_read .active_id() .as_deref() .and_then(UpstreamAudioTransport::from_id) .unwrap_or_default(); - state - .borrow_mut() - .select_upstream_audio_transport(selected); + let Ok(mut state_mut) = state.try_borrow_mut() else { + return; + }; + state_mut.select_upstream_audio_transport(selected); + drop(state_mut); let relay_live = child_proc.borrow().is_some(); if relay_live { - apply_live_media_device_change( - &state.borrow(), - &widgets, - &child_proc, - "Microphone transport", - ); + if let Ok(state_ref) = state.try_borrow() { + apply_live_media_device_change( + &state_ref, + &widgets, + &child_proc, + "Microphone transport", + ); + } } else { widgets.status_label.set_text(&format!( "Upstream microphone transport set to {} for the next relay launch.", selected.label() )); } - refresh_launcher_ui(&widgets, &state.borrow(), relay_live); + if let Ok(state_ref) = state.try_borrow() { + refresh_launcher_ui(&widgets, &state_ref, relay_live); + } }); } diff --git a/client/src/launcher/ui_components.rs b/client/src/launcher/ui_components.rs index c93b471..f6e0a2c 100644 --- a/client/src/launcher/ui_components.rs +++ b/client/src/launcher/ui_components.rs @@ -1,4 +1,7 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{ + cell::{Cell, RefCell}, + rc::Rc, +}; use evdev::Device; use gtk::{glib, pango, prelude::*}; diff --git a/client/src/launcher/ui_components/assemble_view.rs b/client/src/launcher/ui_components/assemble_view.rs index 821c462..a340008 100644 --- a/client/src/launcher/ui_components/assemble_view.rs +++ b/client/src/launcher/ui_components/assemble_view.rs @@ -162,7 +162,9 @@ camera_test_button: camera_test_button.clone(), camera_preview_stack: camera_preview_stack.clone(), webcam_transport_combo: webcam_transport_combo.clone(), + webcam_transport_syncing: Rc::new(Cell::new(false)), upstream_audio_transport_combo: upstream_audio_transport_combo.clone(), + upstream_audio_transport_syncing: Rc::new(Cell::new(false)), camera_mirror_button: camera_mirror_button.clone(), camera_mirror_revealer: camera_mirror_revealer.clone(), microphone_test_button: microphone_test_button.clone(), diff --git a/client/src/launcher/ui_components/types.rs b/client/src/launcher/ui_components/types.rs index 1aeccd8..81c9123 100644 --- a/client/src/launcher/ui_components/types.rs +++ b/client/src/launcher/ui_components/types.rs @@ -169,7 +169,9 @@ pub struct LauncherWidgets { pub camera_test_button: gtk::Button, pub camera_preview_stack: gtk::Stack, pub webcam_transport_combo: gtk::ComboBoxText, + pub webcam_transport_syncing: Rc>, pub upstream_audio_transport_combo: gtk::ComboBoxText, + pub upstream_audio_transport_syncing: Rc>, pub camera_mirror_button: gtk::ToggleButton, pub camera_mirror_revealer: gtk::Revealer, pub microphone_test_button: gtk::Button, diff --git a/client/src/launcher/ui_runtime/status_refresh.rs b/client/src/launcher/ui_runtime/status_refresh.rs index 415c3ae..a7260e7 100644 --- a/client/src/launcher/ui_runtime/status_refresh.rs +++ b/client/src/launcher/ui_runtime/status_refresh.rs @@ -275,9 +275,11 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi if widgets.webcam_transport_combo.active_id().as_deref() != Some(state.effective_webcam_transport().as_id()) { + widgets.webcam_transport_syncing.set(true); widgets .webcam_transport_combo .set_active_id(Some(state.effective_webcam_transport().as_id())); + widgets.webcam_transport_syncing.set(false); } widgets .webcam_transport_combo @@ -295,9 +297,11 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi .as_deref() != Some(state.upstream_audio_transport.as_id()) { + widgets.upstream_audio_transport_syncing.set(true); widgets .upstream_audio_transport_combo .set_active_id(Some(state.upstream_audio_transport.as_id())); + widgets.upstream_audio_transport_syncing.set(false); } widgets .upstream_audio_transport_combo diff --git a/common/Cargo.toml b/common/Cargo.toml index 5580039..c2b448e 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.22.26" +version = "0.22.27" edition = "2024" build = "build.rs" diff --git a/server/Cargo.toml b/server/Cargo.toml index ac73087..3dbd256 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.22.26" +version = "0.22.27" edition = "2024" autobins = false diff --git a/server/src/main/relay_service/output_delay_probe_rpc.rs b/server/src/main/relay_service/output_delay_probe_rpc.rs index a4b14cd..7d68c00 100644 --- a/server/src/main/relay_service/output_delay_probe_rpc.rs +++ b/server/src/main/relay_service/output_delay_probe_rpc.rs @@ -7,8 +7,9 @@ impl Handler { let rpc_id = runtime_support::next_stream_id(); let request = req.into_inner(); let camera_cfg = camera::current_camera_config(); - let microphone_lease = self.upstream_media_rt.activate_microphone(); - let camera_lease = self.upstream_media_rt.activate_camera(); + let bundled_leases = self.upstream_media_rt.activate_bundled_session(); + let microphone_lease = bundled_leases.microphone; + let camera_lease = bundled_leases.camera; info!( rpc_id, session_id = camera_lease.session_id, diff --git a/server/src/main/relay_service/upstream_media_rpc.rs b/server/src/main/relay_service/upstream_media_rpc.rs index b268c7e..fdbc11e 100644 --- a/server/src/main/relay_service/upstream_media_rpc.rs +++ b/server/src/main/relay_service/upstream_media_rpc.rs @@ -6,8 +6,9 @@ impl Handler { ) -> Result>>, Status> { let rpc_id = runtime_support::next_stream_id(); let camera_cfg = camera::current_camera_config(); - let microphone_lease = self.upstream_media_rt.activate_microphone(); - let camera_lease = self.upstream_media_rt.activate_camera(); + let bundled_leases = self.upstream_media_rt.activate_bundled_session(); + let microphone_lease = bundled_leases.microphone; + let camera_lease = bundled_leases.camera; info!( rpc_id, session_id = camera_lease.session_id, diff --git a/server/src/main/relay_service_coverage/relay_trait_impl.rs b/server/src/main/relay_service_coverage/relay_trait_impl.rs index d0ef79f..f940b75 100644 --- a/server/src/main/relay_service_coverage/relay_trait_impl.rs +++ b/server/src/main/relay_service_coverage/relay_trait_impl.rs @@ -149,8 +149,9 @@ impl Relay for Handler { &self, req: Request>, ) -> Result, Status> { - let microphone_lease = self.upstream_media_rt.activate_microphone(); - let camera_lease = self.upstream_media_rt.activate_camera(); + let bundled_leases = self.upstream_media_rt.activate_bundled_session(); + let microphone_lease = bundled_leases.microphone; + let camera_lease = bundled_leases.camera; let (tx, rx) = tokio::sync::mpsc::channel(1); let upstream_media_rt = self.upstream_media_rt.clone(); diff --git a/server/src/upstream_media_runtime.rs b/server/src/upstream_media_runtime.rs index f971980..69c5010 100644 --- a/server/src/upstream_media_runtime.rs +++ b/server/src/upstream_media_runtime.rs @@ -31,6 +31,12 @@ pub struct UpstreamStreamLease { pub generation: u64, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct UpstreamBundledLeases { + pub camera: UpstreamStreamLease, + pub microphone: UpstreamStreamLease, +} + #[derive(Clone, Copy, Debug)] pub struct PlannedUpstreamPacket { pub local_pts_us: u64, diff --git a/server/src/upstream_media_runtime/playout_planning_methods.rs b/server/src/upstream_media_runtime/playout_planning_methods.rs index c34e2ef..80c0797 100644 --- a/server/src/upstream_media_runtime/playout_planning_methods.rs +++ b/server/src/upstream_media_runtime/playout_planning_methods.rs @@ -33,6 +33,36 @@ impl UpstreamMediaRuntime { } } + /// Starts a bundled camera+microphone generation with a fresh shared playout epoch. + fn activate_bundled(&self) -> UpstreamBundledLeases { + let camera_generation = self.next_camera_generation.fetch_add(1, Ordering::SeqCst) + 1; + let microphone_generation = + self.next_microphone_generation + .fetch_add(1, Ordering::SeqCst) + + 1; + let mut state = self + .state + .lock() + .expect("upstream media state mutex poisoned"); + state.session_id = self.next_session_id.fetch_add(1, Ordering::SeqCst) + 1; + reset_session_state(&mut state); + state.session_started_at = Some(Instant::now()); + state.phase = UpstreamSyncPhase::Acquiring; + state.last_reason = "v2 bundled upstream session acquiring media".to_string(); + state.active_camera_generation = Some(camera_generation); + state.active_microphone_generation = Some(microphone_generation); + UpstreamBundledLeases { + camera: UpstreamStreamLease { + session_id: state.session_id, + generation: camera_generation, + }, + microphone: UpstreamStreamLease { + session_id: state.session_id, + generation: microphone_generation, + }, + } + } + /// Keeps `is_active` explicit because it sits on server upstream media scheduling, where timing choices directly affect lip sync. /// Inputs are the typed parameters; output is the return value or side effect. fn is_active(&self, kind: UpstreamMediaKind, generation: u64) -> bool { diff --git a/server/src/upstream_media_runtime/stream_lifecycle_methods.rs b/server/src/upstream_media_runtime/stream_lifecycle_methods.rs index 83b1f9d..aae418f 100644 --- a/server/src/upstream_media_runtime/stream_lifecycle_methods.rs +++ b/server/src/upstream_media_runtime/stream_lifecycle_methods.rs @@ -41,6 +41,11 @@ pub fn new() -> Self { self.activate(UpstreamMediaKind::Microphone) } + #[must_use] + pub fn activate_bundled_session(&self) -> UpstreamBundledLeases { + self.activate_bundled() + } + pub async fn reserve_microphone_sink(&self, generation: u64) -> Option { let permit = self .microphone_sink_gate diff --git a/server/src/upstream_media_runtime/tests/mod.rs b/server/src/upstream_media_runtime/tests/mod.rs index e183579..1ca8be7 100644 --- a/server/src/upstream_media_runtime/tests/mod.rs +++ b/server/src/upstream_media_runtime/tests/mod.rs @@ -225,6 +225,58 @@ fn runtime_public_mapping_helpers_cover_legacy_and_bundled_paths() { }); } +#[test] +/// Keeps `bundled_session_activation_resets_stale_playout_epoch` explicit because reconnecting a v2 webcam+mic stream must not inherit stale clocks from a superseded stream. +/// Inputs are two bundled activations with an old presented frame; output proves the new stream starts at local PTS zero and retires the old generations. +fn bundled_session_activation_resets_stale_playout_epoch() { + with_clean_offset_env(|| { + let runtime = UpstreamMediaRuntime::new(); + let first = runtime.activate_bundled_session(); + assert_eq!(first.camera.session_id, first.microphone.session_id); + + let first_epoch = tokio::time::Instant::now() + std::time::Duration::from_millis(10); + let first_decision = runtime.plan_bundled_pts( + UpstreamMediaKind::Camera, + 500_000, + 33_333, + 100_000, + first_epoch, + ); + let first_plan = match first_decision { + UpstreamPlanDecision::Play(plan) => plan, + other => panic!("expected first bundled play decision, got {other:?}"), + }; + assert_eq!(first_plan.local_pts_us, 400_000); + runtime.mark_video_presented(first_plan.local_pts_us, first_plan.due_at); + assert_eq!( + runtime.snapshot().last_video_presented_pts_us, + Some(400_000) + ); + + let second = runtime.activate_bundled_session(); + assert_ne!(first.camera.session_id, second.camera.session_id); + assert_eq!(second.camera.session_id, second.microphone.session_id); + assert!(!runtime.is_camera_active(first.camera.generation)); + assert!(!runtime.is_microphone_active(first.microphone.generation)); + assert!(runtime.is_camera_active(second.camera.generation)); + assert!(runtime.is_microphone_active(second.microphone.generation)); + assert_eq!(runtime.snapshot().last_video_presented_pts_us, None); + + let second_epoch = tokio::time::Instant::now() + std::time::Duration::from_millis(10); + let second_decision = runtime.plan_bundled_pts( + UpstreamMediaKind::Camera, + 1_000, + 33_333, + 1_000, + second_epoch, + ); + match second_decision { + UpstreamPlanDecision::Play(plan) => assert_eq!(plan.local_pts_us, 0), + other => panic!("expected fresh bundled play decision, got {other:?}"), + } + }); +} + #[test] /// Keeps `runtime_soft_microphone_recovery_cycles_only_the_microphone_generation` explicit because a failed UAC handoff should not disturb active camera playout. /// Inputs are an active camera lease and a soft microphone recovery request; output keeps camera active while cycling microphone state. diff --git a/tests/api/server/upstream_media_runtime/server_upstream_media_bundle_contract.rs b/tests/api/server/upstream_media_runtime/server_upstream_media_bundle_contract.rs index a3972ef..e6bf887 100644 --- a/tests/api/server/upstream_media_runtime/server_upstream_media_bundle_contract.rs +++ b/tests/api/server/upstream_media_runtime/server_upstream_media_bundle_contract.rs @@ -226,6 +226,14 @@ mod server_upstream_media_bundle_normal_mode { env!("CARGO_MANIFEST_DIR"), "/server/src/main/relay_stream_lifecycle.rs" )); + const UPSTREAM_RUNTIME: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/server/src/upstream_media_runtime.rs" + )); + const UPSTREAM_RUNTIME_LIFECYCLE: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/server/src/upstream_media_runtime/stream_lifecycle_methods.rs" + )); #[test] fn bundled_rpc_preserves_one_media_unit_at_server_ingress() { @@ -233,5 +241,8 @@ mod server_upstream_media_bundle_normal_mode { assert!(RELAY_RPC.contains("UpstreamMediaBundle")); assert!(RELAY_LIFECYCLE.contains("client_capture_pts_us")); assert!(RELAY_RPC.contains("record_client_timing")); + assert!(RELAY_RPC.contains("activate_bundled_session()")); + assert!(UPSTREAM_RUNTIME.contains("pub struct UpstreamBundledLeases")); + assert!(UPSTREAM_RUNTIME_LIFECYCLE.contains("pub fn activate_bundled_session")); } } diff --git a/tests/contract/client/input/inputs/client_inputs_routing_contract.rs b/tests/contract/client/input/inputs/client_inputs_routing_contract.rs index 8c7dfdd..be5bf4c 100644 --- a/tests/contract/client/input/inputs/client_inputs_routing_contract.rs +++ b/tests/contract/client/input/inputs/client_inputs_routing_contract.rs @@ -381,6 +381,30 @@ mod inputs_contract { ); } + #[test] + fn begin_local_release_publishes_local_before_ungrab_finishes() { + let dir = tempfile::tempdir().expect("tempdir"); + let state_path = dir.path().join("routing-state"); + let mut agg = new_aggregator(); + agg.routing_state_path = Some(state_path.clone()); + + agg.publish_routing_state_if_changed(); + assert_eq!( + std::fs::read_to_string(&state_path).expect("initial routing state"), + "remote\n" + ); + + agg.begin_local_release(); + + assert!(agg.pending_release); + assert!(!agg.remote_capture_active()); + assert_eq!( + std::fs::read_to_string(&state_path).expect("local routing state"), + "local\n", + "launcher polling must see local as soon as remote sends are disabled, not only after final ungrab" + ); + } + #[test] fn local_release_timeout_helpers_are_stable() { let mut agg = new_aggregator(); diff --git a/tests/ui/client/launcher/client_codec_transport_ui_contract.rs b/tests/ui/client/launcher/client_codec_transport_ui_contract.rs index b251bfe..1c10881 100644 --- a/tests/ui/client/launcher/client_codec_transport_ui_contract.rs +++ b/tests/ui/client/launcher/client_codec_transport_ui_contract.rs @@ -68,7 +68,9 @@ fn webcam_transport_selector_exposes_real_hevc_and_mjpeg_choices() { #[test] fn webcam_transport_changes_are_staged_when_relay_is_live() { for marker in [ - "state.borrow_mut().select_webcam_transport(selected);", + "if widgets.webcam_transport_syncing.get()", + "state.try_borrow_mut()", + "state_mut.select_webcam_transport(selected);", "child_proc.borrow().is_some()", "apply_live_media_device_change(", "\"Webcam transport\"", @@ -81,6 +83,8 @@ fn webcam_transport_changes_are_staged_when_relay_is_live() { } for marker in [ + "widgets.webcam_transport_syncing.set(true);", + "widgets.webcam_transport_syncing.set(false);", ".webcam_transport_combo\n .set_sensitive(state.channels.camera);", "Changing upstream webcam transport restarts the live camera path; the picture may pause briefly.", "Use the server-advertised upstream webcam transport for the next relay launch; MJPEG is the safe calibrated default.", diff --git a/tests/ui/client/launcher/client_launcher_runtime_contract.rs b/tests/ui/client/launcher/client_launcher_runtime_contract.rs index a6f8494..476ceb4 100644 --- a/tests/ui/client/launcher/client_launcher_runtime_contract.rs +++ b/tests/ui/client/launcher/client_launcher_runtime_contract.rs @@ -135,6 +135,14 @@ const UI_COMPONENTS_SRC: &str = concat!( env!("CARGO_MANIFEST_DIR"), "/client/src/launcher/ui_components.rs" )), + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/client/src/launcher/ui_components/types.rs" + )), + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/client/src/launcher/ui_components/assemble_view.rs" + )), include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/client/src/launcher/ui_components/build_shell.rs" @@ -264,6 +272,10 @@ fn launcher_webcam_quality_selection_reaches_preview_and_relay_env() { assert!(UI_SRC.contains("let camera_quality_syncing = Rc::new(Cell::new(false));")); assert!(UI_SRC.contains("camera_quality_syncing.set(true);")); assert!(UI_SRC.contains("if camera_quality_syncing.get()")); + assert!(UI_COMPONENTS_SRC.contains("webcam_transport_syncing: Rc>")); + assert!(UI_COMPONENTS_SRC.contains("upstream_audio_transport_syncing: Rc>")); + assert!(UI_RUNTIME_SRC.contains("widgets.webcam_transport_syncing.set(true);")); + assert!(UI_SRC.contains("if widgets.webcam_transport_syncing.get()")); assert!(UI_SRC.contains("state.try_borrow_mut()")); assert!(UI_SRC.contains("tests.set_camera_quality")); assert!(DEVICE_TEST_SRC.contains("pub fn set_camera_quality"));