Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions packages/core/src/runtime/init.buffering.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { afterEach, expect, it, vi } from "vitest";
import { initSandboxRuntimeModular } from "./init";
import { heldPosters } from "./proxySrc";
import { createMockTimeline, resetRuntimeFixtureDom } from "./runtimeSeekFixture.test-helpers";

function audio(id: string, duration: number, readyState: { value: number; bufferedTo?: number }) {
Expand All @@ -26,12 +27,12 @@ function audio(id: string, duration: number, readyState: { value: number; buffer
return el;
}

function playPodcast(voiceStart = 0) {
function playPodcast(voiceStart = 0, voiceTag = "audio") {
resetRuntimeFixtureDom();
document.body.innerHTML =
'<div data-composition-id="main" data-root="true" data-duration="10">' +
'<audio id="music" data-start="0" data-duration="10" src="/music.m4a"></audio>' +
`<audio id="voice" data-start="${voiceStart}" data-duration="4" src="/voice.m4a"></audio>` +
`<${voiceTag} id="voice" data-start="${voiceStart}" data-duration="4" src="/voice.m4a"></${voiceTag}>` +
"</div>";
const voiceReady: { value: number; bufferedTo?: number } = { value: 1 };
const music = audio("music", 10, { value: 4 });
Expand Down Expand Up @@ -80,6 +81,26 @@ it("a track not ready pauses everything with Buffering, and resumes when it can
expect(states.at(-1)).toEqual({ isPlaying: true, buffering: false });
});

it("the music plays on while a video shows a still across a copy swap", () => {
const { music, voice, states, playFor } = playPodcast(0, "video");
heldPosters.set(voice, { poster: null, since: performance.now() });

playFor(1);
expect(window.__player!.getTime()).toBeGreaterThan(0.9);
expect(music.paused).toBe(false);
expect(states.at(-1)).toEqual({ isPlaying: true, buffering: false });
});

it("a copy that never lands stops standing in, and the film waits for it", () => {
const { music, voice, states, playFor } = playPodcast(0, "video");
heldPosters.set(voice, { poster: null, since: performance.now() - 60_000 });

playFor(1);
expect(window.__player!.getTime()).toBeLessThan(0.1);
expect(music.paused).toBe(true);
expect(states.at(-1)).toEqual({ isPlaying: true, buffering: true });
});

it("holds on the frame a slow tick skips into a clip with no data, after asking it to play", () => {
const { voice, states, playFor, step } = playPodcast(0.5);

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/runtime/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ import {
forgetPositionEdit,
installPositionEditsSeekReapply,
} from "./positionEdits";
import { unheldPoster, unproxiedSrc } from "./proxySrc";
import { stillStandsIn, unheldPoster, unproxiedSrc } from "./proxySrc";
import { applyVariableBindings, unproxiedMediaSrc } from "./applyVariableBindings";
import { createColorGradingRuntime, type RuntimeColorGradingApi } from "./colorGrading";
import { COLOR_GRADING_AUTHORED_OPACITY_ATTR } from "../colorGrading";
Expand Down Expand Up @@ -5694,6 +5694,7 @@ export function initSandboxRuntimeModular(): void {
isInClipWindow(t, start, end) &&
!hasDataAhead(el) &&
isPlayableTrack(el) &&
!stillStandsIn(el) &&
!webAudio.ownsElement(el),
)
.map(({ el }) => el);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/runtime/mediaProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,12 +235,12 @@ function holdShownFrame(el: HTMLMediaElement): void {
if (!isVideoElement(el) || heldPosters.has(el)) return;
const still = stillOf(el);
if (!still) return;
heldPosters.set(el, el.getAttribute("poster"));
heldPosters.set(el, { poster: el.getAttribute("poster"), since: performance.now() });
el.setAttribute("poster", still);
const letGo = () => {
for (const type of RELEASE_EVENTS) el.removeEventListener(type, release);
letGoOfStill.delete(el);
const authored = heldPosters.get(el) ?? null;
const authored = heldPosters.get(el)?.poster ?? null;
heldPosters.delete(el);
if (authored === null) el.removeAttribute("poster");
else el.setAttribute("poster", authored);
Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/runtime/proxySrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ export function unproxiedSrc(el: Element): string | null {
return swappedElements.has(el) ? (swappedElements.get(el) ?? null) : el.getAttribute("src");
}

/** Videos showing a still of their last frame as poster across a copy swap, each with the poster it had before. */
export const heldPosters = new WeakMap<Element, string | null>();
/** Videos showing a still of their last frame as poster across a copy swap: the poster they had before, and when. */
export const heldPosters = new WeakMap<Element, { poster: string | null; since: number }>();

export function unheldPoster(el: Element): string | null {
return heldPosters.has(el) ? (heldPosters.get(el) ?? null) : el.getAttribute("poster");
return heldPosters.has(el) ? (heldPosters.get(el)?.poster ?? null) : el.getAttribute("poster");
}

const STILL_STANDS_IN_MS = 2000;

/** The video shows a still while its new copy loads, so the film need not wait for it. */
export function stillStandsIn(el: Element): boolean {
const held = heldPosters.get(el);
return held !== undefined && performance.now() - held.since < STILL_STANDS_IN_MS;
}
Loading