Fix frame upload: Clutter.Image isn't constructible from GJS anymore

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
This commit is contained in:
Claude
2026-08-04 01:26:38 +00:00
parent d5f6d3ecad
commit 2365d303f3
+54 -7
View File
@@ -1,11 +1,36 @@
import GLib from 'gi://GLib'; import GLib from 'gi://GLib';
import Gio from 'gi://Gio'; import Gio from 'gi://Gio';
import Clutter from 'gi://Clutter'; import Clutter from 'gi://Clutter';
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import {SettingsKey} from './settingsKeys.js'; import {SettingsKey} from './settingsKeys.js';
import {loadGstreamerModules, GSTREAMER_INSTALL_HINT} from './gstreamerAvailability.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. * Plays a video or animated GIF file as a looping animated background.
* `playbin` typefinds the source by content rather than extension, so GIFs * `playbin` typefinds the source by content rather than extension, so GIFs
@@ -53,6 +78,8 @@ export class LiveWallpaperManager {
this._pollCount = 0; this._pollCount = 0;
this._pollTimeoutId = 0; this._pollTimeoutId = 0;
this._noFrameWatchdogId = 0; this._noFrameWatchdogId = 0;
this._imageStrategyIndex = 0;
this._frameErrorCount = 0;
} }
get isAvailable() { get isAvailable() {
@@ -219,6 +246,8 @@ export class LiveWallpaperManager {
this._frameCount = 0; this._frameCount = 0;
this._pollCount = 0; this._pollCount = 0;
this._imageStrategyIndex = 0;
this._frameErrorCount = 0;
// ~30fps polling of the appsink from the main thread. See the // ~30fps polling of the appsink from the main thread. See the
// comment on appsink's properties above for why this replaces // comment on appsink's properties above for why this replaces
// a 'new-sample' signal handler. // a 'new-sample' signal handler.
@@ -282,7 +311,14 @@ export class LiveWallpaperManager {
buffer.unmap(mapInfo); buffer.unmap(mapInfo);
} }
} catch (e) { } 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; return GLib.SOURCE_CONTINUE;
@@ -292,20 +328,31 @@ export class LiveWallpaperManager {
if (!this._actor) if (!this._actor)
return; return;
if (!this._image || this._videoWidth !== width || this._videoHeight !== height) { const needsNewImage = !this._image || this._videoWidth !== width || this._videoHeight !== height;
this._image = new Clutter.Image();
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._videoWidth = width;
this._videoHeight = height; this._videoHeight = height;
} }
strategy.upload(this._image, this._Cogl, data, width, height);
try {
this._image.set_data(data, this._Cogl.PixelFormat.RGBA_8888, width, height, width * 4);
this._actor.set_content(this._image); this._actor.set_content(this._image);
return;
} catch (e) { } catch (e) {
this._logger.error(e, 'Failed to upload live wallpaper frame'); this._logger.warn(
`Live wallpaper: frame image strategy "${strategy.name}" failed ` +
`(${e.message ?? e}), trying the next one`);
this._imageStrategyIndex++;
this._image = null;
} }
} }
throw new Error('All live wallpaper frame image strategies failed');
}
_onBusMessage(message) { _onBusMessage(message) {
const Gst = this._Gst; const Gst = this._Gst;
switch (message.type) { switch (message.type) {