Skip to content

Commit dc420a0

Browse files
fix(producer): advance a sped-up clip's file clock by the source time it played
When opacity raises an auto-start clip's start, the encoder's in-point moved by the timeline delta, as if the clip plays at 1x. Integrate the clip's rate over that delta instead, and shift a rate lane to the new start.
1 parent f87a271 commit dc420a0

2 files changed

Lines changed: 61 additions & 1 deletion

File tree

‎packages/producer/src/services/render/stages/probeStage.test.ts‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { collectRenderMedia } from "../../renderMediaCollector.js";
1414
const workDir = mkdtempSync(join(tmpdir(), "hf-stage-test-"));
1515
afterAll(() => rmSync(workDir, { recursive: true, force: true }));
1616
import { createHash } from "node:crypto";
17+
import { shiftRateLane, sourceTimeAt } from "@hyperframes/core";
1718
import {
1819
hasAutoStartVideos,
1920
hasScriptedAudioVolumeAutomation,
@@ -981,6 +982,62 @@ describe("runProbeStage — forceScreenshot threading", () => {
981982
);
982983
});
983984

985+
it("shifts auto-start mediaStart by the source time a sped-up clip played", async () => {
986+
resetRetryMocks();
987+
visibilityWindows = [{ videoId: "clip", visibleStart: 3.5, visibleEnd: 6 }];
988+
const { runProbeStage } = await import("./probeStage.js");
989+
const input = makeProbeInput({});
990+
input.composition.videos.push({
991+
id: "clip",
992+
src: "runtime.mp4",
993+
start: 2,
994+
end: 6,
995+
mediaStart: 0,
996+
playbackRate: 2,
997+
loop: false,
998+
hasAudio: false,
999+
});
1000+
input.compiled.html = `<video id="clip" src="runtime.mp4" data-hf-auto-start=""></video>`;
1001+
1002+
await runProbeStage(input);
1003+
1004+
expect(input.composition.videos[0]).toEqual(
1005+
expect.objectContaining({ id: "clip", start: 3.5, end: 6, mediaStart: 3, playbackRate: 2 }),
1006+
);
1007+
});
1008+
1009+
it("shifts a rate lane to the raised start", async () => {
1010+
resetRetryMocks();
1011+
visibilityWindows = [{ videoId: "clip", visibleStart: 3, visibleEnd: 6 }];
1012+
const { runProbeStage } = await import("./probeStage.js");
1013+
const input = makeProbeInput({});
1014+
const lane = {
1015+
target: "rate",
1016+
points: [
1017+
{ t: 0, v: 1 },
1018+
{ t: 2, v: 3 },
1019+
],
1020+
} satisfies RateSpec;
1021+
input.composition.videos.push({
1022+
id: "clip",
1023+
src: "runtime.mp4",
1024+
start: 2,
1025+
end: 6,
1026+
mediaStart: 0,
1027+
playbackRate: lane,
1028+
loop: false,
1029+
hasAudio: false,
1030+
});
1031+
input.compiled.html = `<video id="clip" src="runtime.mp4" data-hf-auto-start=""></video>`;
1032+
1033+
await runProbeStage(input);
1034+
1035+
const video = input.composition.videos[0]!;
1036+
expect(video.start).toBe(3);
1037+
expect(video.mediaStart).toBeCloseTo(sourceTimeAt(lane, 1), 9);
1038+
expect(video.playbackRate).toEqual(shiftRateLane(lane, 1));
1039+
});
1040+
9841041
it("leaves mediaStart at 0 when visibility raises start on an empty-src video", async () => {
9851042
resetRetryMocks();
9861043
visibilityWindows = [{ videoId: "clip", visibleStart: 3.5, visibleEnd: 6 }];

‎packages/producer/src/services/render/stages/probeStage.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -800,7 +800,10 @@ export async function runProbeStage(input: ProbeStageInput): Promise<ProbeStageR
800800
video.end = win.visibleEnd;
801801
const raised = video.start - prevStart;
802802
if (raised > 0 && video.mediaStart <= 0 && hostClockIds.has(video.id)) {
803-
video.mediaStart += raised;
803+
const rate = normalizeRateSpec(video.playbackRate);
804+
video.mediaStart += sourceTimeAt(rate, raised);
805+
if (video.playbackRate !== undefined)
806+
video.playbackRate = shiftRateLane(rate, raised);
804807
}
805808
const duration = video.end - prevStart;
806809
if (duration > 0) {

0 commit comments

Comments
 (0)