Skip to content

Commit 0a0eabf

Browse files
dkulpclaude
andcommitted
fix(media): say why a video-only file will not play instead of blaming a missing plug-in
Playing a file with no audio track on an item whose video output is disabled -- or whose display is unplugged, which drops FPP into the same fallback -- produced three messages, each pointing somewhere the problem was not: GStreamer sync error (src=decodebin0): Your GStreamer installation is missing a plug-in. GStreamer sync error (src=qtdemux0): Internal data stream error. GStreamer: pipeline never reached PLAYING within 15000ms — A PipeWire restart under a running fppd does this; fppd must be restarted to reconnect. Nothing was missing, the stream was not corrupt, and PipeWire was uninvolved. The audio-only fallback pins decodebin to caps="audio/x-raw", so a file with no audio gives it nothing to expose and the autoplug failure surfaces as a missing plug-in. Reported as #2839, where it cost the reporter a hunt for absent packages. Only that fallback is affected. The video pipeline links its pads by hand and OnNoMorePads already tears down the unconnected audio chain for video-only media, so with a display attached these files play silently and correctly -- confirmed on a Pi with an HDMI display, where the same file plays clean with the video output set and fails the moment it is disabled. So name the fallback's decodebin and watch pad-added. GStreamer itself then answers whether the file yielded decodable audio: no probe, no TagLib guess (its zero-channel answer cannot distinguish "no audio track" from "container I cannot parse"), and nothing added to the normal start path. The demuxer's follow-on error is folded into the same event rather than logged as a second, unrelated failure, and the raw GStreamer wording is kept at debug level. A missing audio pad is not on its own evidence about the file, which the first version of this got wrong: stop PipeWire under a running fppd, play a plain WAV, and pwsink errors before decodebin ever gets that far -- reporting a backend outage as "this file has no audio" is a worse lie than the one being fixed. The error has to come from inside the decode chain too, so the source is walked up GST_OBJECT_PARENT to the named decodebin before the file is blamed. Separately, the preroll check reported GST_STATE_CHANGE_FAILURE and a genuine timeout identically. An element that errors returns FAILURE at once, so the message claimed a 15-second wait it never made and named a PipeWire restart that never happened -- for any failure at all. Split the two: FAILURE points at the error the bus handler has already logged, and only a real timeout keeps the stale-connection advice. Both paths verified on a single-core AM335x board, which is slow enough to lose the race to the bus handler and take that branch. The entry still ends rather than holding its duration, so a playlist advances early past such an item; making it consume the full duration means synthesising playback timing and is left alone here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1ce4f64 commit 0a0eabf

2 files changed

Lines changed: 115 additions & 12 deletions

File tree

src/mediaoutput/GStreamerOut.cpp

Lines changed: 103 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1666,7 +1666,7 @@ int GStreamerOutput::Start(int msTime) {
16661666
}
16671667
std::string rateCaps = decodeRate ? ",rate=" + std::to_string(decodeRate) : "";
16681668
std::string pipelineStr =
1669-
"filesrc location=\"" + fullPath + "\" ! decodebin expose-all-streams=false caps=\"audio/x-raw\" ! audioconvert ! audioresample ! "
1669+
"filesrc location=\"" + fullPath + "\" ! decodebin name=decoder expose-all-streams=false caps=\"audio/x-raw\" ! audioconvert ! audioresample ! "
16701670
"audio/x-raw" + rateCaps + chOrderCaps + " ! " + chOrderPermute +
16711671
"tee name=t "
16721672
"t. ! queue ! volume name=vol ! " + sinkStr + " "
@@ -1685,6 +1685,18 @@ int GStreamerOutput::Start(int msTime) {
16851685
return 0;
16861686
}
16871687

1688+
// This pipeline's only sink is audio, so a file with nothing decodable
1689+
// to feed it cannot play at all -- and the caps filter above turns that
1690+
// into an autoplug failure decodebin reports as a missing plug-in, which
1691+
// sends people off installing packages that were never the problem.
1692+
// Watch the pads so the real reason can be given instead; the linking
1693+
// itself stays with gst_parse_launch's own delayed-link handler.
1694+
m_audioOnlyPipeline = true;
1695+
if (GstElement* fbDecoder = gst_bin_get_by_name(GST_BIN(m_pipeline), "decoder")) {
1696+
g_signal_connect(fbDecoder, "pad-added", G_CALLBACK(OnPadAdded), this);
1697+
gst_object_unref(fbDecoder);
1698+
}
1699+
16881700
// Get the volume element for later control
16891701
m_volume = gst_bin_get_by_name(GST_BIN(m_pipeline), "vol");
16901702

