diff --git a/client/Cargo.toml b/client/Cargo.toml index 7115727..01e8c65 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.11.31" +version = "0.11.32" edition = "2024" [dependencies] diff --git a/client/src/input/inputs.rs b/client/src/input/inputs.rs index c8af0b5..e650977 100644 --- a/client/src/input/inputs.rs +++ b/client/src/input/inputs.rs @@ -975,7 +975,7 @@ fn remote_failsafe_timeout_from_env() -> Duration { let millis = std::env::var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS") .ok() .and_then(|raw| raw.parse::().ok()) - .unwrap_or(5_000); + .unwrap_or(60_000); Duration::from_millis(millis) } diff --git a/client/src/launcher/ui_components.rs b/client/src/launcher/ui_components.rs index 7bdf45e..b185a9d 100644 --- a/client/src/launcher/ui_components.rs +++ b/client/src/launcher/ui_components.rs @@ -111,8 +111,8 @@ const LESAVKA_ICON_SEARCH_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/ass const LAUNCHER_DEFAULT_WIDTH: i32 = 1380; const LAUNCHER_DEFAULT_HEIGHT: i32 = 860; const OPERATIONS_RAIL_WIDTH: i32 = 288; -const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 144; -const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 256; +const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 108; +const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 192; pub fn build_launcher_view( app: >k::Application, @@ -211,7 +211,7 @@ pub fn build_launcher_view( build_panel_with_action("Device Staging", Some(device_refresh_button.upcast_ref())); devices_panel.set_hexpand(true); devices_panel.set_vexpand(false); - devices_panel.set_valign(gtk::Align::Start); + devices_panel.set_valign(gtk::Align::Fill); devices_body.set_spacing(8); let control_group = build_subgroup("Control Inputs"); @@ -321,7 +321,8 @@ pub fn build_launcher_view( let (preview_panel, preview_body) = build_panel("Device Testing"); preview_panel.set_hexpand(true); preview_panel.set_vexpand(false); - preview_panel.set_valign(gtk::Align::Start); + preview_panel.set_valign(gtk::Align::Fill); + preview_body.set_vexpand(true); preview_body.set_spacing(6); let camera_preview = gtk::Picture::new(); camera_preview.set_can_shrink(false); @@ -355,6 +356,8 @@ pub fn build_launcher_view( preview_body.append(&webcam_group); let playback_group = build_subgroup("Mic Playback"); + playback_group.set_vexpand(true); + playback_group.set_valign(gtk::Align::Fill); let playback_body = gtk::Box::new(gtk::Orientation::Vertical, 6); let playback_row = gtk::Box::new(gtk::Orientation::Horizontal, 8); playback_row.set_homogeneous(false); diff --git a/client/src/output/audio.rs b/client/src/output/audio.rs index 146e477..17cb4f3 100644 --- a/client/src/output/audio.rs +++ b/client/src/output/audio.rs @@ -18,8 +18,7 @@ pub struct AudioOut { #[derive(Default)] struct AudioTimeline { - first_remote_pts_us: Option, - last_local_pts_us: u64, + last_remote_pts_us: Option, packets: u64, } @@ -39,6 +38,7 @@ impl AudioOut { aacparse ! avdec_aac ! \ audioconvert ! audioresample ! \ audio/x-raw,format=S16LE,channels=2,rate=48000 ! \ + level name=remote_audio_level interval=1000000000 message=true ! \ queue max-size-time=400000000 max-size-bytes=0 max-size-buffers=0 ! {}", sink, ); @@ -49,6 +49,7 @@ impl AudioOut { queue max-size-time=500000000 max-size-bytes=0 max-size-buffers=0 ! \ aacparse ! avdec_aac ! audioconvert ! audioresample ! \ audio/x-raw,format=S16LE,channels=2,rate=48000 ! \ + level name=remote_audio_level interval=1000000000 message=true ! \ queue max-size-time=400000000 max-size-bytes=0 max-size-buffers=0 ! {} \ t. ! queue ! filesink location=/tmp/lesavka-audio.aac", sink, @@ -93,10 +94,15 @@ impl AudioOut { w.error(), w.debug().unwrap_or_default() ), - Element(e) => debug!( - "🔎 gst element message: {}", - e.structure().map(|s| s.to_string()).unwrap_or_default() - ), + Element(e) => { + if let Some(structure) = e.structure() { + if structure.name() == "level" { + info!("🔊 decoded audio level {}", structure); + } else { + debug!("🔎 gst element message: {}", structure); + } + } + } StateChanged(s) if s.current() == gst::State::Playing => { if msg.src().map(|s| s.is::()).unwrap_or(false) { info!("🔊 audio pipeline ▶️ (sink='{}')", sink); @@ -123,28 +129,7 @@ impl AudioOut { } pub fn push(&self, pkt: AudioPacket) { - let mut buf = gst::Buffer::from_slice(pkt.data); - if let Ok(mut timeline) = self.timeline.lock() { - let base = timeline.first_remote_pts_us.get_or_insert(pkt.pts); - let mut local_pts_us = pkt.pts.saturating_sub(*base); - if local_pts_us < timeline.last_local_pts_us { - local_pts_us = timeline.last_local_pts_us.saturating_add(1); - } - timeline.last_local_pts_us = local_pts_us; - timeline.packets = timeline.packets.saturating_add(1); - if timeline.packets <= 8 || timeline.packets % 600 == 0 { - debug!( - packet = timeline.packets, - remote_pts_us = pkt.pts, - local_pts_us, - bytes = buf.size(), - "🔊 audio packet queued" - ); - } - buf.get_mut() - .unwrap() - .set_pts(Some(gst::ClockTime::from_useconds(local_pts_us))); - } + let buf = live_audio_buffer(pkt, &self.timeline); #[cfg(not(coverage))] if let Err(e) = self.src.push_buffer(buf) { warn!("📉 AppSrc push failed: {e:?}"); @@ -157,6 +142,27 @@ impl AudioOut { } } +fn live_audio_buffer(pkt: AudioPacket, timeline: &Mutex) -> gst::Buffer { + let buf = gst::Buffer::from_slice(pkt.data); + if let Ok(mut timeline) = timeline.lock() { + let remote_gap_us = timeline + .last_remote_pts_us + .map(|last| pkt.pts.saturating_sub(last)); + timeline.last_remote_pts_us = Some(pkt.pts); + timeline.packets = timeline.packets.saturating_add(1); + if timeline.packets <= 8 || timeline.packets % 600 == 0 { + debug!( + packet = timeline.packets, + remote_pts_us = pkt.pts, + remote_gap_us, + bytes = buf.size(), + "🔊 audio packet queued for live appsrc timestamping" + ); + } + } + buf +} + impl Drop for AudioOut { fn drop(&mut self) { let _ = self.pipeline.set_state(gst::State::Null); diff --git a/common/Cargo.toml b/common/Cargo.toml index 6cc5044..1d77d5c 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.11.31" +version = "0.11.32" edition = "2024" build = "build.rs" diff --git a/common/src/cli.rs b/common/src/cli.rs index b8f8824..3c2806b 100644 --- a/common/src/cli.rs +++ b/common/src/cli.rs @@ -17,6 +17,6 @@ mod tests { #[test] fn banner_includes_version() { - assert_eq!(banner("0.11.31"), "lesavka-common CLI (v0.11.31)"); + assert_eq!(banner("0.11.32"), "lesavka-common CLI (v0.11.32)"); } } diff --git a/server/Cargo.toml b/server/Cargo.toml index 4a7164f..dc13666 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.11.31" +version = "0.11.32" edition = "2024" autobins = false diff --git a/testing/tests/client_inputs_contract.rs b/testing/tests/client_inputs_contract.rs index dd82be6..6cff788 100644 --- a/testing/tests/client_inputs_contract.rs +++ b/testing/tests/client_inputs_contract.rs @@ -466,7 +466,7 @@ mod inputs_contract { with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", None::<&str>, || { assert_eq!( remote_failsafe_timeout_from_env(), - Duration::from_millis(5_000) + Duration::from_millis(60_000) ); }); with_var("LESAVKA_INPUT_REMOTE_FAILSAFE_MS", Some("0"), || { diff --git a/testing/tests/client_output_audio_include_contract.rs b/testing/tests/client_output_audio_include_contract.rs index f74a952..e10de7d 100644 --- a/testing/tests/client_output_audio_include_contract.rs +++ b/testing/tests/client_output_audio_include_contract.rs @@ -80,7 +80,7 @@ exit 0 sinks, vec![( "alsa_output.usb-DAC_1234-00.analog-stereo".to_string(), - "UNKNOWN".to_string() + "DEFAULT".to_string() )] ); let sink = pick_sink_element().expect("pick sink"); @@ -168,4 +168,21 @@ exit 0 }, ); } + + #[test] + fn live_audio_buffer_leaves_pts_for_appsrc_timestamping() { + let _ = gst::init(); + let timeline = std::sync::Mutex::new(AudioTimeline::default()); + let buffer = live_audio_buffer( + AudioPacket { + id: 0, + pts: 42_666, + data: vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0x1F, 0xFC], + }, + &timeline, + ); + + assert_eq!(buffer.pts(), None); + assert_eq!(timeline.lock().expect("timeline").packets, 1); + } }