From 94061675d371f15809044c2e5085f25bfab9771e Mon Sep 17 00:00:00 2001 From: Alec Watts Date: Mon, 26 Oct 2026 01:03:33 -0700 Subject: [PATCH 1/2] fix(media): bound paused audio lookahead with a live context --- packages/media/src/audio-iterator-manager.ts | 11 ++- packages/media/src/media-player.ts | 1 + .../src/test/audio-anchor-reschedule.test.tsx | 78 +++++++++++++++++++ .../media/src/test/audio-iterator.test.ts | 1 + .../src/test/audio-seek-tolerance.test.ts | 1 + .../video-with-no-frames-in-beginning.test.ts | 1 + 6 files changed, 92 insertions(+), 1 deletion(-) diff --git a/packages/media/src/audio-iterator-manager.ts b/packages/media/src/audio-iterator-manager.ts index 98d16ee50f9..52ae7d20ca8 100644 --- a/packages/media/src/audio-iterator-manager.ts +++ b/packages/media/src/audio-iterator-manager.ts @@ -76,6 +76,7 @@ export const audioIteratorManager = ({ getSequenceDurationInSeconds, getMediaEndTimestamp, getStartTime, + getIsPlaying, initialMuted, initialVolume, toneFrequency, @@ -89,6 +90,7 @@ export const audioIteratorManager = ({ getSequenceDurationInSeconds: () => number; getMediaEndTimestamp: () => number; getStartTime: () => number; + getIsPlaying: () => boolean; initialMuted: boolean; initialVolume: number; toneFrequency: number; @@ -321,7 +323,14 @@ export const audioIteratorManager = ({ sequenceStartTime: getStartTime(), }); - return scheduledTime - getAudioContextCurrentTimeMockedInTest(); + // A keep-alive context advances while paused, but queued nodes cannot end. + // Freeze lookahead at iterator creation until playback advances. + return ( + scheduledTime - + (getIsPlaying() + ? getAudioContextCurrentTimeMockedInTest() + : currentTime) + ); }, fn: () => iterator.getNextFn(), onDone: (result, next) => { diff --git a/packages/media/src/media-player.ts b/packages/media/src/media-player.ts index 2169ffc81bd..3d87a843a47 100644 --- a/packages/media/src/media-player.ts +++ b/packages/media/src/media-player.ts @@ -453,6 +453,7 @@ export class MediaPlayer { getMediaEndTimestamp: () => this.getMediaEndTimestamp(), getSequenceEndTimestamp: () => this.getSequenceEndTimestamp(), getStartTime: () => this.getStartTime(), + getIsPlaying: () => this.playing, initialMuted, initialVolume, toneFrequency: this.toneFrequency, diff --git a/packages/media/src/test/audio-anchor-reschedule.test.tsx b/packages/media/src/test/audio-anchor-reschedule.test.tsx index d3b4b630a0b..dc236e10173 100644 --- a/packages/media/src/test/audio-anchor-reschedule.test.tsx +++ b/packages/media/src/test/audio-anchor-reschedule.test.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {createRoot} from 'react-dom/client'; import {Internals} from 'remotion'; import {expect, test, vi} from 'vitest'; +import {page} from 'vitest/browser'; import {Audio} from '../audio/audio'; test('replaces queued audio after a suspension re-anchors a stationary Player', async () => { @@ -172,3 +173,80 @@ test('paused seeks unschedule unstarted nodes while playing cleanup still stops vi.restoreAllMocks(); } }); + +test('keep-alive does not decode beyond the lookahead of a paused Player', async () => { + const nodes: AudioBufferSourceNode[] = []; + const originalCreateBufferSource = AudioContext.prototype.createBufferSource; + const createBufferSourceSpy = vi + .spyOn(AudioContext.prototype, 'createBufferSource') + .mockImplementation(function (this: AudioContext) { + const node = originalCreateBufferSource.call(this); + nodes.push(node); + return node; + }); + let audioContext: AudioContext | null = null; + const Composition: React.FC = () => { + audioContext = + React.useContext(Internals.SharedAudioContext)?.audioContext ?? null; + return