From 5157f4a1c91cd8531f22866958128aabc85a93d6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 01:11:29 +0000 Subject: [PATCH] Build the live wallpaper sink bin explicitly instead of via string parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User testing pinned the bug precisely: our try/catch is airtight now, and the diagnostics showed "new-sample signal fired 0 times" — the appsink genuinely never receives a single sample inside gnome-shell's process, while an equivalent pipeline (videoconvert ! videoscale ! RGBA caps ! appsink) played correctly standalone via gst-launch-1.0. Replaces Gst.parse_bin_from_description()'s gst-launch mini-language parsing (with its automatic unlinked-pad ghosting) with explicit ElementFactory.make() + Bin.add() + element.link() + a manually created GhostPad. This removes the string-parsing path as a variable entirely and, importantly, surfaces any link() failure as a thrown error instead of failing silently, which the old code path had no way to report even if that patch were the whole cachet of the problem. Also fixes a related warning ("Trying to dispose element ..., but it is in PLAYING instead of the NULL state") from _stop() dropping the playbin reference immediately after requesting the NULL state, before the (async) transition actually completed — now waits briefly via get_state() for it to settle first. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8 --- lib/liveWallpaper.js | 72 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/lib/liveWallpaper.js b/lib/liveWallpaper.js index fe69289..6317ee9 100644 --- a/lib/liveWallpaper.js +++ b/lib/liveWallpaper.js @@ -128,6 +128,61 @@ export class LiveWallpaperManager { this._start(); } + /** + * Builds videoconvert ! videoscale ! capsfilter(RGBA) ! appsink using + * explicit element creation, linking, and a ghost pad, rather than + * Gst.parse_bin_from_description()'s gst-launch mini-language. Both + * are meant to be equivalent, but a user's testing showed the + * string-parsed version's appsink never fired 'new-sample' even once + * inside gnome-shell's process while an equivalent standalone + * gst-launch-1.0 pipeline worked fine — this removes the string + * parsing (and its automatic ghost-pad detection) as a variable, and + * surfaces link() failures explicitly instead of failing silently. + */ + _buildSinkBin() { + const Gst = this._Gst; + + const videoconvert = Gst.ElementFactory.make('videoconvert', 'benthicbloom-convert'); + const videoscale = Gst.ElementFactory.make('videoscale', 'benthicbloom-scale'); + const capsfilter = Gst.ElementFactory.make('capsfilter', 'benthicbloom-capsfilter'); + const appsink = Gst.ElementFactory.make('appsink', 'benthicbloom-appsink'); + + if (!videoconvert || !videoscale || !capsfilter || !appsink) { + throw new Error( + 'Failed to create one or more GStreamer elements ' + + '(videoconvert/videoscale/capsfilter/appsink) — a required plugin is likely missing'); + } + + capsfilter.set_property('caps', Gst.Caps.from_string('video/x-raw,format=RGBA')); + appsink.set_property('emit-signals', true); + appsink.set_property('max-buffers', 2); + appsink.set_property('drop', true); + appsink.set_property('sync', true); + + const sinkBin = new Gst.Bin({name: 'benthicbloom-sinkbin'}); + sinkBin.add(videoconvert); + sinkBin.add(videoscale); + sinkBin.add(capsfilter); + sinkBin.add(appsink); + + if (!videoconvert.link(videoscale)) + throw new Error('Failed to link videoconvert -> videoscale'); + if (!videoscale.link(capsfilter)) + throw new Error('Failed to link videoscale -> capsfilter'); + if (!capsfilter.link(appsink)) + throw new Error('Failed to link capsfilter -> appsink'); + + const sinkPad = videoconvert.get_static_pad('sink'); + const ghostPad = Gst.GhostPad.new('sink', sinkPad); + if (!ghostPad) + throw new Error('Failed to create ghost pad for live wallpaper sink bin'); + ghostPad.set_active(true); + sinkBin.add_pad(ghostPad); + + this._appsink = appsink; + return sinkBin; + } + _start() { const path = this._settings.get_string(SettingsKey.LIVE_WALLPAPER_PATH); if (!this._available || !path) @@ -143,11 +198,7 @@ export class LiveWallpaperManager { Main.layoutManager._backgroundGroup.add_child(this._actor); this._layoutActor(); - const sinkBin = Gst.parse_bin_from_description( - 'videoconvert ! videoscale ! video/x-raw,format=RGBA ! ' + - 'appsink name=benthicbloom_sink emit-signals=true max-buffers=2 drop=true sync=true', - true); - this._appsink = sinkBin.get_by_name('benthicbloom_sink'); + const sinkBin = this._buildSinkBin(); this._appsink.connect('new-sample', sink => this._onNewSample(sink)); this._playbin = Gst.ElementFactory.make('playbin', 'benthicbloom-live-wallpaper'); @@ -381,13 +432,22 @@ export class LiveWallpaperManager { } if (this._playbin) { + const Gst = this._Gst; const bus = this._playbin.get_bus(); if (this._busWatchId) { bus.disconnect(this._busWatchId); this._busWatchId = 0; } bus.remove_signal_watch(); - this._playbin.set_state(this._Gst.State.NULL); + + this._playbin.set_state(Gst.State.NULL); + // Block briefly for the (normally fast) transition to actually + // finish before dropping our reference — otherwise the element + // can get disposed mid-transition, which GStreamer logs as + // "Trying to dispose element ..., but it is in PLAYING instead + // of the NULL state". + this._playbin.get_state(200 * Gst.MSECOND); + this._playbin = null; this._appsink = null; }