diff --git a/packages/core/src/runtime/mediaVolumeEnvelope.test.ts b/packages/core/src/runtime/mediaVolumeEnvelope.test.ts new file mode 100644 index 000000000..a7b0350c5 --- /dev/null +++ b/packages/core/src/runtime/mediaVolumeEnvelope.test.ts @@ -0,0 +1,31 @@ +/** @vitest-environment jsdom */ +import { describe, expect, it } from "vitest"; +import { probeAndCacheElementVolume } from "./mediaVolumeEnvelope"; + +describe("probeAndCacheElementVolume", () => { + it("restores the timeline playhead after sampling volume automation", () => { + const audio = document.createElement("audio"); + audio.dataset.volume = "1"; + document.body.append(audio); + + let playhead = 0.75; + const timeline = { + totalTime(next?: number) { + if (next !== undefined) { + playhead = next; + audio.volume = next >= 1 ? 0 : 1; + } + return playhead; + }, + }; + const cache = new WeakMap(); + + probeAndCacheElementVolume(audio, timeline, 1, cache); + + expect(playhead).toBe(0.75); + expect(audio.volume).toBe(1); + expect(cache.get(audio)).toEqual( + expect.arrayContaining([expect.objectContaining({ volume: 0 })]), + ); + }); +}); diff --git a/packages/core/src/runtime/mediaVolumeEnvelope.ts b/packages/core/src/runtime/mediaVolumeEnvelope.ts index 69d55ea0c..00e0094d7 100644 --- a/packages/core/src/runtime/mediaVolumeEnvelope.ts +++ b/packages/core/src/runtime/mediaVolumeEnvelope.ts @@ -126,8 +126,8 @@ export function probeElementVolumeKeyframes( } export interface RuntimeTimelineRef { - totalTime?: ((t: number, suppressEvents?: boolean) => unknown) | undefined; - seek?: ((t: number, suppressEvents?: boolean) => unknown) | undefined; + totalTime?: ((t?: number, suppressEvents?: boolean) => unknown) | undefined; + seek?: ((t?: number, suppressEvents?: boolean) => unknown) | undefined; } /** @@ -155,8 +155,17 @@ export function probeAndCacheElementVolume( // ignore seek failures during probe } }; - + // Sampling seeks the live timeline through the entire composition. Preserve + // its playhead so the probe cannot perturb the first rendered frame (or any + // user scrub in the preview). + const originalTime = + typeof timeline.totalTime === "function" + ? Number(timeline.totalTime()) + : typeof timeline.seek === "function" + ? Number(timeline.seek()) + : 0; const keyframes = probeElementVolumeKeyframes(mediaEl, seekFn, compositionDuration, 60); + if (Number.isFinite(originalTime)) seekFn(originalTime); if (keyframes) { cache.set(mediaEl, keyframes); }