From 9616ba00ead97c0314d0db0003bb97bd23708fbd Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Tue, 21 Apr 2026 01:31:21 -0300 Subject: [PATCH] fix(client): make H.264 decoder selection startup-safe for previews --- client/src/launcher/preview.rs | 90 ++++++++++++++++++++++++++++++---- client/src/output/video.rs | 12 +++-- client/src/video_support.rs | 12 +++-- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/client/src/launcher/preview.rs b/client/src/launcher/preview.rs index fe83b2e..8a60c2a 100644 --- a/client/src/launcher/preview.rs +++ b/client/src/launcher/preview.rs @@ -823,9 +823,22 @@ impl PreviewFeed { session_active_flag, active_bindings_flag, running_flag, - shared_state, - log_sink, + Arc::clone(&shared_state), + Arc::clone(&log_sink), ) { + set_shared_status( + &shared_state, + &log_sink, + monitor_id, + "Preview pipeline setup failed. See session log.", + true, + ); + log_preview_issue( + &shared_state, + &log_sink, + monitor_id, + &format!("Preview feed startup failed: {err:#}"), + ); warn!(monitor_id, ?err, "launcher preview feed exited"); } }); @@ -981,7 +994,33 @@ fn run_preview_feed( shared: Arc>, log_sink: Arc>>>, ) -> Result<()> { - let (pipeline, appsrc, appsink, decoder_name) = build_preview_pipeline(profile)?; + let mut startup_error = None; + let mut selected = None; + for decoder_name in preview_decoder_candidates() { + match build_preview_pipeline(profile, &decoder_name) { + Ok((pipeline, appsrc, appsink, decoder_label)) => { + match pipeline + .set_state(gst::State::Playing) + .context("starting launcher preview pipeline") + { + Ok(_) => { + selected = Some((pipeline, appsrc, appsink, decoder_label)); + break; + } + Err(err) => { + let _ = pipeline.set_state(gst::State::Null); + startup_error = Some(err); + } + } + } + Err(err) => { + startup_error = Some(err); + } + } + } + let (pipeline, appsrc, appsink, decoder_name) = selected.ok_or_else(|| { + startup_error.unwrap_or_else(|| anyhow::anyhow!("no usable H.264 decoder")) + })?; let parser = pipeline.by_name("preview_parse"); let decoder = pipeline.by_name("decoder"); if let Ok(mut slot) = shared.lock() { @@ -997,10 +1036,6 @@ fn run_preview_feed( } }); } - pipeline - .set_state(gst::State::Playing) - .context("starting launcher preview pipeline")?; - { let shared = Arc::clone(&shared); let appsink = appsink.clone(); @@ -1410,8 +1445,8 @@ fn looks_like_preview_problem(status: &str) -> bool { #[cfg(not(coverage))] fn build_preview_pipeline( profile: PreviewProfile, + decoder_name: &str, ) -> Result<(gst::Pipeline, gst_app::AppSrc, gst_app::AppSink, String)> { - let decoder_name = pick_h264_decoder(); let source_mode = eye_source_mode_for_request( profile.requested_width.max(2) as u32, profile.requested_height.max(2) as u32, @@ -1421,10 +1456,11 @@ 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 name=preview_parse disable-passthrough=true ! {decoder_name} name=decoder ! videoconvert ! \ + h264parse name=preview_parse disable-passthrough=true ! {} name=decoder ! videoconvert ! \ videoscale add-borders=false ! \ video/x-raw,format=RGBA,width=(int){render_width},height=(int){render_height},pixel-aspect-ratio=1/1 ! \ appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true", + decoder_name, ); let pipeline = gst::parse::launch(&desc)? .downcast::() @@ -1457,7 +1493,7 @@ fn build_preview_pipeline( .build(), )); - Ok((pipeline, appsrc, appsink, decoder_name)) + Ok((pipeline, appsrc, appsink, decoder_name.to_string())) } #[cfg(not(coverage))] @@ -1481,6 +1517,40 @@ fn preview_render_size( (render_w.max(2), render_h.max(2)) } +#[cfg(not(coverage))] +fn preview_decoder_candidates() -> Vec { + let mut candidates = Vec::new(); + let preferred = pick_h264_decoder(); + if !preferred.trim().is_empty() { + candidates.push(preferred); + } + for name in [ + "avdec_h264", + "openh264dec", + "vah264dec", + "vaapih264dec", + "v4l2h264dec", + "v4l2slh264dec", + "nvh264dec", + "nvh264sldec", + "decodebin", + ] { + if name == "decodebin" || gst::ElementFactory::find(name).is_some() { + candidates.push(name.to_string()); + } + } + candidates.sort(); + candidates.dedup(); + if let Some(pos) = candidates + .iter() + .position(|name| name == &pick_h264_decoder()) + { + let preferred = candidates.remove(pos); + candidates.insert(0, preferred); + } + candidates +} + #[cfg(not(coverage))] fn push_preview_packet(appsrc: &gst_app::AppSrc, pkt: VideoPacket) { let mut buf = gst::Buffer::from_slice(pkt.data); diff --git a/client/src/output/video.rs b/client/src/output/video.rs index 84bde87..600d883 100644 --- a/client/src/output/video.rs +++ b/client/src/output/video.rs @@ -16,22 +16,22 @@ fn pick_h264_decoder() -> String { if name.eq_ignore_ascii_case("decodebin") { return "decodebin".to_string(); } - if !name.is_empty() && gst::ElementFactory::find(name).is_some() { + if !name.is_empty() && buildable_decoder(name) { return name.to_string(); } } for name in [ + "avdec_h264", + "openh264dec", "nvh264dec", "nvh264sldec", "vah264dec", "vaapih264dec", "v4l2h264dec", "v4l2slh264dec", - "openh264dec", - "avdec_h264", ] { - if gst::ElementFactory::find(name).is_some() { + if buildable_decoder(name) { return name.to_string(); } } @@ -39,6 +39,10 @@ fn pick_h264_decoder() -> String { "decodebin".to_string() } +fn buildable_decoder(name: &str) -> bool { + gst::ElementFactory::find(name).is_some() && gst::ElementFactory::make(name).build().is_ok() +} + pub struct MonitorWindow { _pipeline: gst::Pipeline, src: gst_app::AppSrc, diff --git a/client/src/video_support.rs b/client/src/video_support.rs index de344df..b2f01af 100644 --- a/client/src/video_support.rs +++ b/client/src/video_support.rs @@ -17,25 +17,29 @@ pub fn pick_h264_decoder() -> String { if name.eq_ignore_ascii_case("decodebin") { return "decodebin".to_string(); } - if !name.is_empty() && gst::ElementFactory::find(name).is_some() { + if !name.is_empty() && buildable_decoder(name) { return name.to_string(); } } for name in [ + "avdec_h264", + "openh264dec", "nvh264dec", "nvh264sldec", "vah264dec", "vaapih264dec", "v4l2h264dec", "v4l2slh264dec", - "openh264dec", - "avdec_h264", ] { - if gst::ElementFactory::find(name).is_some() { + if buildable_decoder(name) { return name.to_string(); } } "decodebin".to_string() } + +fn buildable_decoder(name: &str) -> bool { + gst::ElementFactory::find(name).is_some() && gst::ElementFactory::make(name).build().is_ok() +}