Skip to content

Commit 7fac2a8

Browse files
committed
fix(cli): honor authored playback rate in snapshots
1 parent af1cb1c commit 7fac2a8

2 files changed

Lines changed: 34 additions & 14 deletions

File tree

packages/cli/src/commands/snapshot.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import snapshotCommand, {
2929
requireSnapshotFfmpeg,
3030
resolveSnapshotVideoClipStart,
3131
resolveSnapshotVideoFrameTime,
32+
resolveSnapshotVideoPlaybackRate,
3233
tailFrameTime,
3334
} from "./snapshot.js";
3435

@@ -262,6 +263,12 @@ describe("resolveSnapshotVideoClipStart", () => {
262263
});
263264
});
264265

266+
describe("resolveSnapshotVideoPlaybackRate", () => {
267+
it("prefers the authored data-playback-rate over the browser default", () => {
268+
expect(resolveSnapshotVideoPlaybackRate({ authoredRate: "1.8", defaultRate: 1 })).toBe(1.8);
269+
});
270+
});
271+
265272
describe("computeSnapshotTimes (FINDING [7]: tail is always captured)", () => {
266273
it("default frames: last point is the readable tail, never exact duration", () => {
267274
const { times, appendedTail } = computeSnapshotTimes(8, { frames: 5 });

packages/cli/src/commands/snapshot.ts

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ export function resolveSnapshotVideoClipStart(input: {
109109
return input.runtimeResolvedStart ?? input.authoredStart;
110110
}
111111

112+
/** Match runtime/render timing: authored data-playback-rate wins over the
113+
* browser default, then the effective rate is clamped to the supported range. */
114+
export function resolveSnapshotVideoPlaybackRate(input: {
115+
authoredRate: string | undefined;
116+
defaultRate: number;
117+
}): number {
118+
const authoredRate = Number.parseFloat(input.authoredRate ?? "");
119+
const rawRate =
120+
Number.isFinite(authoredRate) && authoredRate > 0 ? authoredRate : input.defaultRate;
121+
return Number.isFinite(rawRate) && rawRate > 0 ? Math.max(0.1, Math.min(5, rawRate)) : 1;
122+
}
123+
112124
export function requireSnapshotFfmpeg(ffmpegPath: string | undefined): string {
113125
if (ffmpegPath) return ffmpegPath;
114126
throw new Error(
@@ -425,38 +437,37 @@ async function captureSnapshots(
425437
const v = el as HTMLVideoElement;
426438
const authoredStart = parseFloat(v.dataset.start ?? "0") || 0;
427439
const runtimeResolvedStart = runtimeWindow.__hfResolveMediaStartSeconds?.(v);
428-
const rawRate = v.defaultPlaybackRate;
429-
const playbackRate =
430-
Number.isFinite(rawRate) && rawRate > 0 ? Math.max(0.1, Math.min(5, rawRate)) : 1;
431440
const mediaStart =
432441
parseFloat(v.dataset.playbackStart ?? v.dataset.mediaStart ?? "0") || 0;
433442
const rawDuration = parseFloat(v.dataset.duration ?? "");
434443
const srcDur = Number.isFinite(v.duration) && v.duration > 0 ? v.duration : 0;
435-
const duration =
436-
Number.isFinite(rawDuration) && rawDuration > 0
437-
? rawDuration
438-
: srcDur > 0
439-
? Math.max(0, (srcDur - mediaStart) / playbackRate)
440-
: Number.POSITIVE_INFINITY;
441444
return {
442445
id: v.id,
443446
src: v.currentSrc || v.src,
444447
authoredStart,
448+
authoredRate: v.dataset.playbackRate,
449+
defaultRate: v.defaultPlaybackRate,
445450
runtimeResolvedStart:
446451
runtimeResolvedStart !== undefined && Number.isFinite(runtimeResolvedStart)
447452
? runtimeResolvedStart
448453
: null,
449-
duration,
454+
authoredDuration:
455+
Number.isFinite(rawDuration) && rawDuration > 0 ? rawDuration : null,
450456
srcDuration: srcDur,
451-
playbackRate,
452457
mediaStart,
453458
loop: v.loop,
454459
};
455460
});
456461
});
457462
const active = candidates.flatMap((candidate) => {
458463
const start = resolveSnapshotVideoClipStart(candidate);
459-
let relTime = (time - start) * candidate.playbackRate + candidate.mediaStart;
464+
const playbackRate = resolveSnapshotVideoPlaybackRate(candidate);
465+
const duration =
466+
candidate.authoredDuration ??
467+
(candidate.srcDuration > 0
468+
? Math.max(0, (candidate.srcDuration - candidate.mediaStart) / playbackRate)
469+
: Number.POSITIVE_INFINITY);
470+
let relTime = (time - start) * playbackRate + candidate.mediaStart;
460471
if (
461472
candidate.loop &&
462473
candidate.srcDuration > candidate.mediaStart &&
@@ -470,11 +481,13 @@ async function captureSnapshots(
470481
const frameTime = resolveSnapshotVideoFrameTime({
471482
globalTime: time,
472483
clipStart: start,
473-
clipDuration: candidate.duration,
484+
clipDuration: duration,
474485
relativeTime: relTime,
475486
sourceDuration: candidate.srcDuration,
476487
});
477-
return frameTime === null ? [] : [{ ...candidate, start, relTime: frameTime }];
488+
return frameTime === null
489+
? []
490+
: [{ ...candidate, start, playbackRate, duration, relTime: frameTime }];
478491
});
479492

480493
const updates: Array<{ videoId: string; dataUri: string }> = [];

0 commit comments

Comments
 (0)