diff --git a/client/Cargo.toml b/client/Cargo.toml index d66426a..f78576f 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.11.9" +version = "0.11.10" edition = "2024" [dependencies] diff --git a/client/src/launcher/diagnostics.rs b/client/src/launcher/diagnostics.rs index e5a0bad..e72c3fd 100644 --- a/client/src/launcher/diagnostics.rs +++ b/client/src/launcher/diagnostics.rs @@ -26,6 +26,8 @@ pub struct PerformanceSample { pub left_server_queue_peak: u32, pub left_server_encoder_label: String, pub left_decoder_label: String, + pub left_stream_caps_label: String, + pub left_decoded_caps_label: String, pub right_receive_fps: f32, pub right_present_fps: f32, pub right_server_fps: f32, @@ -39,6 +41,8 @@ pub struct PerformanceSample { pub right_server_queue_peak: u32, pub right_server_encoder_label: String, pub right_decoder_label: String, + pub right_stream_caps_label: String, + pub right_decoded_caps_label: String, pub dropped_frames: u64, pub queue_depth: u32, } @@ -109,6 +113,8 @@ pub struct SnapshotReport { pub left_server_send_gap_peak_ms: f32, pub left_server_queue_peak: u32, pub left_server_encoder_label: String, + pub left_stream_caps_label: String, + pub left_decoded_caps_label: String, pub right_surface: String, pub right_capture_profile: String, pub right_capture_transport: String, @@ -123,6 +129,8 @@ pub struct SnapshotReport { pub right_server_send_gap_peak_ms: f32, pub right_server_queue_peak: u32, pub right_server_encoder_label: String, + pub right_stream_caps_label: String, + pub right_decoded_caps_label: String, pub selected_camera: Option, pub selected_microphone: Option, pub selected_speaker: Option, @@ -223,6 +231,24 @@ impl SnapshotReport { } }) .unwrap_or_else(|| "pending".to_string()), + left_stream_caps_label: latest + .map(|sample| { + if sample.left_stream_caps_label.is_empty() { + "pending".to_string() + } else { + sample.left_stream_caps_label.clone() + } + }) + .unwrap_or_else(|| "pending".to_string()), + left_decoded_caps_label: latest + .map(|sample| { + if sample.left_decoded_caps_label.is_empty() { + "pending".to_string() + } else { + sample.left_decoded_caps_label.clone() + } + }) + .unwrap_or_else(|| "pending".to_string()), right_surface: state.display_surface(1).label().to_string(), right_capture_profile: format!( "{} | {}x{} | {} fps | {} kbit", @@ -277,6 +303,24 @@ impl SnapshotReport { } }) .unwrap_or_else(|| "pending".to_string()), + right_stream_caps_label: latest + .map(|sample| { + if sample.right_stream_caps_label.is_empty() { + "pending".to_string() + } else { + sample.right_stream_caps_label.clone() + } + }) + .unwrap_or_else(|| "pending".to_string()), + right_decoded_caps_label: latest + .map(|sample| { + if sample.right_decoded_caps_label.is_empty() { + "pending".to_string() + } else { + sample.right_decoded_caps_label.clone() + } + }) + .unwrap_or_else(|| "pending".to_string()), selected_camera: state.devices.camera.clone(), selected_microphone: state.devices.microphone.clone(), selected_speaker: state.devices.speaker.clone(), @@ -333,6 +377,8 @@ impl SnapshotReport { self.left_queue_depth, self.left_queue_peak ); + let _ = writeln!(text, " stream caps: {}", self.left_stream_caps_label); + let _ = writeln!(text, " decoded caps: {}", self.left_decoded_caps_label); let _ = writeln!( text, " server: encoder={} cpu={:.1}% gaps={:.0}/{:.0}ms queue-peak={}", @@ -357,6 +403,8 @@ impl SnapshotReport { self.right_queue_depth, self.right_queue_peak ); + let _ = writeln!(text, " stream caps: {}", self.right_stream_caps_label); + let _ = writeln!(text, " decoded caps: {}", self.right_decoded_caps_label); let _ = writeln!( text, " server: encoder={} cpu={:.1}% gaps={:.0}/{:.0}ms queue-peak={}", @@ -657,6 +705,9 @@ mod tests { left_server_queue_peak: n as u32 + 1, left_server_encoder_label: "x264enc".to_string(), left_decoder_label: "decodebin".to_string(), + left_stream_caps_label: "video/x-h264, width=(int)1920, height=(int)1080".to_string(), + left_decoded_caps_label: + "video/x-raw, format=(string)NV12, width=(int)1920, height=(int)1080".to_string(), right_receive_fps: 30.0, right_present_fps: 28.0, right_server_fps: 30.0, @@ -670,6 +721,9 @@ mod tests { right_server_queue_peak: n as u32 + 1, right_server_encoder_label: "source-pass-through".to_string(), right_decoder_label: "decodebin".to_string(), + right_stream_caps_label: "video/x-h264, width=(int)1920, height=(int)1080".to_string(), + right_decoded_caps_label: + "video/x-raw, format=(string)NV12, width=(int)1920, height=(int)1080".to_string(), dropped_frames: n, queue_depth: n as u32, } @@ -730,6 +784,8 @@ mod tests { assert!(report.left_capture_profile.contains("fps")); assert_eq!(report.left_capture_transport, "server re-encode"); assert_eq!(report.left_decoder_label, "decodebin"); + assert!(report.left_stream_caps_label.contains("video/x-h264")); + assert!(report.left_decoded_caps_label.contains("video/x-raw")); } #[test] @@ -758,6 +814,8 @@ mod tests { assert!(text.contains("left eye")); assert!(text.contains("transport:")); assert!(text.contains("live: decoder=")); + assert!(text.contains("stream caps:")); + assert!(text.contains("decoded caps:")); assert!(text.contains("recommendations")); } diff --git a/client/src/launcher/preview.rs b/client/src/launcher/preview.rs index 0337511..30a40b8 100644 --- a/client/src/launcher/preview.rs +++ b/client/src/launcher/preview.rs @@ -5,7 +5,7 @@ use anyhow::{Context, Result}; #[cfg(not(coverage))] use gstreamer as gst; #[cfg(not(coverage))] -use gstreamer::prelude::{Cast, ElementExt, GstBinExt, GstObjectExt}; +use gstreamer::prelude::{Cast, ElementExt, GstBinExt, GstObjectExt, PadExt}; #[cfg(not(coverage))] use gstreamer_app as gst_app; #[cfg(not(coverage))] @@ -90,6 +90,8 @@ pub struct PreviewMetricsSnapshot { pub server_queue_peak: u32, pub server_encoder_label: String, pub decoder_label: String, + pub stream_caps_label: String, + pub decoded_caps_label: String, } #[cfg(not(coverage))] @@ -496,6 +498,8 @@ struct PreviewTelemetry { latest_server_queue_peak: u32, latest_server_encoder_label: String, decoder_label: String, + stream_caps_label: String, + decoded_caps_label: String, } #[cfg(not(coverage))] @@ -597,6 +601,18 @@ impl PreviewTelemetry { } } + fn note_stream_caps(&mut self, caps_label: &str) { + if !caps_label.is_empty() { + self.stream_caps_label = caps_label.to_string(); + } + } + + fn note_decoded_caps(&mut self, caps_label: &str) { + if !caps_label.is_empty() { + self.decoded_caps_label = caps_label.to_string(); + } + } + fn snapshot(&mut self) -> PreviewMetricsSnapshot { self.snapshot_at(Instant::now()) } @@ -640,6 +656,8 @@ impl PreviewTelemetry { server_queue_peak: self.latest_server_queue_peak, server_encoder_label: self.latest_server_encoder_label.clone(), decoder_label: self.decoder_label.clone(), + stream_caps_label: self.stream_caps_label.clone(), + decoded_caps_label: self.decoded_caps_label.clone(), } } @@ -810,6 +828,8 @@ fn run_preview_feed( log_sink: Arc>>>, ) -> Result<()> { let (pipeline, appsrc, appsink, decoder_name) = build_preview_pipeline(profile)?; + let parser = pipeline.by_name("preview_parse"); + let decoder = pipeline.by_name("decoder"); if let Ok(mut slot) = shared.lock() { slot.telemetry.note_decoder(&decoder_name); } @@ -830,6 +850,8 @@ fn run_preview_feed( { let shared = Arc::clone(&shared); let appsink = appsink.clone(); + let parser = parser.clone(); + let decoder = decoder.clone(); let running = Arc::clone(&running); std::thread::spawn(move || { loop { @@ -837,6 +859,12 @@ fn run_preview_feed( break; } if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250)) { + if let Some(parser) = parser.as_ref() { + record_preview_caps(&shared, parser, "src", PreviewCapsKind::Stream); + } + if let Some(decoder) = decoder.as_ref() { + record_preview_caps(&shared, decoder, "src", PreviewCapsKind::Decoded); + } if let Some(frame) = sample_to_frame(&sample) { if let Ok(mut slot) = shared.lock() { slot.push_frame(frame); @@ -1225,7 +1253,7 @@ fn build_preview_pipeline( let desc = format!( "appsrc name=src is-live=true format=time do-timestamp=true block=false ! \ queue max-size-buffers=6 max-size-time=0 max-size-bytes=0 leaky=downstream ! \ - h264parse disable-passthrough=true ! {decoder_name} name=decoder ! videoconvert ! videoscale ! \ + h264parse name=preview_parse disable-passthrough=true ! {decoder_name} name=decoder ! videoconvert ! videoscale ! \ video/x-raw,format=RGBA,width={},height={},pixel-aspect-ratio=1/1 ! \ appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true", profile.display_width, profile.display_height @@ -1272,6 +1300,45 @@ fn push_preview_packet(appsrc: &gst_app::AppSrc, pkt: VideoPacket) { let _ = appsrc.push_buffer(buf); } +#[cfg(not(coverage))] +#[derive(Clone, Copy)] +enum PreviewCapsKind { + Stream, + Decoded, +} + +#[cfg(not(coverage))] +fn record_preview_caps( + shared: &Arc>, + element: &gst::Element, + pad_name: &str, + kind: PreviewCapsKind, +) { + let Some(pad) = element.static_pad(pad_name) else { + return; + }; + let Some(caps) = pad.current_caps() else { + return; + }; + let caps_label = preview_caps_summary(&caps); + if caps_label.is_empty() { + return; + } + if let Ok(mut slot) = shared.lock() { + match kind { + PreviewCapsKind::Stream => slot.telemetry.note_stream_caps(&caps_label), + PreviewCapsKind::Decoded => slot.telemetry.note_decoded_caps(&caps_label), + } + } +} + +#[cfg(not(coverage))] +fn preview_caps_summary(caps: &gst::Caps) -> String { + caps.structure(0) + .map(|structure| structure.to_string()) + .unwrap_or_else(|| caps.to_string()) +} + #[cfg(not(coverage))] fn record_preview_packet(shared: &Arc>, pkt: &VideoPacket) { if let Ok(mut slot) = shared.lock() { diff --git a/client/src/launcher/ui.rs b/client/src/launcher/ui.rs index 3dab378..3faecd5 100644 --- a/client/src/launcher/ui.rs +++ b/client/src/launcher/ui.rs @@ -303,6 +303,8 @@ fn record_diagnostics_sample( left_server_queue_peak: left_metrics.server_queue_peak, left_server_encoder_label: left_metrics.server_encoder_label.clone(), left_decoder_label: left_metrics.decoder_label.clone(), + left_stream_caps_label: left_metrics.stream_caps_label.clone(), + left_decoded_caps_label: left_metrics.decoded_caps_label.clone(), right_receive_fps: right_metrics.receive_fps, right_present_fps: right_metrics.present_fps, right_server_fps: right_metrics.server_fps, @@ -316,6 +318,8 @@ fn record_diagnostics_sample( right_server_queue_peak: right_metrics.server_queue_peak, right_server_encoder_label: right_metrics.server_encoder_label.clone(), right_decoder_label: right_metrics.decoder_label.clone(), + right_stream_caps_label: right_metrics.stream_caps_label.clone(), + right_decoded_caps_label: right_metrics.decoded_caps_label.clone(), dropped_frames: left_metrics .dropped_frames .saturating_add(right_metrics.dropped_frames), diff --git a/common/Cargo.toml b/common/Cargo.toml index 2710ec0..9b45a12 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.11.9" +version = "0.11.10" edition = "2024" build = "build.rs" diff --git a/common/src/cli.rs b/common/src/cli.rs index 25455f4..fa6cc51 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.9"), "lesavka-common CLI (v0.11.9)"); + assert_eq!(banner("0.11.10"), "lesavka-common CLI (v0.11.10)"); } } diff --git a/server/Cargo.toml b/server/Cargo.toml index debde42..5a0b476 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.11.9" +version = "0.11.10" edition = "2024" autobins = false