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
3 changes: 1 addition & 2 deletions packages/core/src/runtime/init.videoPreseek.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ it.each(["video", "audio"])(
expect(play).not.toHaveBeenCalled();
for (let i = 0; i < 11; i++) step();
if (tag === "video") expect(video.style.visibility).toBe("visible");
// A video is landed once more where its pre-roll plays from.
expect(seek).toHaveBeenCalledTimes(tag === "video" ? 2 : 1);
expect(seek).toHaveBeenCalledTimes(1);
expect(play).toHaveBeenCalledTimes(1);
},
);
Expand Down
10 changes: 2 additions & 8 deletions packages/core/src/runtime/media.preseek.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,8 @@ describe("incoming media preparation", () => {
expect(el.currentTime).toBe(mediaElementTime(8));
expect(el.play).not.toHaveBeenCalled();
tick(3);
if (tag === "video") {
// A video plays from one second before its in-point, so it is landed there once more as it starts.
expect(el.currentTime).toBe(mediaElementTime(7));
expect(seek).toHaveBeenCalledTimes(2);
} else {
tick(4);
expect(seek).toHaveBeenCalledTimes(1);
}
tick(4);
expect(seek).toHaveBeenCalledTimes(1);
expect(el.play).toHaveBeenCalledTimes(1);
},
);
Expand Down
184 changes: 99 additions & 85 deletions packages/core/src/runtime/media.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
readElementPlaybackRate,
readElementPlaybackStart,
refreshRuntimeMediaCache,
resetVideoStartLatency,
resolveRuntimeMediaClipDuration,
syncRuntimeMedia as syncRuntimeMediaWithDuration,
} from "./media";
Expand Down Expand Up @@ -1014,7 +1015,7 @@ describe("syncRuntimeMedia", () => {
expect(clip.el.play).not.toHaveBeenCalled();
});

