From 2365d303f3ee4305dde5631266f5dff3e7c8072c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 01:26:38 +0000 Subject: [PATCH] Fix frame upload: Clutter.Image isn't constructible from GJS anymore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8 --- lib/liveWallpaper.js | 69 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/lib/liveWallpaper.js b/lib/liveWallpaper.js index 8b67347..9cafde0 100644 --- a/lib/liveWallpaper.js +++ b/lib/liveWallpaper.js @@ -1,11 +1,36 @@ import GLib from 'gi://GLib'; import Gio from 'gi://Gio'; import Clutter from 'gi://Clutter'; +import St from 'gi://St'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import {SettingsKey} from './settingsKeys.js'; import {loadGstreamerModules, GSTREAMER_INSTALL_HINT} from './gstreamerAvailability.js'; +/** + * Ways to get a decoded frame onto an actor, tried in order and "pinned" + * once one works. `new Clutter.Image()` failed with "is not a constructor" + * in real-world testing (Clutter.Image is apparently no longer directly + * constructible from GJS in current Mutter); St.ImageContent — the same + * mechanism gnome-shell's own code uses for uploading raw pixel buffers — + * is tried first, with the legacy Clutter.Image/set_data path kept only as + * a fallback for older shells. + */ +const FRAME_IMAGE_STRATEGIES = [ + { + name: 'St.ImageContent', + create: (width, height) => St.ImageContent.new_with_preferred_size(width, height), + upload: (image, Cogl, data, width, height) => + image.set_bytes(GLib.Bytes.new(data), Cogl.PixelFormat.RGBA_8888, width, height, width * 4), + }, + { + name: 'Clutter.Image', + create: () => new Clutter.Image(), + upload: (image, Cogl, data, width, height) => + image.set_data(data, Cogl.PixelFormat.RGBA_8888, width, height, width * 4), + }, +]; + /** * Plays a video or animated GIF file as a looping animated background. * `playbin` typefinds the source by content rather than extension, so GIFs @@ -53,6 +78,8 @@ export class LiveWallpaperManager { this._pollCount = 0; this._pollTimeoutId = 0; this._noFrameWatchdogId = 0; + this._imageStrategyIndex = 0; + this._frameErrorCount = 0; } get isAvailable() { @@ -219,6 +246,8 @@ export class LiveWallpaperManager { this._frameCount = 0; this._pollCount = 0; + this._imageStrategyIndex = 0; + this._frameErrorCount = 0; // ~30fps polling of the appsink from the main thread. See the // comment on appsink's properties above for why this replaces // a 'new-sample' signal handler. @@ -282,7 +311,14 @@ export class LiveWallpaperManager { buffer.unmap(mapInfo); } } catch (e) { - this._logger.error(e, 'Failed to poll live wallpaper frame'); + // Rate-limited: this runs at ~30fps, so logging every failure + // would flood the journal (and this loop keeps retrying every + // frame, e.g. while cycling through FRAME_IMAGE_STRATEGIES). + this._frameErrorCount++; + if (this._frameErrorCount === 1 || this._frameErrorCount % 300 === 0) { + this._logger.error( + e, `Failed to poll live wallpaper frame (${this._frameErrorCount} failures so far)`); + } } return GLib.SOURCE_CONTINUE; @@ -292,18 +328,29 @@ export class LiveWallpaperManager { if (!this._actor) return; - if (!this._image || this._videoWidth !== width || this._videoHeight !== height) { - this._image = new Clutter.Image(); - this._videoWidth = width; - this._videoHeight = height; + const needsNewImage = !this._image || this._videoWidth !== width || this._videoHeight !== height; + + while (this._imageStrategyIndex < FRAME_IMAGE_STRATEGIES.length) { + const strategy = FRAME_IMAGE_STRATEGIES[this._imageStrategyIndex]; + try { + if (needsNewImage) { + this._image = strategy.create(width, height); + this._videoWidth = width; + this._videoHeight = height; + } + strategy.upload(this._image, this._Cogl, data, width, height); + this._actor.set_content(this._image); + return; + } catch (e) { + this._logger.warn( + `Live wallpaper: frame image strategy "${strategy.name}" failed ` + + `(${e.message ?? e}), trying the next one`); + this._imageStrategyIndex++; + this._image = null; + } } - try { - this._image.set_data(data, this._Cogl.PixelFormat.RGBA_8888, width, height, width * 4); - this._actor.set_content(this._image); - } catch (e) { - this._logger.error(e, 'Failed to upload live wallpaper frame'); - } + throw new Error('All live wallpaper frame image strategies failed'); } _onBusMessage(message) {