set_bytes() takes a Cogl.Context as its first parameter (added upstream
in GNOME Shell 48, see js/ui/screenshot.js); omitting it produced
"At least 6 arguments required, but only 5 passed" for every frame,
which fell through to the equally non-functional Clutter.Image fallback
and left the live wallpaper blank.
The polling fix worked and pinpointed the real, final bug: appsink was
delivering frames correctly (confirmed by the polling infrastructure
itself running fine), but every single upload attempt threw
"(intermediate value).Image is not a constructor" from `new
Clutter.Image()`. In current Mutter, Clutter.Image apparently isn't
directly constructible via `new` from GJS anymore, and the resulting
exception was also flooding the log at ~30/sec with no rate limiting.
Adds St.ImageContent.new_with_preferred_size() + set_bytes(GLib.Bytes)
as the primary path — the same mechanism gnome-shell's own code uses
for uploading raw pixel buffers onto actors — with the old
Clutter.Image/set_data() path kept only as a fallback for older
shells. The two are tried in order per frame size change and "pinned"
once one works, avoiding a live per-frame strategy search. Also
rate-limits the poll error log to the 1st and every 300th failure
instead of every single one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
The last round of testing ruled out the pipeline construction itself:
even with the sink bin rebuilt from explicit, individually-checked
link() calls (no thrown errors, playbin reaches PLAYING), the
'new-sample' signal still never fired inside gnome-shell — while an
equivalent pipeline worked fine standalone via gst-launch-1.0.
The remaining, gnome-shell-specific difference: appsink emits
'new-sample' from GStreamer's own streaming thread, not the main
thread. GJS's JS engine isn't safe to call into from an arbitrary
background thread, and a cross-thread signal emission can be silently
dropped rather than invoked or crashed on — which looks exactly like
"the signal never fires" from here, even though the pipeline is
actually running.
Replaces the signal entirely with polling: a ~30fps GLib.timeout_add()
on the main thread calls appsink.try_pull_sample(0), which is
explicitly documented as safe to call from any thread since we are now
the ones calling into GStreamer rather than the reverse. Also updates
the no-frames watchdog to report the poll count instead of a signal
fire count.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
Standalone gst-launch testing confirmed appsink-as-playbin-video-sink
works fine outside the extension (reaches PLAYING, negotiates caps,
runs to completion), narrowing the "no frames received" symptom down
to something inside our own new-sample handling in the shell process.
_onNewSample()'s try/catch didn't cover the sink.pull_sample() call
itself — if that specific call throws when invoked as a GJS signal
callback, the exception bypassed our logging entirely, falling back
to GJS's generic uncaught-exception path (which, like the
openPreferences case, may not mention "benthicbloom" and gets missed
by a filtered grep). Now the whole handler is covered, logs when the
signal fires for the first time, logs a null pull_sample() or failed
buffer.map() explicitly, and the watchdog reports the new-sample fire
count so we can tell "signal never fired" from "signal fired but
processing failed" on the next test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
A user's live wallpaper reached "started" with zero errors (Cogl and
GStreamer both loading fine, actor added to _backgroundGroup) but
never actually appeared on screen, and no code path here could tell
us why: the bus handler only looked at EOS/ERROR, silently discarding
WARNING and STATE_CHANGED messages where a stalled negotiation or
preroll would actually show up.
Adds: WARNING message logging, PLAYING/PAUSED/etc. state-changed
logging for the playbin itself, a frame counter with first-frame and
periodic debug logs, and a one-shot watchdog that logs an explicit
warning if zero frames arrive within 4s of set_state(PLAYING). Also
switched appsink's pull-sample from an emitted action signal to the
plain pull_sample() method, which is the more directly supported
GstApp.AppSink API and one less variable while debugging.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
The Preferences window's availability probe (added to surface missing
GStreamer deps in-UI) reused loadGstreamerModules(), which also checks
for Cogl. Cogl is Mutter's private library — its typelib is only
reachable from inside the actual gnome-shell process (which gets a
private GI search path), never from the separate, plain-GTK4
Preferences process. So the check always failed there with "Requiring
Cogl ... not found" even when live wallpapers were working correctly
in the real shell process, as confirmed by a user report where the
extension's own log showed live wallpaper playback starting cleanly
while Preferences simultaneously claimed GStreamer was missing.
Split into loadGstreamerModules() (Gst+GstApp+Cogl, used only by
liveWallpaper.js in the shell process) and a new lighter
checkGstreamerBaseAvailable() (Gst+GstApp only) for prefs.js, with a
note in the UI that a clean check there isn't a full guarantee since
the private half can't be verified from that process.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
The extension's own log showed rotation kept firing ("Wallpaper
changed to ...") after live wallpaper playback had already started.
Each rotation change writes org.gnome.desktop.background's
picture-uri, which makes GNOME Shell repaint its own background actor
into the same _backgroundGroup our live wallpaper actor lives in,
landing on top of it and hiding the video/GIF entirely.
LiveWallpaperManager now takes an onActiveChanged callback, invoked
only on actual start/stop transitions, which extension.js uses to
suspend/unsuspend RotationManager. This is tracked separately from
user-initiated pause()/resume() so turning live wallpaper off restores
whatever rotation state the user actually had, and next()/previous()
now no-op while suspended so manual/forced-OLED wallpaper changes
can't sneak one in either. The indicator's "Next Wallpaper" item is
greyed out to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
Previously, if the Gst/GstApp/Cogl GI typelibs couldn't be loaded (e.g.
gst-plugins-base/good not installed, only the bare gstreamer package),
the only sign was a warning buried in the shell's log — the live
wallpaper page looked fully functional and silently did nothing.
Preferences now probes for the same bindings the running extension
needs and shows a warning row with the exact import error plus
per-distro install commands, and greys out the controls that only
matter once GStreamer is actually available. Extracted the shared
probing logic into lib/gstreamerAvailability.js so extension.js and
prefs.js (separate processes) don't duplicate it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
The playback pipeline already typefinds by content and decodes GIFs
via GStreamer's decodebin like any other video, but the preferences
file picker filtered to video/* MIME types only, hiding .gif files.
Widen the filter to include image/gif and *.gif, add an "All files"
fallback, and update the related copy/docs to mention GIF support.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8