Skip to content

Commit 1cdc271

Browse files
committed
perf(producer): distributed chunks download only the videos they show
A project video under compiled/ that chunks extract their own frames from now goes only to the chunks that show it, like video-sources/ files. A chunk skips the readiness wait for a deferred video it does not show, so it no longer logs media_load_failed for a file it was never given.
1 parent cc9e1bb commit 1cdc271

4 files changed

Lines changed: 159 additions & 22 deletions

File tree

‎packages/producer/src/services/distributed/planV2.test.ts‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -450,20 +450,43 @@ describe("Plan v2 manifest", () => {
450450
);
451451
});
452452

453-
it("keeps a deferred source the page itself loads in every chunk", () => {
453+
it("ships each compiled/ video source only to the chunks that show it", () => {
454454
const root = tempPath("hf-plan-v2-deferred-compiled-source-");
455455
const v1 = createV1Plan(root, {
456456
video: true,
457457
videoStart: 1 / 30,
458458
deferredSource: "compiled/hero.mp4",
459459
});
460+
// A second clip shown in both chunks, also shipped from compiled/.
461+
const videosPath = join(v1, "meta", "videos.json");
462+
const videos = JSON.parse(readFileSync(videosPath, "utf-8")) as PlanVideosJson;
463+
const hero = videos.extracted[0]!;
464+
videos.videos.push({ ...videos.videos[0]!, id: "long", src: "long.mp4", start: 0 });
465+
videos.extracted.push({
466+
...hero,
467+
videoId: "long",
468+
deferredRange: { ...hero.deferredRange!, sourcePath: "compiled/long.mp4" },
469+
});
470+
writeFileSync(videosPath, JSON.stringify(videos));
471+
writeFileSync(join(v1, "compiled", "long.mp4"), "long source video");
472+
refreshV1PlanHash(v1);
460473
const manifest = readPlanV2Manifest(createPlanV2FromV1(v1, join(root, "v2")).planDir);
474+
const chunksOf = (path: string) =>
475+
manifest.artifacts.find((artifact) => artifact.path === path)?.chunks;
461476

462477
expect(manifest.limitations.videoDependencyMode).toBe("source-extract");
463-
for (const chunkIndex of [0, 1]) {
464-
const artifacts = listPlanV2ArtifactsForTarget(manifest, { role: "chunk", chunkIndex });
465-
expect(artifacts.some((artifact) => artifact.path === "compiled/hero.mp4")).toBe(true);
466-
}
478+
expect(chunksOf("compiled/hero.mp4")).toEqual([1]);
479+
expect(chunksOf("compiled/long.mp4")).toEqual([0, 1]);
480+
expect(chunksOf("compiled/index.html")).toBe("all");
481+
expect(chunksOf("compiled/asset.txt")).toBe("all");
482+
483+
const chunk0Dir = join(root, "chunk-0");
484+
materializePlanV2Target(join(root, "v2"), { role: "chunk", chunkIndex: 0 }, chunk0Dir);
485+
expect(existsSync(join(chunk0Dir, "compiled", "hero.mp4"))).toBe(false);
486+
expect(existsSync(join(chunk0Dir, "compiled", "long.mp4"))).toBe(true);
487+
expect(validatePlanV2MaterializedTarget(chunk0Dir, { role: "chunk", chunkIndex: 0 })).not.toBe(
488+
null,
489+
);
467490
});
468491

469492
it("rejects a deferred video source outside the plan directory", () => {

‎packages/producer/src/services/distributed/planV2.ts‎

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -288,21 +288,25 @@ function artifactTargets(
288288
if (path === "plan.json" || path === "meta/chunks.json" || path === "meta/encoder.json") {
289289
return { chunks: "all", assembler: true };
290290
}
291-
// Each chunk's page loads every <video> src at init (a missing one fails the
292-
// chunk), so deferred sources under compiled/ still go to every chunk.
291+
// A compiled/ file in the map is a deferred video source. Chunks inject its
292+
// frames and skip its <video> readiness, so only chunks that show it get it.
293293
if (
294-
path === "meta/composition.json" ||
295-
path === "meta/videos.json" ||
296-
path.startsWith("compiled/")
294+
path.startsWith("video-frames/") ||
295+
path.startsWith("video-sources/") ||
296+
(path.startsWith("compiled/") && videoDependencies?.has(path))
297297
) {
298-
return { chunks: "all", assembler: false };
299-
}
300-
if (path.startsWith("video-frames/") || path.startsWith("video-sources/")) {
301298
return {
302299
chunks: videoDependencies === null ? "all" : (videoDependencies.get(path) ?? []),
303300
assembler: false,
304301
};
305302
}
303+
if (
304+
path === "meta/composition.json" ||
305+
path === "meta/videos.json" ||
306+
path.startsWith("compiled/")
307+
) {
308+
return { chunks: "all", assembler: false };
309+
}
306310
// Unknown future execution files go to both roles. Over-including is safe;
307311
// silently omitting a new execution dependency is not.
308312
return { chunks: "all", assembler: true };

‎packages/producer/src/services/distributed/renderChunk.test.ts‎

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,24 @@
2020

2121
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
2222
import { spawnSync } from "node:child_process";
23-
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
23+
import {
24+
copyFileSync,
25+
cpSync,
26+
mkdirSync,
27+
mkdtempSync,
28+
readFileSync,
29+
readdirSync,
30+
rmSync,
31+
writeFileSync,
32+
} from "node:fs";
2433
import { tmpdir } from "node:os";
2534
import { join } from "node:path";
2635
import { type CaptureOptions, extractVideoFramesRange } from "@hyperframes/engine";
2736
import { HOST_CHROME_FAILURE_PATTERNS } from "./__test_utils__/hostChromeFailures.js";
37+
import { defaultLogger } from "../../logger.js";
2838
import { RenderQualityError } from "../renderOrchestrator.js";
2939
import { plan } from "./plan.js";
40+
import { materializePlanV2Target, planV2, readPlanV2Manifest } from "./planV2.js";
3041
import {
3142
CHUNK_INDEX_OUT_OF_RANGE,
3243
distributedCaptureSessionDependencies,
@@ -530,6 +541,98 @@ describe("renderChunk() — script failure", () => {
530541
}, 60_000);
531542
});
532543

544+
describe("renderChunk() — a video the chunk does not show", () => {
545+
it("renders the same without the video's file, with no media warning", async () => {
546+
if (!hasChrome) {
547+
console.warn(
548+
"[renderChunk.test] skipping unshown-video test — chrome-headless-shell not available on this host",
549+
);
550+
return;
551+
}
552+
553+
const videoDir = join(runRoot, "project-two-videos");
554+
mkdirSync(videoDir, { recursive: true });
555+
for (const [name, seconds] of [
556+
["short.mp4", 2],
557+
["long.mp4", 3],
558+
] as const) {
559+
const synth = spawnSync("ffmpeg", [
560+
"-v",
561+
"error",
562+
"-y",
563+
"-f",
564+
"lavfi",
565+
"-i",
566+
`testsrc2=s=64x36:r=30:d=${seconds}`,
567+
"-c:v",
568+
"libx264",
569+
"-pix_fmt",
570+
"yuv420p",
571+
join(videoDir, name),
572+
]);
573+
if (synth.status !== 0) throw new Error(synth.stderr.toString());
574+
}
575+
writeFileSync(
576+
join(videoDir, "index.html"),
577+
// Clips over a second long, so the planner defers them to the chunks.
578+
FIXTURE_HTML.replace('data-duration="0.16667"', 'data-duration="2.4"').replace(
579+
"</div>",
580+
'<video id="short" src="short.mp4" muted data-start="0" data-duration="1.2" style="position:absolute;left:0;top:0;width:64px"></video>' +
581+
'<video id="long" src="long.mp4" muted data-start="0" data-duration="2.4" style="position:absolute;right:0;bottom:0;width:64px"></video></div>',
582+
),
583+
"utf-8",
584+
);
585+
const v2Dir = join(runRoot, "plan-two-videos");
586+
await planV2(
587+
videoDir,
588+
{ fps: 30, width: 160, height: 120, format: "png-sequence", chunkSize: 36 },
589+
v2Dir,
590+
);
591+
const manifest = readPlanV2Manifest(v2Dir);
592+
expect(manifest.artifacts.find((a) => a.path === "compiled/short.mp4")?.chunks).toEqual([0]);
593+
594+
const without = join(runRoot, "chunk1-without-short");
595+
const withShort = join(runRoot, "chunk1-with-short");
596+
materializePlanV2Target(v2Dir, { role: "chunk", chunkIndex: 1 }, without);
597+
cpSync(without, withShort, { recursive: true });
598+
copyFileSync(join(videoDir, "short.mp4"), join(withShort, "compiled", "short.mp4"));
599+
600+
const warnings: unknown[] = [];
601+
const skipped: (readonly string[] | undefined)[] = [];
602+
const originalWarn = defaultLogger.warn;
603+
const originalCreate = distributedCaptureSessionDependencies.createCaptureSession;
604+
defaultLogger.warn = (message, meta) => {
605+
warnings.push(meta);
606+
originalWarn(message, meta);
607+
};
608+
distributedCaptureSessionDependencies.createCaptureSession = ((...args) => {
609+
skipped.push(args[2].skipReadinessVideoIds);
610+
return originalCreate(...args);
611+
}) as typeof originalCreate;
612+
let a, b;
613+
try {
614+
a = await renderChunk(without, 1, join(runRoot, "chunk1-out-without"));
615+
b = await renderChunk(withShort, 1, join(runRoot, "chunk1-out-with"));
616+
} catch (err) {
617+
const message = err instanceof Error ? err.message : String(err);
618+
if (!HOST_CHROME_FAILURE_PATTERNS.test(message)) throw err;
619+
console.warn(
620+
"[renderChunk.test] skipping unshown-video test — host Chrome stack can't render. Diagnostic:",
621+
message.slice(0, 240),
622+
);
623+
return;
624+
} finally {
625+
defaultLogger.warn = originalWarn;
626+
distributedCaptureSessionDependencies.createCaptureSession = originalCreate;
627+
}
628+
629+
expect(JSON.stringify(warnings)).not.toContain("media_load_failed");
630+
// Only the unshown video is skipped; the shown one keeps today's readiness wait.
631+
expect(new Set(skipped.map(String))).toEqual(new Set(["short"]));
632+
expect(a.sha256).toBe(b.sha256);
633+
}, 90_000);
634+
});
635+
533636
describe("resolvePresetForLockedEncoder", () => {
534637
// Tiny fast tests for the codec-override helper. No Chrome, no ffmpeg —
535638
// exists so a refactor that moves the override (e.g. into

‎packages/producer/src/services/distributed/renderChunk.ts‎

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,16 @@ export async function renderChunk(
774774
// its own injector hook because the hook remembers the last injected frame
775775
// per video; sharing that state across a fresh retry page can suppress the
776776
// first injection and produce a blank/stale frame.
777+
const deferredVideoFrames = planVideos
778+
? await extractDeferredVideoFramesForChunk({
779+
planDir,
780+
planVideos,
781+
slice,
782+
fps: { num: plan.dimensions.fpsNum, den: plan.dimensions.fpsDen },
783+
outputRoot: videoFramesDir,
784+
cfg,
785+
})
786+
: [];
777787
const videoFrameLookup =
778788
planVideos && planVideos.extracted.length > 0
779789
? createFrameLookupTable(
@@ -784,14 +794,7 @@ export async function renderChunk(
784794
planVideos.extracted.filter((video) => !video.deferredRange),
785795
v2Manifest === null ? "dense-v1" : "sparse-v2",
786796
),
787-
...(await extractDeferredVideoFramesForChunk({
788-
planDir,
789-
planVideos,
790-
slice,
791-
fps: { num: plan.dimensions.fpsNum, den: plan.dimensions.fpsDen },
792-
outputRoot: videoFramesDir,
793-
cfg,
794-
})),
797+
...deferredVideoFrames,
795798
],
796799
resolveRenderFpsConfig(job.config.fps).value,
797800
)
@@ -832,6 +835,10 @@ export async function renderChunk(
832835
// engine skips the `evaluateOnNewDocument` injection.
833836
variables: encoder.variables,
834837
subTimelineWaitMemo: {},
838+
// The plan ships a deferred source only to chunks that show it; don't wait on the rest.
839+
skipReadinessVideoIds: deferredVideoFrames
840+
.filter((video) => video.framePaths.size === 0)
841+
.map((video) => video.videoId),
835842
...(videoCaptureBeyondViewport !== undefined
836843
? { captureBeyondViewport: videoCaptureBeyondViewport }
837844
: {}),

0 commit comments

Comments
 (0)