@@ -1911,17 +1923,34 @@ int GStreamerOutput::Start(int msTime) {
19111923
// the failure is completely invisible.
19121924
if (!cancel->load()) {
19131925
GstState startedState = GST_STATE_NULL;
1914-
if (gst_element_get_state(pipeline, &startedState, nullptr,
1915-
(GstClockTime)PREROLL_TIMEOUT_MS * GST_MSECOND) != GST_STATE_CHANGE_SUCCESS ||
1916-
startedState != GST_STATE_PLAYING) {
1926+
GstStateChangeReturn scr = gst_element_get_state(pipeline, &startedState, nullptr,
1927+
(GstClockTime)PREROLL_TIMEOUT_MS * GST_MSECOND);
1928+
if (scr != GST_STATE_CHANGE_SUCCESS || startedState != GST_STATE_PLAYING) {
19171929
if (!cancel->load()) {
1918-
LogErr(VB_MEDIAOUT,
1919-
"GStreamer: pipeline never reached PLAYING within %dms (state=%d) — output will be "
1920-
"silent. A PipeWire restart under a running fppd does this; fppd must be restarted "
1921-
"to reconnect.\n",
1922-
PREROLL_TIMEOUT_MS, startedState);
1923-
WarningHolder::AddWarningTimeout(60, 30,
1924-
"Media playback did not start (audio backend connection lost — restart FPPD)");
1930+
// FAILURE and "still not there after the timeout" are
1931+
// different faults and want different advice, but this
1932+
// reported both as the stale-PipeWire-connection case.
1933+
// An element that errors out returns FAILURE straight
1934+
// away, so the message claimed a 15-second timeout it
1935+
// had not waited for and blamed a PipeWire restart that
1936+
// had not happened -- for any failure at all, including
1937+
// a media file the pipeline simply could not play. The
1938+
// bus error handler has already logged the real cause in
1939+
// that case, so say nothing more than the outcome here.
1940+
if (scr == GST_STATE_CHANGE_FAILURE) {
1941+
LogErr(VB_MEDIAOUT,
1942+
"GStreamer: pipeline failed to start (state=%s) — see the error logged above "
1943+
"for the cause.\n",
1944+
gst_element_state_get_name(startedState));
1945+
} else {
1946+
LogErr(VB_MEDIAOUT,
1947+
"GStreamer: pipeline never reached PLAYING within %dms (state=%s) — output will "
1948+
"be silent. A PipeWire restart under a running fppd does this; fppd must be "
1949+
"restarted to reconnect.\n",
1950+
PREROLL_TIMEOUT_MS, gst_element_state_get_name(startedState));
1951+
WarningHolder::AddWarningTimeout(60, 30,
1952+
"Media playback did not start (audio backend connection lost — restart FPPD)");
1953+
}
19251954
}
19261955
}
19271956
}
@@ -2963,7 +2992,60 @@ GstBusSyncReply GStreamerOutput::BusSyncHandler(GstBus* bus, GstMessage* msg, gp
29632992
// disconnected during playback — treat as non-fatal.
29642993
bool isDirectKmsSink = (strncmp(srcName, "dkms_", 5) == 0);
29652994

2966-
if (isAES67Branch || isVideoPWSink || isDirectKmsSink) {
2995+
// A file with nothing decodable to feed the audio-only pipeline fails
2996+
// here, and the raw GStreamer wording is actively misleading: decodebin
2997+
// reports its autoplug failure as "your installation is missing a
2998+
// plug-in" (nothing is missing -- there is simply no audio to plug), and
2999+
// the demuxer then follows with a generic "Internal data stream error".
3000+
// A video-only clip played on an item whose video output is disabled or
3001+
// whose display is unplugged lands exactly here, so say what actually
3002+
// happened and what to do about it. Everything else keeps the raw
3003+
// message -- this only claims the case it can prove, which is that
3004+
// decodebin never produced an audio pad.
3005+
// "No audio pad yet" is not on its own evidence that the file has no
3006+
// audio: anything that fails before decodebin gets that far leaves the
3007+
// flag clear too. Stopping PipeWire under a running fppd and playing a
3008+
// plain WAV does exactly that -- pwsink errors first, and blaming the
3009+
// file for an audio-backend outage is a worse lie than the one being
3010+
// fixed. So the source has to be the decode chain as well.
3011+
bool errorFromDecoder = false;
3012+
for (GstObject* o = GST_MESSAGE_SRC(msg); o; o = GST_OBJECT_PARENT(o)) {
3013+
const gchar* n = GST_OBJECT_NAME(o);
3014+
if (n && strcmp(n, "decoder") == 0) {
3015+
errorFromDecoder = true;
3016+
break;
3017+
}
3018+
}
3019+
if (self->m_audioOnlyPipeline && errorFromDecoder &&
3020+
!self->m_sawAudioPad.load(std::memory_order_acquire)) {
3021+
if (!self->m_reportedNoAudio.exchange(true, std::memory_order_acq_rel)) {
3022+
LogErr(VB_MEDIAOUT,
3023+
"GStreamer: '%s' has no playable audio stream, and no video output is "
3024+
"configured for it, so there is nothing to play. If this is a video-only "
3025+
"file, set the item's Video Output to a connected display; FPP falls back to "
3026+
"audio-only whenever the chosen display is disabled or unplugged.\n",
3027+
self->m_mediaFilename.c_str());
3028+
WarningHolder::AddWarningTimeout(60, 30,
3029+
"No playable audio in " + self->m_mediaFilename +
3030+
" and no video output — nothing to play");
3031+
LogDebug(VB_MEDIAOUT, "GStreamer no-audio underlying error (src=%s): %s\n",
3032+
srcName, err->message);
3033+
#ifdef HAS_AES67_GSTREAMER
3034+
self->DetachAES67Branches();
3035+
#endif
3036+
self->m_playing = false;
3037+
if (self->m_mediaOutputStatus) {
3038+
self->m_mediaOutputStatus->status = MEDIAOUTPUTSTATUS_IDLE;
3039+
}
3040+
self->Stopping();
3041+
self->Stopped();
3042+
} else {
3043+
// The demuxer's follow-on error describes the same event; logging
3044+
// it again reads as a second, unrelated failure.
3045+
LogDebug(VB_MEDIAOUT, "GStreamer no-audio follow-on error (src=%s): %s\n",
3046+
srcName, err->message);
3047+
}
3048+
} else if (isAES67Branch || isVideoPWSink || isDirectKmsSink) {
29673049
LogWarn(VB_MEDIAOUT, "GStreamer non-fatal error (src=%s): %s\n",
29683050
srcName, err->message);
29693051
LogDebug(VB_MEDIAOUT, "GStreamer AES67 branch debug: %s\n",
@@ -3567,6 +3649,15 @@ void GStreamerOutput::OnPadAdded(GstElement* element, GstPad* pad, gpointer user
35673649
const gchar* name = gst_structure_get_name(gst_caps_get_structure(caps, 0));
35683650
LogDebug(VB_MEDIAOUT, "GStreamer decodebin pad-added: %s\n", name);
35693651

3652+
// Record that the file yielded decodable audio, independently of whether
3653+
// this pipeline has an audio chain to link it into. The audio-only fallback
3654+
// links its pads through gst_parse_launch rather than here, so m_audioChain
3655+
// is null there and the branch below is skipped -- but that pipeline is
3656+
// exactly the one that needs to know, so the flag is set before the check.
3657+
if (g_str_has_prefix(name, "audio/")) {
3658+
self->m_sawAudioPad.store(true, std::memory_order_release);
3659+
}
3660+
35703661
if (g_str_has_prefix(name, "audio/") && self->m_audioChain) {
35713662
GstPad* sinkPad = gst_element_get_static_pad(self->m_audioChain, "sink");
35723663
if (sinkPad && !gst_pad_is_linked(sinkPad)) {

src/mediaoutput/GStreamerOut.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,18 @@ class GStreamerOutput : public MediaOutputBase {
226226
bool m_audioLinked = false; // true when audio pad was connected
227227
bool m_videoLinked = false; // true when video pad was connected
228228

229+
// True when Start() built the audio-only fallback (no video output for this
230+
// item), which is the one pipeline shape that cannot survive a file with no
231+
// playable audio -- see the no-audio handling in BusSyncHandler().
232+
bool m_audioOnlyPipeline = false;
233+
// Whether decodebin ever exposed a decoded audio pad. Written on the
234+
// streaming thread from OnPadAdded, read on the bus thread.
235+
std::atomic<bool> m_sawAudioPad{false};
236+
// Set once the "file has no playable audio" case has been reported, so the
237+
// demuxer's follow-on "Internal data stream error" is not logged as a second,
238+
// unrelated-looking failure.
239+
std::atomic<bool> m_reportedNoAudio{false};
240+
229241
#ifdef HAS_AES67_GSTREAMER
230242
// Zero-hop AES67 RTP branches attached to the audio tee (Phase 7.9)
231243
std::vector<AES67Manager::InlineRTPBranch> m_aes67Branches;

0 commit comments

Comments
 (0)