diff --git a/lib/liveWallpaper.js b/lib/liveWallpaper.js index 6317ee9..8b67347 100644 --- a/lib/liveWallpaper.js +++ b/lib/liveWallpaper.js @@ -50,7 +50,8 @@ export class LiveWallpaperManager { this._upowerSignalId = 0; this._frameCount = 0; - this._newSampleCallCount = 0; + this._pollCount = 0; + this._pollTimeoutId = 0; this._noFrameWatchdogId = 0; } @@ -154,7 +155,11 @@ export class LiveWallpaperManager { } capsfilter.set_property('caps', Gst.Caps.from_string('video/x-raw,format=RGBA')); - appsink.set_property('emit-signals', true); + // emit-signals is deliberately left off: appsink's 'new-sample' fires + // from GStreamer's streaming thread, not gnome-shell's main thread, + // and a cross-thread call into the JS engine can be silently dropped + // rather than invoked. We poll with try_pull_sample() from a main + // thread GLib timer instead, which is safe to call from any thread. appsink.set_property('max-buffers', 2); appsink.set_property('drop', true); appsink.set_property('sync', true); @@ -199,7 +204,6 @@ export class LiveWallpaperManager { this._layoutActor(); const sinkBin = this._buildSinkBin(); - this._appsink.connect('new-sample', sink => this._onNewSample(sink)); this._playbin = Gst.ElementFactory.make('playbin', 'benthicbloom-live-wallpaper'); this._playbin.set_property('video-sink', sinkBin); @@ -214,16 +218,20 @@ export class LiveWallpaperManager { this._logger.debug(`playbin.set_state(PLAYING) returned ${stateChangeResult}`); this._frameCount = 0; - this._newSampleCallCount = 0; + this._pollCount = 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. + this._pollTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 33, () => this._pollForSample()); + this._noFrameWatchdogId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 4, () => { this._noFrameWatchdogId = 0; if (this._active && this._frameCount === 0) { this._logger.warn( `Live wallpaper: no frames received 4s after starting playback ` + - `(new-sample signal fired ${this._newSampleCallCount} times). ` + - 'If that count is 0, the signal itself never fired (check for a stalled ' + - 'pipeline above); if it is nonzero, pull_sample()/buffer processing is failing ' + - '— check for a "Failed to process live wallpaper frame" error above.'); + `(polled appsink ${this._pollCount} times). If that count is 0, the poll timer ` + + 'itself never ran; if it is nonzero, try_pull_sample() keeps returning nothing ' + + '— check for a "Failed to poll live wallpaper frame" error above.'); } return GLib.SOURCE_REMOVE; }); @@ -238,18 +246,19 @@ export class LiveWallpaperManager { } } - _onNewSample(sink) { + _pollForSample() { + if (!this._appsink) + return GLib.SOURCE_REMOVE; + const Gst = this._Gst; - this._newSampleCallCount = (this._newSampleCallCount ?? 0) + 1; - if (this._newSampleCallCount === 1) - this._logger.debug('Live wallpaper: new-sample signal fired for the first time'); + this._pollCount++; + if (this._pollCount === 1) + this._logger.debug('Live wallpaper: appsink polling started'); try { - const sample = sink.pull_sample(); - if (!sample) { - this._logger.debug('Live wallpaper: pull_sample() returned no sample'); - return Gst.FlowReturn.OK; - } + const sample = this._appsink.try_pull_sample(0); + if (!sample) + return GLib.SOURCE_CONTINUE; const buffer = sample.get_buffer(); const structure = sample.get_caps().get_structure(0); @@ -259,7 +268,7 @@ export class LiveWallpaperManager { const [ok, mapInfo] = buffer.map(Gst.MapFlags.READ); if (!ok) { this._logger.debug('Live wallpaper: buffer.map() failed'); - return Gst.FlowReturn.OK; + return GLib.SOURCE_CONTINUE; } try { @@ -273,10 +282,10 @@ export class LiveWallpaperManager { buffer.unmap(mapInfo); } } catch (e) { - this._logger.error(e, 'Failed to process live wallpaper frame (new-sample handler)'); + this._logger.error(e, 'Failed to poll live wallpaper frame'); } - return Gst.FlowReturn.OK; + return GLib.SOURCE_CONTINUE; } _updateFrame(data, width, height) { @@ -431,6 +440,11 @@ export class LiveWallpaperManager { this._noFrameWatchdogId = 0; } + if (this._pollTimeoutId) { + GLib.source_remove(this._pollTimeoutId); + this._pollTimeoutId = 0; + } + if (this._playbin) { const Gst = this._Gst; const bus = this._playbin.get_bus();