Skip to content

Commit a497310

Browse files
Checkpoint current playback recovery work
1 parent d1466a9 commit a497310

7 files changed

Lines changed: 201 additions & 46 deletions

File tree

src/autoPause.js

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
import GObject from 'gi://GObject';
19+
import GLib from 'gi://GLib';
1920
import Meta from 'gi://Meta';
2021
import * as Config from 'resource:///org/gnome/shell/misc/config.js';
2122

@@ -122,6 +123,20 @@ const PauseOnMaximizeOrFullscreenModule = GObject.registerClass(
122123
this._windows = []; // [{metaWindow, signals: [...]}, ...]
123124
this._windowAddedId = null;
124125
this._windowRemovedId = null;
126+
// Coalesce bursts of window-state notifies (a single user
127+
// (un)maximize fires 2-4 notify signals back-to-back) into one
128+
// _update() per idle tick.
129+
this._pendingUpdateId = 0;
130+
}
131+
132+
_scheduleUpdate() {
133+
if (this._pendingUpdateId)
134+
return;
135+
this._pendingUpdateId = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
136+
this._pendingUpdateId = 0;
137+
this._update();
138+
return GLib.SOURCE_REMOVE;
139+
});
125140
}
126141

127142
enable() {
@@ -146,23 +161,19 @@ const PauseOnMaximizeOrFullscreenModule = GObject.registerClass(
146161
let signals = [];
147162
signals.push(
148163
metaWindow.connect('notify::maximized-horizontally', () => {
149-
this._logger.debug('maximized-horizontally changed');
150-
this._update();
164+
this._scheduleUpdate();
151165
}));
152166
signals.push(
153167
metaWindow.connect('notify::maximized-vertically', () => {
154-
this._logger.debug('maximized-vertically changed');
155-
this._update();
168+
this._scheduleUpdate();
156169
}));
157170
signals.push(
158171
metaWindow.connect('notify::fullscreen', () => {
159-
this._logger.debug('fullscreen changed');
160-
this._update();
172+
this._scheduleUpdate();
161173
}));
162174
signals.push(
163175
metaWindow.connect('notify::minimized', () => {
164-
this._logger.debug('minimized changed');
165-
this._update();
176+
this._scheduleUpdate();
166177
})
167178
);
168179
this._windows.push(
@@ -274,6 +285,10 @@ const PauseOnMaximizeOrFullscreenModule = GObject.registerClass(
274285
}
275286

276287
disable() {
288+
if (this._pendingUpdateId) {
289+
GLib.source_remove(this._pendingUpdateId);
290+
this._pendingUpdateId = 0;
291+
}
277292
this._workspaceManager?.disconnect(this._activeWorkspaceChangedId);
278293
this._windows.forEach(({metaWindow, signals}) => {
279294
signals.forEach(signal => metaWindow.disconnect(signal));

src/extension.js

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,20 @@ export default class HanabiExtension extends Extension {
120120
// handler would immediately pause it.)
121121
this.launchRenderer();
122122

123+
// Handle monitor configuration changes (connect/disconnect)
124+
this._monitorsChangedId = Main.layoutManager.connect('monitors-changed', () => {
125+
this._onMonitorsChanged();
126+
});
127+
123128
this.playbackState.suppressMismatch = true;
124129
this._autoPauseDelayId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 5000, () => {
125130
this._autoPauseDelayId = null;
126131
this.playbackState.suppressMismatch = false;
132+
127133
if (this.isEnabled)
128134
this.autoPause.enable();
135+
136+
this.playbackState.sync();
129137
return false;
130138
});
131139

@@ -135,6 +143,56 @@ export default class HanabiExtension extends Extension {
135143
this._setupSleepWatch();
136144
}
137145

146+
_onMonitorsChanged() {
147+
if (!this.isEnabled || this._isSuspending)
148+
return;
149+
150+
// Debounce rapid monitor changes
151+
if (this._monitorChangeTimeoutId) {
152+
GLib.source_remove(this._monitorChangeTimeoutId);
153+
this._monitorChangeTimeoutId = null;
154+
}
155+
156+
this._monitorChangeTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
157+
this._monitorChangeTimeoutId = null;
158+
159+
if (!this.isEnabled || this._isSuspending)
160+
return false;
161+
162+
// Disable autoPause during transition
163+
this.autoPause.disable();
164+
165+
// Cancel any pending autoPause delay
166+
if (this._autoPauseDelayId) {
167+
GLib.source_remove(this._autoPauseDelayId);
168+
this._autoPauseDelayId = null;
169+
}
170+
171+
// Reset playback state for fresh start
172+
this.playbackState.reset();
173+
174+
// Restart renderer for new monitor configuration
175+
this.killCurrentProcess();
176+
177+
// If no process was running and no relaunch is pending, launch directly
178+
if (!this.currentProcess && !this.launchRendererId)
179+
this.launchRenderer();
180+
181+
// Suppress mismatch and re-enable autoPause after renderer stabilizes
182+
this.playbackState.suppressMismatch = true;
183+
this._autoPauseDelayId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 5000, () => {
184+
this._autoPauseDelayId = null;
185+
this.playbackState.suppressMismatch = false;
186+
if (this.isEnabled && !this._isSuspending)
187+
this.autoPause.enable();
188+
this.playbackState.sync();
189+
return false;
190+
});
191+
192+
return false;
193+
});
194+
}
195+
138196
_setupSleepWatch() {
139197
if (this._sleepWatchId)
140198
return;
@@ -175,19 +233,16 @@ export default class HanabiExtension extends Extension {
175233
}
176234
this._cancelRendererWait();
177235

178-
// Suppress the mismatch handler so the renderer isn't
179-
// force-paused while starting up.
180-
this.playbackState.suppressMismatch = true;
181-
182-
// After 5 seconds, force-sync: directly tell renderer to
183-
// play via D-Bus (bypassing state machine which has no
184-
// transition from 'playing'), then re-enable autoPause.
185-
this._autoPauseDelayId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 5000, () => {
236+
// Small grace period for the renderer process to come
237+
// up and export its D-Bus interface before we start
238+
// sending it commands. This is just a launch race
239+
// guard, not a "wait for GStreamer to stabilize" hack.
240+
// The actual post-wake play/pause loop is prevented
241+
// by the mismatch-handler and autoPause changes in
242+
// playbackState.js (see comments there).
243+
const POST_WAKE_GRACE_MS = 1500;
244+
this._autoPauseDelayId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, POST_WAKE_GRACE_MS, () => {
186245
this._autoPauseDelayId = null;
187-
this.playbackState.suppressMismatch = false;
188-
// Force renderer to play - reset() set state to 'playing'
189-
// but never sent a D-Bus command.
190-
this.playbackState._renderer.setPlay();
191246
if (this.isEnabled && !this._isSuspending)
192247
this.autoPause.enable();
193248
return false;
@@ -196,7 +251,7 @@ export default class HanabiExtension extends Extension {
196251
}
197252
});
198253
} catch (e) {
199-
Logger.Logger.prototype.warn?.call(null, `Failed to setup sleep watch: ${e}`);
254+
console.warn(`Failed to setup sleep watch: ${e}`);
200255
}
201256
}
202257

@@ -317,6 +372,21 @@ export default class HanabiExtension extends Extension {
317372
this._autoPauseDelayId = null;
318373
}
319374

375+
// Cancel any pending post-wake mismatch-resume timer.
376+
if (this._mismatchResumeId) {
377+
GLib.source_remove(this._mismatchResumeId);
378+
this._mismatchResumeId = 0;
379+
}
380+
381+
if (this._monitorsChangedId) {
382+
Main.layoutManager.disconnect(this._monitorsChangedId);
383+
this._monitorsChangedId = null;
384+
}
385+
if (this._monitorChangeTimeoutId) {
386+
GLib.source_remove(this._monitorChangeTimeoutId);
387+
this._monitorChangeTimeoutId = null;
388+
}
389+
320390
this._cancelRendererWait();
321391
this._teardownSleepWatch();
322392
this._isSuspending = false;

src/playbackState.js

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,19 @@
1515
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1616
*/
1717

18+
import GLib from 'gi://GLib';
19+
1820
import * as DBus from './dbus.js';
1921
import * as Logger from './logger.js';
2022

23+
// Debounce window for the renderer/state-machine mismatch corrector.
24+
// GStreamer can rapidly oscillate between PAUSED and PLAYING while a hardware
25+
// decoder pipeline is warming up (especially right after suspend/resume). If
26+
// we react to every isPlayingChanged immediately, our forced setPlay/setPause
27+
// calls amplify that oscillation and the video can stay frozen for tens of
28+
// seconds. Only act on a mismatch that has remained stable for this long.
29+
const MISMATCH_DEBOUNCE_MS = 1500;
30+
2131

2232
/**
2333
* Ref: https://kentcdodds.com/blog/implementing-a-simple-state-machine-library-in-javascript
@@ -57,6 +67,8 @@ export class PlaybackState {
5767
// Used during startup and wake-from-sleep to let the renderer start
5868
// playing without being immediately force-paused.
5969
this.suppressMismatch = false;
70+
// GLib source id for the debounced mismatch corrector (0 = none).
71+
this._mismatchTimeoutId = 0;
6072
this._machineDefinition = {
6173
initialState: 'playing',
6274
playing: {
@@ -154,26 +166,28 @@ export class PlaybackState {
154166
},
155167
},
156168
};
157-
this._renderer.proxy.connectSignal(
158-
'isPlayingChanged',
159-
(_proxy, _sender, [isPlaying]) => {
160-
if (this.suppressMismatch) {
161-
this._logger.debug('isPlayingChanged mismatch check suppressed');
162-
return;
163-
}
164-
if (isPlaying && this.getCurrentState() !== 'playing') {
165-
// The renderer is playing the media but the current playback state isn't 'playing'
166-
// This discrepancy can happen when the shell reload, renderer process reload,
167-
// or when the user restarts the playback (e.g. select another video file in prefs).
168-
// Pause the renderer if that's the case.
169-
this._renderer.setPause();
170-
}
171-
}
172-
);
169+
// Note: we do NOT install an `isPlayingChanged` corrector that fights
170+
// the renderer when its reported state diverges from ours. The signal
171+
// is reported by GStreamer's state-changed callback in the renderer,
172+
// which can fire transient PAUSED reports while the pipeline is
173+
// mid-recovery (very common after suspend/resume with hardware video
174+
// decode). Any auto-correction here re-sends setPlay()/setPause() into
175+
// a recovering pipeline on a fixed cadence and creates a self-
176+
// sustaining play/pause loop. The state machine here is the single
177+
// source of truth for what we WANT; what the renderer momentarily
178+
// REPORTS is best-effort and only used for UI labels.
173179
// Initialize
174180
this.reset();
175181
}
176182

183+
sync() {
184+
if (this.getCurrentState() === 'playing') {
185+
this._renderer.setPlay();
186+
} else {
187+
this._renderer.setPause();
188+
}
189+
}
190+
177191
getCurrentState() {
178192
return this._machine.value;
179193
}

src/renderer/renderer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ let changeWallpaperMode = extSettings ? extSettings.get_int('change-wallpaper-mo
107107
let changeWallpaperInterval = extSettings ? extSettings.get_int('change-wallpaper-interval') : 15;
108108
let windowDimension = {width: 1920, height: 1080};
109109
let windowed = false;
110-
let fullscreened = true;
110+
let fullscreened = false;
111111
let isDebugMode = extSettings ? extSettings.get_boolean('debug-mode') : true;
112112
let changeWallpaperTimerId = null;
113113
let fadeDuration = extSettings ? extSettings.get_int('fade-duration') : 500;

src/roundedCornersEffect.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616
* along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
1818

19+
import Cogl from 'gi://Cogl';
1920
import GObject from 'gi://GObject';
2021
import Shell from 'gi://Shell';
2122

2223
import * as Logger from './logger.js';
2324

25+
// Shell.SnippetHook was removed in GNOME Shell 47+, use Cogl as fallback
26+
const SnippetHook = Shell.SnippetHook ?? Cogl.SnippetHook;
27+
2428
const logger = new Logger.Logger('roundedCorners');
2529

2630
// This shader is copied from Mutter project.
@@ -86,7 +90,7 @@ export const RoundedCornersEffect = GObject.registerClass(
8690
class RoundedCornersEffect extends Shell.GLSLEffect {
8791
vfunc_build_pipeline() {
8892
this.add_glsl_snippet(
89-
Shell.SnippetHook.FRAGMENT,
93+
SnippetHook.FRAGMENT,
9094
fragmentShaderDeclarations,
9195
fragmentShaderCode,
9296
false

0 commit comments

Comments
 (0)