From eb749f8c199bde4024f7705ba63e97187bdfc8ea Mon Sep 17 00:00:00 2001 From: sudacode Date: Wed, 30 Sep 2026 01:01:49 -0700 Subject: [PATCH] fix(overlay): force packed float audio for macOS timing review preview - Pass --audio-format=float to the preview mpv on darwin so coreaudio accepts Opus output on Bluetooth devices like AirPods instead of falling back to avfoundation - Fixes the stuttering preview cursor that jumped to the end partway through the selection - Thread the platform into buildMediaTimingPreviewArgs and add a test covering darwin vs linux args --- changes/timing-review-macos-cursor.md | 4 ++++ src/core/services/media-timing-preview.test.ts | 16 ++++++++++++++++ src/core/services/media-timing-preview.ts | 9 ++++++++- 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 changes/timing-review-macos-cursor.md diff --git a/changes/timing-review-macos-cursor.md b/changes/timing-review-macos-cursor.md new file mode 100644 index 00000000..f7a7d370 --- /dev/null +++ b/changes/timing-review-macos-cursor.md @@ -0,0 +1,4 @@ +type: fixed +area: overlay + +- On macOS with Bluetooth headphones such as AirPods, the timing review preview cursor now moves smoothly across the selection instead of stuttering and jumping to the end partway through. This affected Opus audio, including Jellyfin streams. diff --git a/src/core/services/media-timing-preview.test.ts b/src/core/services/media-timing-preview.test.ts index eea91f60..b598ce8c 100644 --- a/src/core/services/media-timing-preview.test.ts +++ b/src/core/services/media-timing-preview.test.ts @@ -39,6 +39,22 @@ describe('buildMediaTimingPreviewArgs', () => { ); }); + test('keeps macOS previews on coreaudio with packed float output', () => { + const options = { mediaPath: '/tmp/window.mkv' }; + + assert.ok( + buildMediaTimingPreviewArgs('/tmp/review.sock', options, 'darwin').includes( + '--audio-format=float', + ), + ); + assert.equal( + buildMediaTimingPreviewArgs('/tmp/review.sock', options, 'linux').some((arg) => + arg.startsWith('--audio-format='), + ), + false, + ); + }); + test('separates an option-like media path without adding optional audio arguments', () => { const args = buildMediaTimingPreviewArgs('/tmp/review.sock', { mediaPath: '--fullscreen', diff --git a/src/core/services/media-timing-preview.ts b/src/core/services/media-timing-preview.ts index e2d2066e..d5f54574 100644 --- a/src/core/services/media-timing-preview.ts +++ b/src/core/services/media-timing-preview.ts @@ -44,6 +44,7 @@ interface MediaTimingPreviewDeps { export function buildMediaTimingPreviewArgs( socketPath: string, options: MediaTimingPreviewStartOptions, + platform: NodeJS.Platform = process.platform, ): string[] { const args = [ '--no-config', @@ -66,6 +67,12 @@ export function buildMediaTimingPreviewArgs( if (options.absoluteTimestamps) { args.push('--rebase-start-time=no'); } + if (platform === 'darwin') { + // coreaudio can reject planar float (what Opus decodes to) on Bluetooth outputs such as + // AirPods, and mpv then falls back to avfoundation. Its 2 s device buffer makes time-pos + // updates sparse and ends the clip well before the audio does, so the preview cursor jumps. + args.push('--audio-format=float'); + } args.push('--', options.mediaPath); return args; } @@ -170,7 +177,7 @@ export class MediaTimingPreviewSession { this.startupError = null; const child = this.deps.spawnProcess( command, - buildMediaTimingPreviewArgs(socketPath, { ...options, mediaPath }), + buildMediaTimingPreviewArgs(socketPath, { ...options, mediaPath }, this.deps.platform), ); this.process = child; child.once('error', (error) => {