fix(client): make H.264 decoder selection startup-safe for previews

This commit is contained in:
Brad Stein 2026-04-21 01:31:21 -03:00
parent 22a92c9fe7
commit 9616ba00ea
3 changed files with 96 additions and 18 deletions

View File

@ -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<Mutex<SharedPreviewState>>,
log_sink: Arc<Mutex<Option<std::sync::mpsc::Sender<String>>>>,
) -> 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::<gst::Pipeline>()
@ -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<String> {
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);

View File

@ -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,

View File

@ -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()
}