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]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.11.9"
|
version = "0.11.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -26,6 +26,8 @@ pub struct PerformanceSample {
|
|||||||
pub left_server_queue_peak: u32,
|
pub left_server_queue_peak: u32,
|
||||||
pub left_server_encoder_label: String,
|
pub left_server_encoder_label: String,
|
||||||
pub left_decoder_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_receive_fps: f32,
|
||||||
pub right_present_fps: f32,
|
pub right_present_fps: f32,
|
||||||
pub right_server_fps: f32,
|
pub right_server_fps: f32,
|
||||||
@ -39,6 +41,8 @@ pub struct PerformanceSample {
|
|||||||
pub right_server_queue_peak: u32,
|
pub right_server_queue_peak: u32,
|
||||||
pub right_server_encoder_label: String,
|
pub right_server_encoder_label: String,
|
||||||
pub right_decoder_label: String,
|
pub right_decoder_label: String,
|
||||||
|
pub right_stream_caps_label: String,
|
||||||
|
pub right_decoded_caps_label: String,
|
||||||
pub dropped_frames: u64,
|
pub dropped_frames: u64,
|
||||||
pub queue_depth: u32,
|
pub queue_depth: u32,
|
||||||
}
|
}
|
||||||
@ -109,6 +113,8 @@ pub struct SnapshotReport {
|
|||||||
pub left_server_send_gap_peak_ms: f32,
|
pub left_server_send_gap_peak_ms: f32,
|
||||||
pub left_server_queue_peak: u32,
|
pub left_server_queue_peak: u32,
|
||||||
pub left_server_encoder_label: String,
|
pub left_server_encoder_label: String,
|
||||||
|
pub left_stream_caps_label: String,
|
||||||
|
pub left_decoded_caps_label: String,
|
||||||
pub right_surface: String,
|
pub right_surface: String,
|
||||||
pub right_capture_profile: String,
|
pub right_capture_profile: String,
|
||||||
pub right_capture_transport: String,
|
pub right_capture_transport: String,
|
||||||
@ -123,6 +129,8 @@ pub struct SnapshotReport {
|
|||||||
pub right_server_send_gap_peak_ms: f32,
|
pub right_server_send_gap_peak_ms: f32,
|
||||||
pub right_server_queue_peak: u32,
|
pub right_server_queue_peak: u32,
|
||||||
pub right_server_encoder_label: String,
|
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_camera: Option<String>,
|
||||||
pub selected_microphone: Option<String>,
|
pub selected_microphone: Option<String>,
|
||||||
pub selected_speaker: Option<String>,
|
pub selected_speaker: Option<String>,
|
||||||
@ -223,6 +231,24 @@ impl SnapshotReport {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "pending".to_string()),
|
.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_surface: state.display_surface(1).label().to_string(),
|
||||||
right_capture_profile: format!(
|
right_capture_profile: format!(
|
||||||
"{} | {}x{} | {} fps | {} kbit",
|
"{} | {}x{} | {} fps | {} kbit",
|
||||||
@ -277,6 +303,24 @@ impl SnapshotReport {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "pending".to_string()),
|
.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_camera: state.devices.camera.clone(),
|
||||||
selected_microphone: state.devices.microphone.clone(),
|
selected_microphone: state.devices.microphone.clone(),
|
||||||
selected_speaker: state.devices.speaker.clone(),
|
selected_speaker: state.devices.speaker.clone(),
|
||||||
@ -333,6 +377,8 @@ impl SnapshotReport {
|
|||||||
self.left_queue_depth,
|
self.left_queue_depth,
|
||||||
self.left_queue_peak
|
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!(
|
let _ = writeln!(
|
||||||
text,
|
text,
|
||||||
" server: encoder={} cpu={:.1}% gaps={:.0}/{:.0}ms queue-peak={}",
|
" server: encoder={} cpu={:.1}% gaps={:.0}/{:.0}ms queue-peak={}",
|
||||||
@ -357,6 +403,8 @@ impl SnapshotReport {
|
|||||||
self.right_queue_depth,
|
self.right_queue_depth,
|
||||||
self.right_queue_peak
|
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!(
|
let _ = writeln!(
|
||||||
text,
|
text,
|
||||||
" server: encoder={} cpu={:.1}% gaps={:.0}/{:.0}ms queue-peak={}",
|
" 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_queue_peak: n as u32 + 1,
|
||||||
left_server_encoder_label: "x264enc".to_string(),
|
left_server_encoder_label: "x264enc".to_string(),
|
||||||
left_decoder_label: "decodebin".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_receive_fps: 30.0,
|
||||||
right_present_fps: 28.0,
|
right_present_fps: 28.0,
|
||||||
right_server_fps: 30.0,
|
right_server_fps: 30.0,
|
||||||
@ -670,6 +721,9 @@ mod tests {
|
|||||||
right_server_queue_peak: n as u32 + 1,
|
right_server_queue_peak: n as u32 + 1,
|
||||||
right_server_encoder_label: "source-pass-through".to_string(),
|
right_server_encoder_label: "source-pass-through".to_string(),
|
||||||
right_decoder_label: "decodebin".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,
|
dropped_frames: n,
|
||||||
queue_depth: n as u32,
|
queue_depth: n as u32,
|
||||||
}
|
}
|
||||||
@ -730,6 +784,8 @@ mod tests {
|
|||||||
assert!(report.left_capture_profile.contains("fps"));
|
assert!(report.left_capture_profile.contains("fps"));
|
||||||
assert_eq!(report.left_capture_transport, "server re-encode");
|
assert_eq!(report.left_capture_transport, "server re-encode");
|
||||||
assert_eq!(report.left_decoder_label, "decodebin");
|
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]
|
#[test]
|
||||||
@ -758,6 +814,8 @@ mod tests {
|
|||||||
assert!(text.contains("left eye"));
|
assert!(text.contains("left eye"));
|
||||||
assert!(text.contains("transport:"));
|
assert!(text.contains("transport:"));
|
||||||
assert!(text.contains("live: decoder="));
|
assert!(text.contains("live: decoder="));
|
||||||
|
assert!(text.contains("stream caps:"));
|
||||||
|
assert!(text.contains("decoded caps:"));
|
||||||
assert!(text.contains("recommendations"));
|
assert!(text.contains("recommendations"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ use anyhow::{Context, Result};
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
use gstreamer as gst;
|
use gstreamer as gst;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
use gstreamer::prelude::{Cast, ElementExt, GstBinExt, GstObjectExt};
|
use gstreamer::prelude::{Cast, ElementExt, GstBinExt, GstObjectExt, PadExt};
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
use gstreamer_app as gst_app;
|
use gstreamer_app as gst_app;
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
@ -90,6 +90,8 @@ pub struct PreviewMetricsSnapshot {
|
|||||||
pub server_queue_peak: u32,
|
pub server_queue_peak: u32,
|
||||||
pub server_encoder_label: String,
|
pub server_encoder_label: String,
|
||||||
pub decoder_label: String,
|
pub decoder_label: String,
|
||||||
|
pub stream_caps_label: String,
|
||||||
|
pub decoded_caps_label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
@ -496,6 +498,8 @@ struct PreviewTelemetry {
|
|||||||
latest_server_queue_peak: u32,
|
latest_server_queue_peak: u32,
|
||||||
latest_server_encoder_label: String,
|
latest_server_encoder_label: String,
|
||||||
decoder_label: String,
|
decoder_label: String,
|
||||||
|
stream_caps_label: String,
|
||||||
|
decoded_caps_label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[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 {
|
fn snapshot(&mut self) -> PreviewMetricsSnapshot {
|
||||||
self.snapshot_at(Instant::now())
|
self.snapshot_at(Instant::now())
|
||||||
}
|
}
|
||||||
@ -640,6 +656,8 @@ impl PreviewTelemetry {
|
|||||||
server_queue_peak: self.latest_server_queue_peak,
|
server_queue_peak: self.latest_server_queue_peak,
|
||||||
server_encoder_label: self.latest_server_encoder_label.clone(),
|
server_encoder_label: self.latest_server_encoder_label.clone(),
|
||||||
decoder_label: self.decoder_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>>>>,
|
log_sink: Arc<Mutex<Option<std::sync::mpsc::Sender<String>>>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let (pipeline, appsrc, appsink, decoder_name) = build_preview_pipeline(profile)?;
|
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() {
|
if let Ok(mut slot) = shared.lock() {
|
||||||
slot.telemetry.note_decoder(&decoder_name);
|
slot.telemetry.note_decoder(&decoder_name);
|
||||||
}
|
}
|
||||||
@ -830,6 +850,8 @@ fn run_preview_feed(
|
|||||||
{
|
{
|
||||||
let shared = Arc::clone(&shared);
|
let shared = Arc::clone(&shared);
|
||||||
let appsink = appsink.clone();
|
let appsink = appsink.clone();
|
||||||
|
let parser = parser.clone();
|
||||||
|
let decoder = decoder.clone();
|
||||||
let running = Arc::clone(&running);
|
let running = Arc::clone(&running);
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
@ -837,6 +859,12 @@ fn run_preview_feed(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if let Some(sample) = appsink.try_pull_sample(gst::ClockTime::from_mseconds(250)) {
|
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 Some(frame) = sample_to_frame(&sample) {
|
||||||
if let Ok(mut slot) = shared.lock() {
|
if let Ok(mut slot) = shared.lock() {
|
||||||
slot.push_frame(frame);
|
slot.push_frame(frame);
|
||||||
@ -1225,7 +1253,7 @@ fn build_preview_pipeline(
|
|||||||
let desc = format!(
|
let desc = format!(
|
||||||
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
"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 ! \
|
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 ! \
|
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",
|
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true",
|
||||||
profile.display_width, profile.display_height
|
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);
|
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))]
|
#[cfg(not(coverage))]
|
||||||
fn record_preview_packet(shared: &Arc<Mutex<SharedPreviewState>>, pkt: &VideoPacket) {
|
fn record_preview_packet(shared: &Arc<Mutex<SharedPreviewState>>, pkt: &VideoPacket) {
|
||||||
if let Ok(mut slot) = shared.lock() {
|
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_queue_peak: left_metrics.server_queue_peak,
|
||||||
left_server_encoder_label: left_metrics.server_encoder_label.clone(),
|
left_server_encoder_label: left_metrics.server_encoder_label.clone(),
|
||||||
left_decoder_label: left_metrics.decoder_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_receive_fps: right_metrics.receive_fps,
|
||||||
right_present_fps: right_metrics.present_fps,
|
right_present_fps: right_metrics.present_fps,
|
||||||
right_server_fps: right_metrics.server_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_queue_peak: right_metrics.server_queue_peak,
|
||||||
right_server_encoder_label: right_metrics.server_encoder_label.clone(),
|
right_server_encoder_label: right_metrics.server_encoder_label.clone(),
|
||||||
right_decoder_label: right_metrics.decoder_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: left_metrics
|
||||||
.dropped_frames
|
.dropped_frames
|
||||||
.saturating_add(right_metrics.dropped_frames),
|
.saturating_add(right_metrics.dropped_frames),
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.11.9"
|
version = "0.11.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn banner_includes_version() {
|
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]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.11.9"
|
version = "0.11.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user