lesavka: expose preview stream and decode caps
This commit is contained in:
parent
381676f3a3
commit
2e47863584
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.11.9"
|
||||
version = "0.11.10"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -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<String>,
|
||||
pub selected_microphone: Option<String>,
|
||||
pub selected_speaker: Option<String>,
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
|
||||
@ -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<Mutex<Option<std::sync::mpsc::Sender<String>>>>,
|
||||
) -> 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<Mutex<SharedPreviewState>>,
|
||||
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<Mutex<SharedPreviewState>>, pkt: &VideoPacket) {
|
||||
if let Ok(mut slot) = shared.lock() {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.11.9"
|
||||
version = "0.11.10"
|
||||
edition = "2024"
|
||||
build = "build.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)");
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.11.9"
|
||||
version = "0.11.10"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user