it("starts no audio early while paused", () => {
it("starts nothing early while paused, and never a video", () => {
const audio = createMockClip({ start: 5, end: 6 }, "audio");
syncRuntimeMedia({
clips: [audio],
Expand All @@ -1023,17 +1024,15 @@ describe("syncRuntimeMedia", () => {
playbackRate: 1,
cueAheadSeconds: 0.017,
});
expect(audio.el.play).not.toHaveBeenCalled();
});

it("starts no video early while paused", () => {
const video = createMockClip({ start: 5, end: 6 }, "video");
syncRuntimeMedia({
clips: [video],
timeSeconds: 4.99,
playing: false,
playing: true,
playbackRate: 1,
cueAheadSeconds: 0.017,
});
expect(audio.el.play).not.toHaveBeenCalled();
expect(video.el.play).not.toHaveBeenCalled();
});

Expand Down Expand Up @@ -2304,106 +2303,121 @@ describe("syncRuntimeMedia", () => {
expect(el.pause).not.toHaveBeenCalled();
});

describe("video pre-roll", () => {
const playing = (clip: RuntimeMediaClip, currentTime: number) => {
describe("start latency", () => {
afterEach(() => resetVideoStartLatency());

// Plays `clip` at `playAt`, then has its element `lag` s behind the playhead at `sampleAt`.
const startWithLag = (
clip: RuntimeMediaClip,
playAt: number,
sampleAt: number,
lag: number,
playheadRunning = true,
) => {
if (playheadRunning)
syncRuntimeMedia({
clips: [],
timeSeconds: playAt,
playing: true,
playbackRate: 1,
getCompositionDuration: () => 30,
});
tick(clip, playAt);
expect(clip.el.play).toHaveBeenCalledTimes(1);
Object.defineProperty(clip.el, "paused", {
value: false,
writable: true,
configurable: true,
});
clip.el.currentTime = currentTime;
clip.el.currentTime = clip.mediaStart + (sampleAt - clip.start) - lag;
tick(clip, sampleAt);
};

it("starts an upcoming video one second before its clip, from one second before its first frame", () => {
const clip = videoClip({ start: 10, end: 20, mediaStart: 5 });
tick(clip, 8.9);
it("does not start an upcoming video early before any start is measured", () => {
const clip = videoClip({ start: 10, end: 20, mediaStart: 0 });
tick(clip, 9.99);
expect(clip.el.play).not.toHaveBeenCalled();
tick(clip, 9);
expect(clip.el.play).toHaveBeenCalledTimes(1);
expect(clip.el.currentTime).toBe(mediaElementTime(4));
});

it("starts a video with less than a second of source before its first frame from the start of the file", () => {
const clip = videoClip({ start: 10, end: 20, mediaStart: 0.3 });
tick(clip, 9.5);
expect(clip.el.play).not.toHaveBeenCalled();
tick(clip, 9.75);
expect(clip.el.play).toHaveBeenCalledTimes(1);
it("starts an upcoming video early by the start latency measured on earlier ones", () => {
startWithLag(videoClip({ start: 0, end: 10, mediaStart: 0 }), 0, 0.3, 0.06);
const due50ms = videoClip({ start: 10, end: 20, mediaStart: 0 });
tick(due50ms, 9.95);
expect(due50ms.el.play).toHaveBeenCalledTimes(1);
const due70ms = videoClip({ start: 10, end: 20, mediaStart: 0 });
tick(due70ms, 9.93);
expect(due70ms.el.play).not.toHaveBeenCalled();
});

it("starts a video that plays from the very start of its file 50 ms early, on its first frame", () => {
const clip = videoClip({ start: 10, end: 20, mediaStart: 0 });
tick(clip, 9.9);
expect(clip.el.play).not.toHaveBeenCalled();
tick(clip, 9.96);
expect(clip.el.play).toHaveBeenCalledTimes(1);
it("starts an upcoming video on the tick nearest its lead", () => {
startWithLag(videoClip({ start: 0, end: 10, mediaStart: 0 }), 0, 0.3, 0.06);
const due75ms = videoClip({ start: 10, end: 20, mediaStart: 0 });
// Now is 15 ms earlier than the 60 ms lead; the next tick, 40 ms on, would be 25 ms late.
syncRuntimeMedia({
clips: [due75ms],
timeSeconds: 9.925,
playing: true,
playbackRate: 1,
cueAheadSeconds: 0.04,
getCompositionDuration: () => 30,
});
expect(due75ms.el.play).toHaveBeenCalledTimes(1);
});

it.each([0, 0.04])(
"plays a from-file-start clip that starts in %d s through its cut without pausing it",
(startLatency) => {
const clip = videoClip({ start: 10, end: 20, mediaStart: 0 });
clip.el.pause = vi.fn();
let playedAt: number | null = null;
clip.el.play = vi.fn(() => {
playedAt = lastTick;
Object.defineProperty(clip.el, "paused", {
value: false,
writable: true,
configurable: true,
});
return Promise.resolve();
});
let lastTick = 9.9;
for (let t = 9.9; t < 10.3; t += 1 / 60) {
if (playedAt !== null && t - playedAt >= startLatency)
clip.el.currentTime += (1 / 60) * clip.el.playbackRate;
tick(clip, t);
lastTick = t;
}
expect(clip.el.play).toHaveBeenCalledTimes(1);
expect(clip.el.pause).not.toHaveBeenCalled();
},
);
it("learns half the start delay of a video started by Play", () => {
startWithLag(videoClip({ start: 0, end: 10, mediaStart: 0 }), 0, 0.3, 0.074, false);
const due45ms = videoClip({ start: 10, end: 20, mediaStart: 0 });
tick(due45ms, 9.955);
expect(due45ms.el.play).not.toHaveBeenCalled();
const due35ms = videoClip({ start: 10, end: 20, mediaStart: 0 });
tick(due35ms, 9.965);
expect(due35ms.el.play).toHaveBeenCalledTimes(1);
});

it("slows a video that started ahead on its first frame, hidden, instead of holding it", () => {
const clip = videoClip({ start: 10, end: 20, mediaStart: 0 });
tick(clip, 9.96);
playing(clip, 0.1);
clip.el.pause = vi.fn();
tick(clip, 9.97);
expect(clip.el.pause).not.toHaveBeenCalled();
expect(clip.el.playbackRate).toBeLessThan(1);
it("learns from a video seeked and started in the same tick", () => {
const seeked = videoClip({ start: 0, end: 10, mediaStart: 0 });
seeked.el.currentTime = 0;
startWithLag(seeked, 5, 5.3, 0.06);
const due25ms = videoClip({ start: 10, end: 20, mediaStart: 0 });
tick(due25ms, 9.975);
expect(due25ms.el.play).toHaveBeenCalledTimes(1);
});

it("runs a pre-rolled video that is late up to twice as fast, hidden", () => {
const clip = videoClip({ start: 10, end: 20, mediaStart: 5 });
tick(clip, 9.5);
playing(clip, 4.4); // its target is 4.5
tick(clip, 9.5);
expect(clip.el.playbackRate).toBeCloseTo(1.5, 9);
playing(clip, 4);
tick(clip, 9.5);
expect(clip.el.playbackRate).toBeCloseTo(2, 9);
it("keeps its lead when an early start lands on time", () => {
startWithLag(videoClip({ start: 0, end: 10, mediaStart: 0 }), 0, 0.3, 0.06);
startWithLag(videoClip({ start: 10, end: 20, mediaStart: 0 }), 9.945, 10.3, 0.005);
const due58ms = videoClip({ start: 20, end: 30, mediaStart: 0 });
tick(due58ms, 19.942);
expect(due58ms.el.play).toHaveBeenCalledTimes(1);
});

it("runs a pre-rolled video that is ahead at half speed instead of holding it", () => {
const clip = videoClip({ start: 10, end: 20, mediaStart: 5 });
tick(clip, 9.5);
playing(clip, 4.9);
clip.el.pause = vi.fn();
tick(clip, 9.5);
expect(clip.el.pause).not.toHaveBeenCalled();
expect(clip.el.playbackRate).toBeCloseTo(0.5, 9);
it("caps the learned start delay at 100 ms", () => {
startWithLag(videoClip({ start: 0, end: 10, mediaStart: 0 }), 0, 0.3, 0.3);
const due150ms = videoClip({ start: 10, end: 20, mediaStart: 0 });
tick(due150ms, 9.85);
expect(due150ms.el.play).not.toHaveBeenCalled();
});

it("steers a video gently once its clip is visible", () => {
const clip = videoClip({ start: 10, end: 20, mediaStart: 5 });
tick(clip, 9.5);
playing(clip, 4.9);
tick(clip, 10);
tick(clip, 10);
expect(clip.el.playbackRate).toBeCloseTo(1.03, 9);
it("does not learn from a looping video sampled across a lap wrap", () => {
startWithLag(videoClip({ start: 0, end: 10, mediaStart: 0 }), 0, 0.3, 0.06);
const loop = videoClip({
start: 10,
end: 20,
mediaStart: 0,
loop: true,
sourceDuration: 0.2,
});
tick(loop, 10);
Object.defineProperty(loop.el, "paused", {
value: false,
writable: true,
configurable: true,
});
loop.el.currentTime = 0.19; // the playhead has wrapped to 0.06, the element not yet
tick(loop, 10.26);
const due50ms = videoClip({ start: 30, end: 40, mediaStart: 0 });
tick(due50ms, 29.95);
expect(due50ms.el.play).toHaveBeenCalledTimes(1);
});
});
});
Expand Down
Loading
Loading