@hyperframes/producer 0.9.16 → 0.9.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,14 +3,14 @@
3
3
  // src/server.ts
4
4
  import {
5
5
  existsSync as existsSync23,
6
- mkdirSync as mkdirSync11,
7
- statSync as statSync10,
6
+ mkdirSync as mkdirSync13,
7
+ statSync as statSync11,
8
8
  mkdtempSync as mkdtempSync4,
9
9
  writeFileSync as writeFileSync8,
10
- rmSync as rmSync11,
10
+ rmSync as rmSync13,
11
11
  createReadStream as createReadStream3
12
12
  } from "node:fs";
13
- import { resolve as resolve10, dirname as dirname14, join as join32 } from "node:path";
13
+ import { resolve as resolve10, dirname as dirname15, join as join35 } from "node:path";
14
14
  import { tmpdir as tmpdir3 } from "node:os";
15
15
  import { parseArgs } from "node:util";
16
16
  import crypto from "node:crypto";
@@ -27,10 +27,10 @@ import {
27
27
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
28
28
  import {
29
29
  existsSync as existsSync20,
30
- mkdirSync as mkdirSync10,
31
- readdirSync as readdirSync5,
32
- rmSync as rmSync10,
33
- statSync as statSync8,
30
+ mkdirSync as mkdirSync12,
31
+ readdirSync as readdirSync6,
32
+ rmSync as rmSync12,
33
+ statSync as statSync9,
34
34
  writeFileSync as writeFileSync7,
35
35
  copyFileSync as copyFileSync5,
36
36
  appendFileSync
@@ -53,7 +53,7 @@ import {
53
53
  closeCaptureSession as closeCaptureSession8,
54
54
  createCaptureSession as createCaptureSession8,
55
55
  initializeSession as initializeSession8,
56
- createVideoFrameInjector,
56
+ createVideoFrameInjector as createVideoFrameInjector2,
57
57
  getEncoderPreset as getEncoderPreset3,
58
58
  distributeFrames as distributeFrames2,
59
59
  distributeFramesInterleaved as distributeFramesInterleaved2,
@@ -84,7 +84,7 @@ import {
84
84
  referenceBackgroundResult,
85
85
  createReferenceBackgroundRecorder
86
86
  } from "@hyperframes/engine";
87
- import { join as join28, dirname as dirname12, resolve as resolve8 } from "path";
87
+ import { join as join31, dirname as dirname13, resolve as resolve8 } from "path";
88
88
  import { totalmem as totalmem2 } from "node:os";
89
89
  import { randomUUID } from "crypto";
90
90
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -2029,6 +2029,7 @@ import {
2029
2029
  getCapturePerfSummary as getCapturePerfSummary2,
2030
2030
  initializeSession as initializeSession2,
2031
2031
  prepareCaptureSessionForReuse as prepareCaptureSessionForReuse2,
2032
+ resolveDeVerifyMinDb as resolveDeVerifyMinDb2,
2032
2033
  shouldDisableBrowserPoolForParallelWorker,
2033
2034
  spawnStreamingEncoder as spawnStreamingEncoder2
2034
2035
  } from "@hyperframes/engine";
@@ -3899,42 +3900,47 @@ function createDrainFrameGuard(args) {
3899
3900
  sizes.push(buf.length);
3900
3901
  if (sizes.length > 60) sizes.shift();
3901
3902
  }
3902
- const truth = session.deVerifyFrames?.get(idx);
3903
- if (truth) {
3904
- let db;
3905
- try {
3906
- db = await psnrDb(buf, truth);
3907
- } catch (err) {
3908
- log.warn("[Render] drawElement self-verify sample skipped (psnr infrastructure)", {
3909
- frame: idx,
3910
- error: err instanceof Error ? err.message : String(err)
3911
- });
3912
- return buf;
3913
- }
3914
- if (db < verifyMinDb) {
3915
- const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3916
- if (dumpDir) {
3917
- await Promise.all([
3918
- writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
3919
- writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
3920
- ]).catch(() => {
3921
- });
3922
- }
3923
- throw new DrawElementVerificationError(
3924
- `drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
3925
- { kind: "psnr", frameIndex: idx, failedDb: db, verifyThresholdDb: verifyMinDb }
3926
- );
3927
- }
3928
- stats.verifyChecked += 1;
3929
- stats.verifyMinDb = stats.verifyMinDb === void 0 ? db : Math.min(stats.verifyMinDb, db);
3930
- log.info("[Render] drawElement self-verify passed", {
3931
- frame: idx,
3932
- psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1))
3933
- });
3934
- }
3903
+ await verifyDrawElementSample(session, idx, buf, { log, stats, verifyMinDb });
3935
3904
  return buf;
3936
3905
  };
3937
3906
  }
3907
+ async function verifyDrawElementSample(session, idx, buf, ctx) {
3908
+ const { log, stats, verifyMinDb } = ctx;
3909
+ const truth = session.deVerifyFrames?.get(idx);
3910
+ if (!truth) return;
3911
+ let db;
3912
+ try {
3913
+ db = await psnrDb(buf, truth);
3914
+ } catch (err) {
3915
+ log.warn("[Render] drawElement self-verify sample skipped (psnr infrastructure)", {
3916
+ frame: idx,
3917
+ error: err instanceof Error ? err.message : String(err)
3918
+ });
3919
+ return;
3920
+ }
3921
+ if (db < verifyMinDb) {
3922
+ const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3923
+ if (dumpDir) {
3924
+ await Promise.all([
3925
+ writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
3926
+ writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
3927
+ ]).catch(() => {
3928
+ });
3929
+ }
3930
+ throw new DrawElementVerificationError(
3931
+ `drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
3932
+ { kind: "psnr", frameIndex: idx, failedDb: db, verifyThresholdDb: verifyMinDb }
3933
+ );
3934
+ }
3935
+ if (stats) {
3936
+ stats.verifyChecked += 1;
3937
+ stats.verifyMinDb = stats.verifyMinDb === void 0 ? db : Math.min(stats.verifyMinDb, db);
3938
+ }
3939
+ log.info("[Render] drawElement self-verify passed", {
3940
+ frame: idx,
3941
+ psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1))
3942
+ });
3943
+ }
3938
3944
  async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, resume, risk) {
3939
3945
  let prev = null;
3940
3946
  const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
@@ -4398,6 +4404,7 @@ async function runCaptureStreamingStage(input) {
4398
4404
  );
4399
4405
  } else {
4400
4406
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
4407
+ const verifyMinDb = resolveDeVerifyMinDb();
4401
4408
  let lastProgressAt = Date.now();
4402
4409
  for (let i = resume.from; i < totalFrames; i++) {
4403
4410
  assertNotAborted();
@@ -4414,6 +4421,7 @@ async function runCaptureStreamingStage(input) {
4414
4421
  { session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
4415
4422
  abortSignal
4416
4423
  );
4424
+ await verifyDrawElementSample(session, i, buffer, { log, verifyMinDb });
4417
4425
  await reorderBuffer.waitForFrame(i);
4418
4426
  ensureFrameWritten(await currentEncoder.writeFrame(buffer), i, currentEncoder);
4419
4427
  reorderBuffer.advanceTo(i + 1);
@@ -4522,6 +4530,10 @@ async function captureSegmentFrames(ctx, segment, encoder) {
4522
4530
  },
4523
4531
  ctx.abortSignal
4524
4532
  );
4533
+ await verifyDrawElementSample(ctx.session, i, buffer, {
4534
+ log: ctx.log,
4535
+ verifyMinDb: ctx.verifyMinDb
4536
+ });
4525
4537
  ensureFrameWritten(await encoder.writeFrame(buffer), i, encoder);
4526
4538
  ctx.job.framesRendered = i + 1;
4527
4539
  lastProgressAt = Date.now();
@@ -4536,6 +4548,25 @@ async function captureSegmentFrames(ctx, segment, encoder) {
4536
4548
  }
4537
4549
  }
4538
4550
  async function captureSegment(ctx, segment) {
4551
+ const { frames } = ctx;
4552
+ if (!frames) return captureSegmentToEncoder(ctx, segment);
4553
+ try {
4554
+ reportFrameProgress(
4555
+ ctx.job,
4556
+ `Reading video for segment ${segment.index + 1}/${ctx.segmentCount}`,
4557
+ Math.round(25 + segment.startFrame / ctx.totalFrames * 55),
4558
+ ctx.onProgress,
4559
+ segment.startFrame,
4560
+ ctx.totalFrames
4561
+ );
4562
+ await frames.prepare(segment);
4563
+ ctx.assertNotAborted();
4564
+ return await captureSegmentToEncoder(ctx, segment);
4565
+ } finally {
4566
+ frames.release();
4567
+ }
4568
+ }
4569
+ async function captureSegmentToEncoder(ctx, segment) {
4539
4570
  const segmentPath = segmentOutputPath(ctx.segmentDir, segment.index);
4540
4571
  let encoder;
4541
4572
  try {
@@ -4588,16 +4619,16 @@ var DEFAULT_DEPS = {
4588
4619
  function defaultSessionFactory(input) {
4589
4620
  let probeSession = input.probeSession;
4590
4621
  return {
4591
- create: async () => {
4622
+ create: async (injector = input.createRenderVideoFrameInjector) => {
4592
4623
  const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
4593
4624
  if (probeSession && probeCannotCaptureEveryFrame) {
4594
4625
  await closeCaptureSession2(probeSession).catch(() => {
4595
4626
  });
4596
4627
  probeSession = null;
4597
4628
  }
4598
- const session = probeSession ?? await openSegmentedSession(input);
4629
+ const session = probeSession ?? await openSegmentedSession(input, injector);
4599
4630
  if (probeSession) {
4600
- openSegmentedSessionReuse(input, probeSession);
4631
+ openSegmentedSessionReuse(input, probeSession, injector);
4601
4632
  probeSession = null;
4602
4633
  }
4603
4634
  try {
@@ -4612,12 +4643,12 @@ function defaultSessionFactory(input) {
4612
4643
  }
4613
4644
  };
4614
4645
  }
4615
- function openSegmentedSessionReuse(input, session) {
4616
- prepareCaptureSessionForReuse2(session, input.framesDir, input.createRenderVideoFrameInjector());
4646
+ function openSegmentedSessionReuse(input, session, injector) {
4647
+ prepareCaptureSessionForReuse2(session, input.framesDir, injector());
4617
4648
  }
4618
- async function openSegmentedSession(input) {
4649
+ async function openSegmentedSession(input, injector) {
4619
4650
  const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
4620
- const videoInjector = input.createRenderVideoFrameInjector();
4651
+ const videoInjector = injector();
4621
4652
  const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
4622
4653
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4623
4654
  parallel: (input.workerCount ?? 1) > 1
@@ -4630,6 +4661,19 @@ async function openSegmentedSession(input) {
4630
4661
  ownBrowser ? { ...captureCfg, enableBrowserPool: false } : captureCfg
4631
4662
  );
4632
4663
  }
4664
+ function removeJoinedSegments(run, segmentPaths) {
4665
+ if (!run.input.segmentVideoFrames || run.input.job.config.keepSegments === true) return;
4666
+ for (const path of segmentPaths) {
4667
+ try {
4668
+ run.deps.removeFile(path);
4669
+ } catch (err) {
4670
+ run.input.log.warn("[Render] could not remove a joined segment file", {
4671
+ path,
4672
+ error: err instanceof Error ? err.message : String(err)
4673
+ });
4674
+ }
4675
+ }
4676
+ }
4633
4677
  async function concatSegments(concat, segmentPaths, videoOnlyPath, abortSignal, cfg) {
4634
4678
  const result = await concat(segmentPaths, videoOnlyPath, abortSignal, cfg);
4635
4679
  if (result.success) return;
@@ -4641,12 +4685,14 @@ async function concatSegments(concat, segmentPaths, videoOnlyPath, abortSignal,
4641
4685
  async function createSegmentWorker(run, id) {
4642
4686
  const { input, deps } = run;
4643
4687
  const factory = input.sessionFactoryForWorker?.(id) ?? run.sessionFactory;
4688
+ const frames = input.segmentVideoFrames?.forWorker();
4644
4689
  return {
4645
4690
  id,
4646
4691
  factory,
4647
4692
  sessionSegments: 0,
4648
4693
  ctx: {
4649
- session: await factory.create(),
4694
+ session: await factory.create(frames && (() => frames.hook)),
4695
+ frames,
4650
4696
  job: input.job,
4651
4697
  cfg: input.cfg,
4652
4698
  totalFrames: input.totalFrames,
@@ -4657,6 +4703,8 @@ async function createSegmentWorker(run, id) {
4657
4703
  spawnEncoder: deps.spawnEncoder,
4658
4704
  captureFrame: deps.captureFrame,
4659
4705
  stallTimeoutMs: resolveCaptureStallTimeoutMs(),
4706
+ log: input.log,
4707
+ verifyMinDb: resolveDeVerifyMinDb2(),
4660
4708
  abortSignal: input.abortSignal,
4661
4709
  assertNotAborted: input.assertNotAborted,
4662
4710
  onProgress: input.onProgress,
@@ -4676,7 +4724,8 @@ async function recycleWorkerSession(run, worker, why) {
4676
4724
  run.input.dedupPerfs.push(getCapturePerfSummary2(worker.ctx.session));
4677
4725
  const memory = worker.ctx.session.chromeMemory?.stats();
4678
4726
  await run.deps.closeSession(worker.ctx.session);
4679
- worker.ctx.session = await worker.factory.create();
4727
+ const { frames } = worker.ctx;
4728
+ worker.ctx.session = await worker.factory.create(frames && (() => frames.hook));
4680
4729
  worker.sessionSegments = 0;
4681
4730
  if (why === "cadence") run.totals.browserRecycles += 1;
4682
4731
  run.input.log.info(`[Render] segment browser recycled (${why})`, {
@@ -4744,6 +4793,7 @@ async function concatResumedRun(run, segmentPaths) {
4744
4793
  input.abortSignal,
4745
4794
  input.cfg
4746
4795
  );
4796
+ removeJoinedSegments(run, segmentPaths);
4747
4797
  return {
4748
4798
  success: true,
4749
4799
  encodeMs: 0,
@@ -4818,6 +4868,7 @@ async function runCaptureSegmentedStage(input) {
4818
4868
  input.abortSignal,
4819
4869
  input.cfg
4820
4870
  );
4871
+ removeJoinedSegments(run, segmentPaths);
4821
4872
  return {
4822
4873
  success: true,
4823
4874
  encodeMs: run.totals.encodeMs,
@@ -5249,10 +5300,273 @@ function checkPageNeverReady(input) {
5249
5300
  return new PageNeverReadyError(error);
5250
5301
  }
5251
5302
 
5303
+ // src/services/render/deferredVideoFrames.ts
5304
+ import { mkdirSync as mkdirSync4, rmSync as rmSync7 } from "node:fs";
5305
+ import { dirname as dirname5, join as join12 } from "node:path";
5306
+ import {
5307
+ createFrameLookupTable,
5308
+ extractVideoFramesRange,
5309
+ runVideoExtractionWithRetry
5310
+ } from "@hyperframes/engine";
5311
+ function contiguousRuns(sortedIndexes) {
5312
+ const runs = [];
5313
+ for (const index of sortedIndexes) {
5314
+ const last = runs[runs.length - 1];
5315
+ if (last && last.firstFrame + last.frames === index) last.frames++;
5316
+ else runs.push({ firstFrame: index, frames: 1 });
5317
+ }
5318
+ return runs;
5319
+ }
5320
+ function shownDeferredFrameIndexes(source, frames, fps) {
5321
+ const shown = /* @__PURE__ */ new Map();
5322
+ const deferred = source.extracted.filter((video) => video.deferredRange);
5323
+ if (deferred.length === 0) return shown;
5324
+ const table = createFrameLookupTable(
5325
+ source.videos,
5326
+ deferred.map((video) => ({
5327
+ ...video,
5328
+ outputDir: "",
5329
+ framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
5330
+ })),
5331
+ resolveRenderFpsConfig(fps).value
5332
+ );
5333
+ for (const frame of frames) {
5334
+ const globalTime = frame * fps.den / fps.num;
5335
+ for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
5336
+ const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
5337
+ indexes.add(payload.frameIndex);
5338
+ shown.set(videoId, indexes);
5339
+ }
5340
+ }
5341
+ return shown;
5342
+ }
5343
+ async function extractDeferredFrames(input) {
5344
+ const deferred = input.extracted.filter((video) => video.deferredRange);
5345
+ if (deferred.length === 0) return [];
5346
+ const shown = shownDeferredFrameIndexes(input, input.frames, input.fps);
5347
+ return Promise.all(
5348
+ deferred.map(async (video) => {
5349
+ const outputDir = join12(input.outputRoot, video.videoId);
5350
+ const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
5351
+ if (indexes.length === 0) {
5352
+ return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
5353
+ }
5354
+ const range = video.deferredRange;
5355
+ const extracted = await extractVideoFramesRange(
5356
+ input.sourceOf(video),
5357
+ video.videoId,
5358
+ range.startTime,
5359
+ range.durationSeconds,
5360
+ {
5361
+ fps: input.fps,
5362
+ outputDir: input.outputRoot,
5363
+ format: range.format,
5364
+ frameRanges: contiguousRuns(indexes),
5365
+ shownBox: input.shownBoxes?.get(video.videoId)
5366
+ },
5367
+ input.signal,
5368
+ input.cfg
5369
+ );
5370
+ const missing = indexes.find((index) => !extracted.framePaths.has(index));
5371
+ if (missing !== void 0) throw input.onMissingFrame(video, missing);
5372
+ return { ...video, outputDir, framePaths: extracted.framePaths, ownedByLookup: false };
5373
+ })
5374
+ );
5375
+ }
5376
+ async function extractAllDeferredFrames(extracted, input) {
5377
+ const framesTotal = extracted.reduce(
5378
+ (sum, video) => sum + (video.deferredRange ? video.totalFrames : 0),
5379
+ 0
5380
+ );
5381
+ let framesDone = 0;
5382
+ for (const video of extracted) {
5383
+ const range = video.deferredRange;
5384
+ if (!range) continue;
5385
+ const { result: filled } = await runVideoExtractionWithRetry(
5386
+ () => extractVideoFramesRange(
5387
+ input.sourceOf?.(video) ?? video.srcPath,
5388
+ video.videoId,
5389
+ range.startTime,
5390
+ range.durationSeconds,
5391
+ { fps: input.fps, outputDir: dirname5(video.outputDir), format: range.format },
5392
+ input.signal,
5393
+ input.cfg
5394
+ ),
5395
+ {
5396
+ signal: input.signal,
5397
+ maxTransientRetries: input.maxTransientRetries,
5398
+ onRetry: () => {
5399
+ rmSync7(video.outputDir, { recursive: true, force: true });
5400
+ mkdirSync4(video.outputDir, { recursive: true });
5401
+ }
5402
+ }
5403
+ );
5404
+ if (filled.framePaths.size < video.totalFrames) {
5405
+ throw new Error(
5406
+ `[Render] video ${JSON.stringify(video.videoId)} has ${filled.framePaths.size} frames; it was expected to have ${video.totalFrames} frames.`
5407
+ );
5408
+ }
5409
+ video.framePaths = filled.framePaths;
5410
+ delete video.deferredRange;
5411
+ framesDone += video.totalFrames;
5412
+ input.onProgress?.(framesDone, framesTotal);
5413
+ }
5414
+ }
5415
+
5416
+ // src/services/render/segmentVideoFrames.ts
5417
+ import { mkdirSync as mkdirSync5, rmSync as rmSync8 } from "node:fs";
5418
+ import { join as join13 } from "node:path";
5419
+ import {
5420
+ createFrameLookupTable as createFrameLookupTable2,
5421
+ createVideoFrameInjector
5422
+ } from "@hyperframes/engine";
5423
+ function segmentFramesDir(rootDir, index) {
5424
+ return join13(rootDir, "segment-frames", `segment_${String(index).padStart(5, "0")}`);
5425
+ }
5426
+ function createSegmentVideoFrames(input) {
5427
+ const extractFrames = input.extractFrames ?? extractDeferredFrames;
5428
+ const sourceOf = input.sourceOf ?? ((video) => video.srcPath);
5429
+ const fullyExtracted = input.extracted.filter((video) => !video.deferredRange);
5430
+ const tableFor = async (frames, dir) => {
5431
+ const sliced = await extractFrames({
5432
+ videos: input.videos,
5433
+ extracted: input.extracted,
5434
+ frames,
5435
+ fps: input.fps,
5436
+ outputRoot: dir,
5437
+ cfg: input.cfg,
5438
+ signal: input.abortSignal,
5439
+ sourceOf,
5440
+ onMissingFrame: (video, index) => new Error(
5441
+ `[Render] video ${JSON.stringify(video.videoId)} has no frame ${index}; it was expected to have ${video.totalFrames} frames.`
5442
+ )
5443
+ });
5444
+ return createFrameLookupTable2(
5445
+ input.videos,
5446
+ [...fullyExtracted, ...sliced],
5447
+ resolveRenderFpsConfig(input.fps).value
5448
+ );
5449
+ };
5450
+ const injectorFor = async (frames, dir) => createVideoFrameInjector(await tableFor(frames, dir), input.injectorOptions);
5451
+ let onDemandCalls = 0;
5452
+ const onDemandHook = () => Object.assign(
5453
+ async (page, time, heldVideoTime) => {
5454
+ const dir = join13(
5455
+ input.rootDir,
5456
+ "segment-frames",
5457
+ `on-demand_${String(onDemandCalls++).padStart(5, "0")}`
5458
+ );
5459
+ try {
5460
+ const frames = [time, heldVideoTime].filter((t) => t !== void 0).map((t) => Math.round(t * input.fps.num / input.fps.den));
5461
+ const injector = await injectorFor(frames, dir);
5462
+ await injector?.(page, time, heldVideoTime);
5463
+ } finally {
5464
+ rmSync8(dir, { recursive: true, force: true });
5465
+ }
5466
+ },
5467
+ { fresh: onDemandHook }
5468
+ );
5469
+ return {
5470
+ async forFrames(frames, name) {
5471
+ const dir = join13(input.rootDir, "segment-frames", name);
5472
+ const release = () => rmSync8(dir, { recursive: true, force: true });
5473
+ try {
5474
+ release();
5475
+ mkdirSync5(dir, { recursive: true });
5476
+ const table = await tableFor([...frames], dir);
5477
+ return { injector: () => createVideoFrameInjector(table, input.injectorOptions), release };
5478
+ } catch (error) {
5479
+ release();
5480
+ throw error;
5481
+ }
5482
+ },
5483
+ forWorker() {
5484
+ let injector = null;
5485
+ let folder = null;
5486
+ const onDemand = onDemandHook();
5487
+ const release = () => {
5488
+ injector = null;
5489
+ if (folder) rmSync8(folder, { recursive: true, force: true });
5490
+ folder = null;
5491
+ };
5492
+ return {
5493
+ hook: Object.assign(
5494
+ (page, time, heldVideoTime) => (injector ?? onDemand)(page, time, heldVideoTime),
5495
+ { fresh: onDemandHook }
5496
+ ),
5497
+ release,
5498
+ async prepare(segment) {
5499
+ release();
5500
+ const dir = segmentFramesDir(input.rootDir, segment.index);
5501
+ folder = dir;
5502
+ try {
5503
+ rmSync8(dir, { recursive: true, force: true });
5504
+ mkdirSync5(dir, { recursive: true });
5505
+ injector = await injectorFor(
5506
+ Array.from(
5507
+ { length: segment.endFrame - segment.startFrame },
5508
+ (_, i) => segment.startFrame + i
5509
+ ),
5510
+ dir
5511
+ );
5512
+ } catch (error) {
5513
+ release();
5514
+ throw error;
5515
+ }
5516
+ }
5517
+ };
5518
+ }
5519
+ };
5520
+ }
5521
+
5522
+ // src/services/render/videoFrameWindow.ts
5523
+ import { readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
5524
+ import { join as join14 } from "node:path";
5525
+ function shouldWindowVideoFrames(args) {
5526
+ return args.segmented && args.env.HF_VIDEO_FRAME_WINDOW?.trim().toLowerCase() === "true";
5527
+ }
5528
+ function directoryBytes(path) {
5529
+ let bytes = 0;
5530
+ let entries;
5531
+ try {
5532
+ entries = readdirSync3(path, { withFileTypes: true });
5533
+ } catch {
5534
+ return 0;
5535
+ }
5536
+ for (const entry of entries) {
5537
+ const child2 = join14(path, entry.name);
5538
+ try {
5539
+ bytes += entry.isDirectory() ? directoryBytes(child2) : statSync4(child2).size;
5540
+ } catch {
5541
+ }
5542
+ }
5543
+ return bytes;
5544
+ }
5545
+ function trackSegmentFramePeak(frames, rootDir, onPeak) {
5546
+ let peak = 0;
5547
+ return {
5548
+ forFrames: (frameIndexes, name) => frames.forFrames(frameIndexes, name),
5549
+ forWorker() {
5550
+ const worker = frames.forWorker();
5551
+ return {
5552
+ ...worker,
5553
+ async prepare(segment) {
5554
+ await worker.prepare(segment);
5555
+ const held = directoryBytes(join14(rootDir, "segment-frames"));
5556
+ if (held > peak) {
5557
+ peak = held;
5558
+ onPeak(peak);
5559
+ }
5560
+ }
5561
+ };
5562
+ }
5563
+ };
5564
+ }
5565
+
5252
5566
  // src/services/render/segmentManifest.ts
5253
5567
  import { createHash as createHash2 } from "node:crypto";
5254
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
5255
- import { join as join12 } from "node:path";
5568
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
5569
+ import { join as join15 } from "node:path";
5256
5570
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
5257
5571
  var MANIFEST_FILENAME = "segments.json";
5258
5572
  function computeSegmentPlanHash(input) {
@@ -5280,7 +5594,7 @@ function computeSegmentPlanHash(input) {
5280
5594
  return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
5281
5595
  }
5282
5596
  function segmentDirFor(projectRendersDir, planHash) {
5283
- return join12(projectRendersDir, ".hf-segments", planHash);
5597
+ return join15(projectRendersDir, ".hf-segments", planHash);
5284
5598
  }
5285
5599
  function hasNumber(value, key) {
5286
5600
  return key in value && typeof Reflect.get(value, key) === "number";
@@ -5298,7 +5612,7 @@ function isManifest(value) {
5298
5612
  return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
5299
5613
  }
5300
5614
  function readSegmentManifest(segmentDir) {
5301
- const path = join12(segmentDir, MANIFEST_FILENAME);
5615
+ const path = join15(segmentDir, MANIFEST_FILENAME);
5302
5616
  if (!existsSync7(path)) return null;
5303
5617
  try {
5304
5618
  const parsed = JSON.parse(readFileSync4(path, "utf-8"));
@@ -5308,10 +5622,10 @@ function readSegmentManifest(segmentDir) {
5308
5622
  }
5309
5623
  }
5310
5624
  function writeSegmentManifest(segmentDir, manifest) {
5311
- mkdirSync4(segmentDir, { recursive: true });
5312
- const tmp = join12(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
5625
+ mkdirSync6(segmentDir, { recursive: true });
5626
+ const tmp = join15(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
5313
5627
  writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
5314
- renameWithRetry2(tmp, join12(segmentDir, MANIFEST_FILENAME));
5628
+ renameWithRetry2(tmp, join15(segmentDir, MANIFEST_FILENAME));
5315
5629
  }
5316
5630
  async function probeSegmentFrameCount(path, probe) {
5317
5631
  try {
@@ -5329,7 +5643,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
5329
5643
  if (manifest.planHash !== expectedHash) return ok;
5330
5644
  for (const entry of manifest.completed) {
5331
5645
  if (!existsSync7(entry.path)) continue;
5332
- const size = statSync4(entry.path).size;
5646
+ const size = statSync5(entry.path).size;
5333
5647
  if (size <= 0 || size !== entry.bytes) continue;
5334
5648
  const frames = await probeFrames(entry.path);
5335
5649
  if (frames !== entry.endFrame - entry.startFrame) continue;
@@ -5872,7 +6186,7 @@ function computeVideoFrameCoverage(videos, extracted, fps) {
5872
6186
  for (const video of videos) {
5873
6187
  const entry = byId.get(video.id);
5874
6188
  const expectedFrames = expectedFramesForVideo(video, entry, fps);
5875
- const capturedFrames = entry ? entry.framePaths.size : 0;
6189
+ const capturedFrames = entry ? entry.deferredRange ? entry.totalFrames : entry.framePaths.size : 0;
5876
6190
  const ratio = expectedFrames === 0 ? 1 : capturedFrames / expectedFrames;
5877
6191
  reports.push({
5878
6192
  videoId: video.id,
@@ -5908,7 +6222,7 @@ function countAuthoredTimedClips(html) {
5908
6222
 
5909
6223
  // src/services/render/stages/compileStage.ts
5910
6224
  import { existsSync as existsSync11, writeFileSync as writeFileSync5 } from "node:fs";
5911
- import { join as join16 } from "node:path";
6225
+ import { join as join19 } from "node:path";
5912
6226
  import { suggestMatchingPreset } from "@hyperframes/core";
5913
6227
  import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
5914
6228
  import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
@@ -5916,8 +6230,8 @@ import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolu
5916
6230
  // src/services/htmlCompiler.ts
5917
6231
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5918
6232
  import { createHash as createHash6 } from "node:crypto";
5919
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
5920
- import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
6233
+ import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7 } from "fs";
6234
+ import { join as join18, dirname as dirname8, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
5921
6235
  import { parseHTML as parseHTML5 } from "linkedom";
5922
6236
  import {
5923
6237
  COMPOSITION_SOURCE_URL,
@@ -6163,8 +6477,8 @@ import {
6163
6477
 
6164
6478
  // src/services/animatedGifPrep.ts
6165
6479
  import { createHash as createHash4 } from "node:crypto";
6166
- import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync7, statSync as statSync5 } from "node:fs";
6167
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join13, resolve as resolve5 } from "node:path";
6480
+ import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync9, statSync as statSync6 } from "node:fs";
6481
+ import { dirname as dirname6, isAbsolute as isAbsolute2, join as join16, resolve as resolve5 } from "node:path";
6168
6482
  import { parseHTML as parseHTML3 } from "linkedom";
6169
6483
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
6170
6484
  import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
@@ -6213,7 +6527,7 @@ function resolveLocalImageSourcePath(src, options) {
6213
6527
  }
6214
6528
  function isUsableFile(path) {
6215
6529
  try {
6216
- const stat = statSync5(path);
6530
+ const stat = statSync6(path);
6217
6531
  return stat.isFile() && stat.size > 0;
6218
6532
  } catch {
6219
6533
  return false;
@@ -6320,11 +6634,11 @@ async function runAnimatedGifTranscode(request) {
6320
6634
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
6321
6635
  }
6322
6636
  async function ensurePreparedWebm(input) {
6323
- if (!existsSync8(dirname5(input.cachePath))) {
6324
- mkdirSync5(dirname5(input.cachePath), { recursive: true });
6637
+ if (!existsSync8(dirname6(input.cachePath))) {
6638
+ mkdirSync7(dirname6(input.cachePath), { recursive: true });
6325
6639
  }
6326
- if (!existsSync8(dirname5(input.outputPath))) {
6327
- mkdirSync5(dirname5(input.outputPath), { recursive: true });
6640
+ if (!existsSync8(dirname6(input.outputPath))) {
6641
+ mkdirSync7(dirname6(input.outputPath), { recursive: true });
6328
6642
  }
6329
6643
  if (!isUsableFile(input.cachePath)) {
6330
6644
  const tmpPath = `${input.cachePath}.tmp-${process.pid}-${Date.now()}`;
@@ -6348,10 +6662,10 @@ async function ensurePreparedWebm(input) {
6348
6662
  if (!isUsableFile(input.cachePath)) {
6349
6663
  renameWithRetry3(tmpPath, input.cachePath);
6350
6664
  } else {
6351
- rmSync7(tmpPath, { force: true });
6665
+ rmSync9(tmpPath, { force: true });
6352
6666
  }
6353
6667
  } catch (error) {
6354
- rmSync7(tmpPath, { force: true });
6668
+ rmSync9(tmpPath, { force: true });
6355
6669
  throw error;
6356
6670
  }
6357
6671
  }
@@ -6407,7 +6721,7 @@ function replaceImageWithVideo(input) {
6407
6721
  return video;
6408
6722
  }
6409
6723
  async function prepareAnimatedGifInputs(html, options) {
6410
- const outputDir = options.outputDir ?? join13(options.downloadDir, PREPARED_GIF_SUBDIR);
6724
+ const outputDir = options.outputDir ?? join16(options.downloadDir, PREPARED_GIF_SUBDIR);
6411
6725
  const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
6412
6726
  const cacheDir = options.cacheDir ?? outputDir;
6413
6727
  const { document: document2 } = parseHTML3(html);
@@ -6434,8 +6748,8 @@ async function prepareAnimatedGifInputs(html, options) {
6434
6748
  );
6435
6749
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
6436
6750
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
6437
- const cachePath = join13(cacheDir, filename);
6438
- const outputPath = join13(outputDir, filename);
6751
+ const cachePath = join16(cacheDir, filename);
6752
+ const outputPath = join16(outputDir, filename);
6439
6753
  const outputSrc = `${outputSrcPrefix}/${filename}`;
6440
6754
  await ensurePreparedWebm({
6441
6755
  sourcePath,
@@ -6472,15 +6786,15 @@ import { createHash as createHash5 } from "node:crypto";
6472
6786
  import {
6473
6787
  closeSync as closeSync4,
6474
6788
  copyFileSync as copyFileSync3,
6475
- mkdirSync as mkdirSync6,
6789
+ mkdirSync as mkdirSync8,
6476
6790
  openSync as openSync4,
6477
6791
  readFileSync as readFileSync6,
6478
6792
  readSync as readSync3,
6479
- rmSync as rmSync8,
6480
- statSync as statSync6,
6793
+ rmSync as rmSync10,
6794
+ statSync as statSync7,
6481
6795
  writeFileSync as writeFileSync4
6482
6796
  } from "node:fs";
6483
- import { dirname as dirname6, join as join14, resolve as resolve6 } from "node:path";
6797
+ import { dirname as dirname7, join as join17, resolve as resolve6 } from "node:path";
6484
6798
  import { parseHTML as parseHTML4 } from "linkedom";
6485
6799
  import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
6486
6800
 
@@ -6720,14 +7034,14 @@ function resolveImageSource(src, options) {
6720
7034
  }
6721
7035
  function isUsableFile2(path) {
6722
7036
  try {
6723
- return statSync6(path).size > 0;
7037
+ return statSync7(path).size > 0;
6724
7038
  } catch {
6725
7039
  return false;
6726
7040
  }
6727
7041
  }
6728
7042
  async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
6729
- mkdirSync6(dirname6(cachePath), { recursive: true });
6730
- mkdirSync6(dirname6(outputPath), { recursive: true });
7043
+ mkdirSync8(dirname7(cachePath), { recursive: true });
7044
+ mkdirSync8(dirname7(outputPath), { recursive: true });
6731
7045
  if (!isUsableFile2(cachePath)) {
6732
7046
  const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
6733
7047
  const inlinePath = `${tmpPath}.heic`;
@@ -6745,8 +7059,8 @@ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
6745
7059
  const reason = error instanceof Error ? error.message : String(error);
6746
7060
  throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
6747
7061
  } finally {
6748
- rmSync8(tmpPath, { force: true });
6749
- rmSync8(inlinePath, { force: true });
7062
+ rmSync10(tmpPath, { force: true });
7063
+ rmSync10(inlinePath, { force: true });
6750
7064
  }
6751
7065
  }
6752
7066
  if (resolve6(outputPath) !== resolve6(cachePath) && !isUsableFile2(outputPath)) {
@@ -6754,7 +7068,7 @@ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
6754
7068
  }
6755
7069
  }
6756
7070
  async function prepareHeifImageInputs(html, options) {
6757
- const outputDir = options.outputDir ?? join14(options.downloadDir, PREPARED_HEIF_SUBDIR);
7071
+ const outputDir = options.outputDir ?? join17(options.downloadDir, PREPARED_HEIF_SUBDIR);
6758
7072
  const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
6759
7073
  const cacheDir = options.cacheDir ?? outputDir;
6760
7074
  const preparedAssets = /* @__PURE__ */ new Map();
@@ -6775,7 +7089,7 @@ async function prepareHeifImageInputs(html, options) {
6775
7089
  await Promise.all(
6776
7090
  [...unique].map(
6777
7091
  ([filename, source]) => withMediaProbeSlot(
6778
- () => ensurePreparedJpeg(source, join14(cacheDir, filename), join14(outputDir, filename), options)
7092
+ () => ensurePreparedJpeg(source, join17(cacheDir, filename), join17(outputDir, filename), options)
6779
7093
  ).catch((error) => {
6780
7094
  if (!options.onFailure) throw error;
6781
7095
  failed.add(filename);
@@ -6787,8 +7101,8 @@ async function prepareHeifImageInputs(html, options) {
6787
7101
  if (failed.has(filename)) continue;
6788
7102
  const outputSrc = `${outputSrcPrefix}/${filename}`;
6789
7103
  img.setAttribute("src", outputSrc);
6790
- preparedAssets.set(outputSrc, join14(outputDir, filename));
6791
- preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join14(outputDir, filename) });
7104
+ preparedAssets.set(outputSrc, join17(outputDir, filename));
7105
+ preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join17(outputDir, filename) });
6792
7106
  }
6793
7107
  return { html: document2.toString(), preparedAssets, preparedImages };
6794
7108
  }
@@ -7112,8 +7426,8 @@ function detectShaderTransitionUsage(html) {
7112
7426
  async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
7113
7427
  let filePath = src;
7114
7428
  if (isHttpUrl(src)) {
7115
- const remoteDir = join15(downloadDir, REMOTE_MEDIA_SUBDIR);
7116
- if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
7429
+ const remoteDir = join18(downloadDir, REMOTE_MEDIA_SUBDIR);
7430
+ if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
7117
7431
  try {
7118
7432
  filePath = await downloadToTemp(src, remoteDir, void 0, void 0, void 0, {
7119
7433
  onTelemetry: logRemoteDownloadTelemetry
@@ -7123,7 +7437,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
7123
7437
  }
7124
7438
  } else {
7125
7439
  filePath = decodedUrlPath(src);
7126
- if (!filePath.startsWith("/")) filePath = join15(baseDir, filePath);
7440
+ if (!filePath.startsWith("/")) filePath = join18(baseDir, filePath);
7127
7441
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
7128
7442
  }
7129
7443
  if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
@@ -7284,7 +7598,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
7284
7598
  workItems.map(async (item) => {
7285
7599
  const { html: compiledSub } = await compileHtmlFile(
7286
7600
  rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
7287
- dirname7(item.filePath),
7601
+ dirname8(item.filePath),
7288
7602
  downloadDir,
7289
7603
  void 0,
7290
7604
  urlByAbsentLocalPath,
@@ -7714,7 +8028,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
7714
8028
  var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
7715
8029
  async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
7716
8030
  if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
7717
- if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
8031
+ if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
7718
8032
  const urlToLocal = /* @__PURE__ */ new Map();
7719
8033
  await Promise.all(
7720
8034
  [...urlSet].map(async (url) => {
@@ -7761,7 +8075,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
7761
8075
  return downloadAndRewriteUrls(
7762
8076
  urlSet,
7763
8077
  html,
7764
- join15(downloadDir, REMOTE_MEDIA_SUBDIR),
8078
+ join18(downloadDir, REMOTE_MEDIA_SUBDIR),
7765
8079
  "Remote media download failed",
7766
8080
  "Localized remote media source(s)"
7767
8081
  );
@@ -7776,7 +8090,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
7776
8090
  return downloadAndRewriteUrls(
7777
8091
  urlSet,
7778
8092
  html,
7779
- join15(downloadDir, REMOTE_MEDIA_SUBDIR),
8093
+ join18(downloadDir, REMOTE_MEDIA_SUBDIR),
7780
8094
  "Remote image download failed",
7781
8095
  "Localized remote image source(s)"
7782
8096
  );
@@ -7792,7 +8106,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
7792
8106
  return downloadAndRewriteUrls(
7793
8107
  urlSet,
7794
8108
  html,
7795
- join15(downloadDir, REMOTE_MEDIA_SUBDIR),
8109
+ join18(downloadDir, REMOTE_MEDIA_SUBDIR),
7796
8110
  "Remote background-image download failed",
7797
8111
  "Localized remote background-image(s)",
7798
8112
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -7902,7 +8216,7 @@ function projectStylesheetFontFamilies(html, projectDir) {
7902
8216
  const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
7903
8217
  const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
7904
8218
  const path = decodedUrlPath(url);
7905
- const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname7(sheet), path);
8219
+ const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname8(sheet), path);
7906
8220
  return isPathInside2(file, projectDir) && existsSync10(file);
7907
8221
  });
7908
8222
  if (family && shipsFile) families.push(family);
@@ -7944,7 +8258,7 @@ function localizeFontFaceUrls(html, downloadDir) {
7944
8258
  return downloadAndRewriteUrls(
7945
8259
  collectFontFaceUrls(html),
7946
8260
  html,
7947
- join15(downloadDir, REMOTE_MEDIA_SUBDIR),
8261
+ join18(downloadDir, REMOTE_MEDIA_SUBDIR),
7948
8262
  "Remote font download failed",
7949
8263
  "Localized remote font face(s)",
7950
8264
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -8618,10 +8932,10 @@ function adaptAspectAgnosticResolution(requested, aspectAgnostic, width, height,
8618
8932
  }
8619
8933
  function resolveEntryHtmlPath(input) {
8620
8934
  const { projectDir, workDir, entryFile, log } = input;
8621
- const htmlPath = join16(projectDir, entryFile);
8935
+ const htmlPath = join19(projectDir, entryFile);
8622
8936
  const rawEntry = readTextFile2(htmlPath);
8623
8937
  if (entryFile === "index.html" || !isTemplateEntry(rawEntry)) return htmlPath;
8624
- const projectIndexPath = join16(projectDir, "index.html");
8938
+ const projectIndexPath = join19(projectDir, "index.html");
8625
8939
  if (!existsSync11(projectIndexPath)) {
8626
8940
  throw new Error(
8627
8941
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
@@ -8637,7 +8951,7 @@ function resolveEntryHtmlPath(input) {
8637
8951
  `Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
8638
8952
  );
8639
8953
  }
8640
- const wrapperPath = join16(workDir, "standalone-entry.html");
8954
+ const wrapperPath = join19(workDir, "standalone-entry.html");
8641
8955
  writeFileSync5(wrapperPath, standaloneHtml, "utf-8");
8642
8956
  log.info("Extracted standalone entry from index.html host context", { entryFile });
8643
8957
  return wrapperPath;
@@ -8658,7 +8972,7 @@ async function runCompileStage(input) {
8658
8972
  } = input;
8659
8973
  const htmlPath = resolveEntryHtmlPath({ projectDir, workDir, entryFile, log });
8660
8974
  const compileStart = Date.now();
8661
- const compiled = await compileForRender(projectDir, htmlPath, join16(workDir, "downloads"), {
8975
+ const compiled = await compileForRender(projectDir, htmlPath, join19(workDir, "downloads"), {
8662
8976
  log,
8663
8977
  failClosedFontFetch: failClosedFontFetch === true,
8664
8978
  allowSystemFontCapture,
@@ -8666,7 +8980,7 @@ async function runCompileStage(input) {
8666
8980
  variables: input.variables,
8667
8981
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
8668
8982
  pageMotionBlur: input.pageMotionBlur,
8669
- animatedGifCacheDir: cfg.extractCacheDir ? join16(cfg.extractCacheDir, "animated-gif") : void 0,
8983
+ animatedGifCacheDir: cfg.extractCacheDir ? join19(cfg.extractCacheDir, "animated-gif") : void 0,
8670
8984
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
8671
8985
  });
8672
8986
  assertNotAborted();
@@ -8723,7 +9037,7 @@ async function runCompileStage(input) {
8723
9037
  };
8724
9038
  await preflightCompositionAssetMediaTypes({
8725
9039
  projectDir,
8726
- compiledDir: join16(workDir, "compiled"),
9040
+ compiledDir: join19(workDir, "compiled"),
8727
9041
  composition,
8728
9042
  signal: abortSignal
8729
9043
  });
@@ -8769,7 +9083,7 @@ async function runCompileStage(input) {
8769
9083
 
8770
9084
  // src/services/render/stages/probeStage.ts
8771
9085
  import { createHash as createHash7 } from "node:crypto";
8772
- import { basename as basename5, join as join17 } from "node:path";
9086
+ import { basename as basename5, join as join20 } from "node:path";
8773
9087
  import { parseHTML as parseHTML6 } from "linkedom";
8774
9088
  import {
8775
9089
  closeCaptureSession as closeCaptureSession4,
@@ -8978,7 +9292,7 @@ async function runProbeStage(input) {
8978
9292
  });
8979
9293
  fileServer = await createFileServer({
8980
9294
  projectDir,
8981
- compiledDir: join17(workDir, "compiled"),
9295
+ compiledDir: join20(workDir, "compiled"),
8982
9296
  port: 0,
8983
9297
  preHeadScripts: [VIRTUAL_TIME_SHIM],
8984
9298
  fps: job.config.fps
@@ -9008,7 +9322,7 @@ async function runProbeStage(input) {
9008
9322
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
9009
9323
  probeSession = await createCaptureSession4(
9010
9324
  fileServer.url,
9011
- join17(workDir, "probe"),
9325
+ join20(workDir, "probe"),
9012
9326
  captureOpts,
9013
9327
  null,
9014
9328
  probeCfg
@@ -9093,7 +9407,7 @@ async function runProbeStage(input) {
9093
9407
  });
9094
9408
  probeSession = await createCaptureSession4(
9095
9409
  fileServer.url,
9096
- join17(workDir, "probe-screenshot"),
9410
+ join20(workDir, "probe-screenshot"),
9097
9411
  captureOpts,
9098
9412
  null,
9099
9413
  { ...probeCfg, forceScreenshot: true }
@@ -9131,7 +9445,7 @@ async function runProbeStage(input) {
9131
9445
  compiled,
9132
9446
  resolutions,
9133
9447
  projectDir,
9134
- join17(workDir, "downloads")
9448
+ join20(workDir, "downloads")
9135
9449
  );
9136
9450
  assertNotAborted();
9137
9451
  composition.videos = compiled.videos;
@@ -9322,7 +9636,7 @@ async function runProbeStage(input) {
9322
9636
  }
9323
9637
  await preflightCompositionAssetMediaTypes({
9324
9638
  projectDir,
9325
- compiledDir: join17(workDir, "compiled"),
9639
+ compiledDir: join20(workDir, "compiled"),
9326
9640
  composition,
9327
9641
  signal: abortSignal
9328
9642
  });
@@ -9457,10 +9771,10 @@ function validateRenderDuration(input) {
9457
9771
 
9458
9772
  // src/services/render/stages/extractVideosStage.ts
9459
9773
  import { existsSync as existsSync12 } from "node:fs";
9460
- import { join as join18 } from "node:path";
9774
+ import { join as join21 } from "node:path";
9461
9775
  import {
9462
9776
  classifyVideoExtractionError,
9463
- createFrameLookupTable,
9777
+ createFrameLookupTable as createFrameLookupTable3,
9464
9778
  detectTransfer,
9465
9779
  extractAllVideoFrames,
9466
9780
  extractMediaMetadata as extractMediaMetadata3,
@@ -9472,7 +9786,7 @@ import {
9472
9786
  resolveVideoExtractionWindow as resolveVideoExtractionWindow2,
9473
9787
  isHttpUrl as isHttpUrl2,
9474
9788
  resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
9475
- runVideoExtractionWithRetry,
9789
+ runVideoExtractionWithRetry as runVideoExtractionWithRetry2,
9476
9790
  safeVideoExtractionSourceIdentity,
9477
9791
  videoAudioId as videoAudioId2
9478
9792
  } from "@hyperframes/engine";
@@ -9728,7 +10042,7 @@ async function runExtractVideosStage(input) {
9728
10042
  const videoPath = resolveProjectRelativeSrc3(v.src, projectDir, compiledDir);
9729
10043
  if (!existsSync12(videoPath)) return null;
9730
10044
  try {
9731
- const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await runVideoExtractionWithRetry(() => extractMediaMetadata3(videoPath), {
10045
+ const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await runVideoExtractionWithRetry2(() => extractMediaMetadata3(videoPath), {
9732
10046
  signal: abortSignal,
9733
10047
  maxTransientRetries: extractionPolicy.maxTransientRetries,
9734
10048
  onRetry: () => {
@@ -9798,14 +10112,14 @@ async function runExtractVideosStage(input) {
9798
10112
  // because short boundary counts can differ by one frame.
9799
10113
  {
9800
10114
  fps: job.config.fps,
9801
- outputDir: join18(compiledDir, "__hyperframes_video_frames"),
10115
+ outputDir: join21(compiledDir, "__hyperframes_video_frames"),
9802
10116
  format: job.config.videoFrameFormat ?? "auto",
9803
10117
  toneMapHdrToSdr: rendersSdrOnly(job.config, composition),
9804
10118
  timelineEnd: composition.duration,
9805
10119
  maxTransientRetries: extractionPolicy.maxTransientRetries,
9806
10120
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
9807
10121
  deferRangeExtraction,
9808
- contentKeyedDirs: [join18(compiledDir, REMOTE_MEDIA_SUBDIR)],
10122
+ contentKeyedDirs: [join21(compiledDir, REMOTE_MEDIA_SUBDIR)],
9809
10123
  urlByAbsentLocalPath,
9810
10124
  onProgress: ({ planned = 0, done = 0 }) => {
9811
10125
  framesPlanned += planned;
@@ -9825,7 +10139,7 @@ async function runExtractVideosStage(input) {
9825
10139
  materializeSymlinks
9826
10140
  });
9827
10141
  if (extractionResult.extracted.length > 0) {
9828
- frameLookup = createFrameLookupTable(
10142
+ frameLookup = createFrameLookupTable3(
9829
10143
  composition.videos,
9830
10144
  extractionResult.extracted,
9831
10145
  resolveRenderFpsConfig(job.config.fps).value
@@ -9988,7 +10302,7 @@ async function planVideoLayerPassthrough(input) {
9988
10302
 
9989
10303
  // src/services/render/stages/audioStage.ts
9990
10304
  import { existsSync as existsSync15 } from "node:fs";
9991
- import { join as join21 } from "node:path";
10305
+ import { join as join24 } from "node:path";
9992
10306
  import {
9993
10307
  MIXED_AUDIO_FILENAME,
9994
10308
  audioFailureDiagnostics,
@@ -9999,7 +10313,7 @@ import {
9999
10313
 
10000
10314
  // src/services/render/audioDeliveryLimit.ts
10001
10315
  import { existsSync as existsSync14 } from "node:fs";
10002
- import { dirname as dirname9, join as join20 } from "node:path";
10316
+ import { dirname as dirname10, join as join23 } from "node:path";
10003
10317
  import {
10004
10318
  applyAudioFxChain,
10005
10319
  describeFfmpegFailure as describeFfmpegFailure3,
@@ -10010,7 +10324,7 @@ import {
10010
10324
  // src/services/render/audioPadTrim.ts
10011
10325
  import { spawn } from "node:child_process";
10012
10326
  import { mkdtempSync as mkdtempSync3 } from "node:fs";
10013
- import { dirname as dirname8, join as join19 } from "node:path";
10327
+ import { dirname as dirname9, join as join22 } from "node:path";
10014
10328
  import {
10015
10329
  DEFAULT_CONFIG,
10016
10330
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -10289,7 +10603,7 @@ async function constrainAacTruePeak(input) {
10289
10603
  }
10290
10604
  async function cutSource(input, scratchDir, attenuationDb) {
10291
10605
  if (!input.sourcePath || !input.rewriteSource) return true;
10292
- const cutPath = join19(scratchDir, "source.wav");
10606
+ const cutPath = join22(scratchDir, "source.wav");
10293
10607
  try {
10294
10608
  const result = await input.rewriteSource(input.sourcePath, cutPath, gainFilter(attenuationDb));
10295
10609
  if (!result.success) throw new Error(result.error);
@@ -10303,8 +10617,8 @@ async function cutSource(input, scratchDir, attenuationDb) {
10303
10617
  async function enforceAacTruePeak(input) {
10304
10618
  let scratchDir;
10305
10619
  try {
10306
- scratchDir = mkdtempSync3(join19(dirname8(input.audioPath), ".true-peak-"));
10307
- const correctedPath = join19(scratchDir, "audio.m4a");
10620
+ scratchDir = mkdtempSync3(join22(dirname9(input.audioPath), ".true-peak-"));
10621
+ const correctedPath = join22(scratchDir, "audio.m4a");
10308
10622
  const measurements = [];
10309
10623
  let attenuationDb = 0;
10310
10624
  let sourcePeakDbfs;
@@ -10495,11 +10809,13 @@ async function runFfprobeJson(args, signal) {
10495
10809
  }
10496
10810
 
10497
10811
  // src/services/render/audioDeliveryLimit.ts
10498
- var LIMIT_CEILING_DBTP = AAC_DELIVERY_TRUE_PEAK_DBFS - 2;
10499
- var LIMIT_CHAIN = {
10812
+ var LIMIT_PASSES = 3;
10813
+ var LOWEST_CEILING_DBTP = AAC_DELIVERY_TRUE_PEAK_DBFS - 2;
10814
+ var PASS_HEADROOM_DB = 0.05;
10815
+ var limitChain = (ceiling) => ({
10500
10816
  version: 1,
10501
- nodes: [{ type: "truepeak", id: "delivery", params: { ceiling: LIMIT_CEILING_DBTP } }]
10502
- };
10817
+ nodes: [{ type: "truepeak", id: "delivery", params: { ceiling } }]
10818
+ });
10503
10819
  async function limitDeliveryPeaks(input, deps = {
10504
10820
  measure: measureTruePeakDbfs,
10505
10821
  applyChain: applyAudioFxChain,
@@ -10511,15 +10827,39 @@ async function limitDeliveryPeaks(input, deps = {
10511
10827
  if (!deps.sourceExists(sourcePath)) return { success: true };
10512
10828
  const peak = await deps.measure(audioPath, { timeoutMs, signal });
10513
10829
  if (peak <= AAC_DELIVERY_TRUE_PEAK_DBFS) return { success: true };
10514
- const limitedPath = join20(dirname9(sourcePath), "audio-source-limited.wav");
10830
+ const alternating = ["a", "b"].map(
10831
+ (k) => join23(dirname10(sourcePath), `audio-source-limited-${k}.wav`)
10832
+ );
10833
+ let encodedFrom;
10834
+ let ceiling = AAC_DELIVERY_TRUE_PEAK_DBFS;
10835
+ for (let pass = 0; pass < LIMIT_PASSES; pass++) {
10836
+ const limitedPath = alternating[pass % 2];
10837
+ const result = await limitPass(input, deps, ceiling, limitedPath);
10838
+ if ("encodeFailure" in result) return result.encodeFailure;
10839
+ if ("limitFailed" in result) break;
10840
+ encodedFrom = limitedPath;
10841
+ if (result.peak <= AAC_DELIVERY_TRUE_PEAK_DBFS || ceiling <= LOWEST_CEILING_DBTP) break;
10842
+ ceiling = Math.max(
10843
+ ceiling - (result.peak - AAC_DELIVERY_TRUE_PEAK_DBFS) - PASS_HEADROOM_DB,
10844
+ LOWEST_CEILING_DBTP
10845
+ );
10846
+ }
10847
+ if (encodedFrom) deps.rename(encodedFrom, sourcePath, { copyFallback: true });
10848
+ return { success: true };
10849
+ }
10850
+ async function limitPass(input, deps, ceiling, limitedPath) {
10851
+ const { audioPath, sourcePath, signal, timeoutMs } = input;
10515
10852
  try {
10516
- await deps.applyChain(sourcePath, LIMIT_CHAIN, limitedPath, { trackId: "delivery", signal });
10853
+ await deps.applyChain(sourcePath, limitChain(ceiling), limitedPath, {
10854
+ trackId: "delivery",
10855
+ signal
10856
+ });
10517
10857
  } catch (error) {
10518
10858
  if (signal?.aborted) throw error;
10519
10859
  input.warn?.("[audio] true-peak limit failed, the whole-mix cut applies", {
10520
10860
  error: error instanceof Error ? error.message : String(error)
10521
10861
  });
10522
- return { success: true };
10862
+ return { limitFailed: true };
10523
10863
  }
10524
10864
  const encoded = await deps.encode(limitedPath, audioPath, input.targetDurationSeconds, {
10525
10865
  signal,
@@ -10527,13 +10867,14 @@ async function limitDeliveryPeaks(input, deps = {
10527
10867
  });
10528
10868
  if (!encoded.success) {
10529
10869
  return {
10530
- success: false,
10531
- error: `[audioDeliveryLimit] ${describeFfmpegFailure3(encoded, timeoutMs)}`,
10532
- failureReason: encoded.failureReason
10870
+ encodeFailure: {
10871
+ success: false,
10872
+ error: `[audioDeliveryLimit] ${describeFfmpegFailure3(encoded, timeoutMs)}`,
10873
+ failureReason: encoded.failureReason
10874
+ }
10533
10875
  };
10534
10876
  }
10535
- deps.rename(limitedPath, sourcePath, { copyFallback: true });
10536
- return { success: true };
10877
+ return { peak: await deps.measure(audioPath, { timeoutMs, signal }) };
10537
10878
  }
10538
10879
 
10539
10880
  // src/services/render/stages/audioStage.ts
@@ -10559,7 +10900,7 @@ function runAudioStage(input) {
10559
10900
  }
10560
10901
  async function runAudioStageScoped(input) {
10561
10902
  const stageStart = Date.now();
10562
- const pcmSourcePath = join21(input.workDir, "audio-source.wav");
10903
+ const pcmSourcePath = join24(input.workDir, "audio-source.wav");
10563
10904
  const duration = input.totalFrames * input.fps.den / input.fps.num;
10564
10905
  let deliverySourcePath;
10565
10906
  try {
@@ -10624,7 +10965,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10624
10965
  assertNotAborted,
10625
10966
  log
10626
10967
  } = input;
10627
- const audioOutputPath = join21(workDir, MIXED_AUDIO_FILENAME);
10968
+ const audioOutputPath = join24(workDir, MIXED_AUDIO_FILENAME);
10628
10969
  let hasAudio = false;
10629
10970
  let audioError;
10630
10971
  let audioFailures;
@@ -10635,7 +10976,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10635
10976
  audioResult = await processCompositionAudio(
10636
10977
  audios,
10637
10978
  projectDir,
10638
- join21(workDir, "audio-work"),
10979
+ join24(workDir, "audio-work"),
10639
10980
  audioOutputPath,
10640
10981
  duration,
10641
10982
  abortSignal,
@@ -10703,8 +11044,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10703
11044
  }
10704
11045
 
10705
11046
  // src/services/render/stages/captureStage.ts
10706
- import { existsSync as existsSync16, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
10707
- import { join as join22 } from "node:path";
11047
+ import { existsSync as existsSync16, readdirSync as readdirSync4, statfsSync as statfsSync2 } from "node:fs";
11048
+ import { join as join25 } from "node:path";
10708
11049
  import {
10709
11050
  classifyCaptureFailure as classifyCaptureFailure2,
10710
11051
  frameFileExtension,
@@ -10775,15 +11116,15 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
10775
11116
  function measureCaptureFrameBytes(workDir, framesDir) {
10776
11117
  let total = sampleDirectoryBytes(framesDir);
10777
11118
  if (!existsSync16(workDir)) return total;
10778
- for (const attempt of readdirSync3(workDir)) {
11119
+ for (const attempt of readdirSync4(workDir)) {
10779
11120
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
10780
- total += sampleDirectoryBytes(join22(workDir, attempt));
11121
+ total += sampleDirectoryBytes(join25(workDir, attempt));
10781
11122
  continue;
10782
11123
  }
10783
11124
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
10784
- const attemptDir = join22(workDir, attempt);
10785
- for (const name of readdirSync3(attemptDir)) {
10786
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join22(attemptDir, name));
11125
+ const attemptDir = join25(workDir, attempt);
11126
+ for (const name of readdirSync4(attemptDir)) {
11127
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join25(attemptDir, name));
10787
11128
  }
10788
11129
  }
10789
11130
  return total;
@@ -11077,8 +11418,8 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
11077
11418
  }
11078
11419
 
11079
11420
  // src/services/render/stages/captureHdrStage.ts
11080
- import { existsSync as existsSync18, mkdirSync as mkdirSync8 } from "node:fs";
11081
- import { join as join26 } from "node:path";
11421
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10 } from "node:fs";
11422
+ import { join as join29 } from "node:path";
11082
11423
  import {
11083
11424
  closeCaptureSession as closeCaptureSession7,
11084
11425
  cloneCaptureWarnings as cloneCaptureWarnings2,
@@ -11150,7 +11491,7 @@ function createHdrImageTransferCache(options = {}) {
11150
11491
  }
11151
11492
 
11152
11493
  // src/services/render/stages/captureHdrSequentialLoop.ts
11153
- import { join as join23 } from "node:path";
11494
+ import { join as join26 } from "node:path";
11154
11495
  import {
11155
11496
  TRANSITIONS,
11156
11497
  crossfade
@@ -11275,7 +11616,7 @@ async function runSequentialLayeredFrameLoop(input) {
11275
11616
  );
11276
11617
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
11277
11618
  writeFileExclusiveSync(
11278
- join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
11619
+ join26(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
11279
11620
  normalCanvas
11280
11621
  );
11281
11622
  }
@@ -11307,7 +11648,7 @@ async function runSequentialLayeredFrameLoop(input) {
11307
11648
  }
11308
11649
 
11309
11650
  // src/services/render/stages/captureHdrHybridLoop.ts
11310
- import { join as join25 } from "node:path";
11651
+ import { join as join28 } from "node:path";
11311
11652
  import {
11312
11653
  TRANSITIONS as TRANSITIONS2,
11313
11654
  closeCaptureSession as closeCaptureSession6,
@@ -11321,7 +11662,7 @@ import {
11321
11662
  // src/services/shaderTransitionWorkerPool.ts
11322
11663
  import { Worker } from "node:worker_threads";
11323
11664
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
11324
- import { dirname as dirname10, join as join24 } from "node:path";
11665
+ import { dirname as dirname11, join as join27 } from "node:path";
11325
11666
  import { createRequire } from "node:module";
11326
11667
  import { existsSync as existsSync17 } from "node:fs";
11327
11668
  import { cpus as cpus2 } from "node:os";
@@ -11334,10 +11675,10 @@ function resolveWorkerEntry(explicit) {
11334
11675
  const isTs = override.endsWith(".ts");
11335
11676
  return { path: override, isTs };
11336
11677
  }
11337
- const moduleDir = dirname10(fileURLToPath3(import.meta.url));
11338
- const jsPath = join24(moduleDir, "shaderTransitionWorker.js");
11678
+ const moduleDir = dirname11(fileURLToPath3(import.meta.url));
11679
+ const jsPath = join27(moduleDir, "shaderTransitionWorker.js");
11339
11680
  if (existsSync17(jsPath)) return { path: jsPath, isTs: false };
11340
- const tsPath = join24(moduleDir, "shaderTransitionWorker.ts");
11681
+ const tsPath = join27(moduleDir, "shaderTransitionWorker.ts");
11341
11682
  return { path: tsPath, isTs: true };
11342
11683
  }
11343
11684
  function buildExecArgv(entryIsTs) {
@@ -11705,7 +12046,7 @@ async function runHybridLayeredFrameLoop(input) {
11705
12046
  );
11706
12047
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
11707
12048
  writeFileExclusiveSync(
11708
- join25(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
12049
+ join28(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
11709
12050
  canvas
11710
12051
  );
11711
12052
  }
@@ -11896,9 +12237,9 @@ async function runCaptureHdrStage(input) {
11896
12237
  if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
11897
12238
  }
11898
12239
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
11899
- const debugDumpDir = debugDumpEnabled ? join26(framesDir, "debug-composite") : null;
12240
+ const debugDumpDir = debugDumpEnabled ? join29(framesDir, "debug-composite") : null;
11900
12241
  if (debugDumpDir && !existsSync18(debugDumpDir)) {
11901
- mkdirSync8(debugDumpDir, { recursive: true });
12242
+ mkdirSync10(debugDumpDir, { recursive: true });
11902
12243
  }
11903
12244
  const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
11904
12245
  const hdrTargetTransfer = compositeTransfer === "srgb" ? void 0 : compositeTransfer;
@@ -12055,15 +12396,15 @@ async function runCaptureHdrStage(input) {
12055
12396
  import {
12056
12397
  copyFileSync as copyFileSync4,
12057
12398
  existsSync as existsSync19,
12058
- mkdirSync as mkdirSync9,
12059
- readdirSync as readdirSync4,
12399
+ mkdirSync as mkdirSync11,
12400
+ readdirSync as readdirSync5,
12060
12401
  readFileSync as readFileSync8,
12061
- rmSync as rmSync9,
12062
- statSync as statSync7,
12402
+ rmSync as rmSync11,
12403
+ statSync as statSync8,
12063
12404
  writeFileSync as writeFileSync6
12064
12405
  } from "node:fs";
12065
12406
  import { readFile as readFile2 } from "node:fs/promises";
12066
- import { dirname as dirname11, join as join27 } from "node:path";
12407
+ import { dirname as dirname12, join as join30 } from "node:path";
12067
12408
  import {
12068
12409
  encodeFramesChunkedConcat,
12069
12410
  encodeFramesFromDir,
@@ -12161,7 +12502,7 @@ async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
12161
12502
  }
12162
12503
  async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12163
12504
  const startTime = Date.now();
12164
- const files = readdirSync4(framesDir).filter((file) => file.match(/\.(jpg|jpeg|png)$/i));
12505
+ const files = readdirSync5(framesDir).filter((file) => file.match(/\.(jpg|jpeg|png)$/i));
12165
12506
  const frameCount = files.length;
12166
12507
  if (frameCount === 0) {
12167
12508
  return gifEncodeFailure(startTime, outputPath, "[FFmpeg] No frame files found in directory");
@@ -12188,7 +12529,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12188
12529
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
12189
12530
  if (failure) return failure;
12190
12531
  }
12191
- const fileSize = existsSync19(outputPath) ? statSync7(outputPath).size : 0;
12532
+ const fileSize = existsSync19(outputPath) ? statSync8(outputPath).size : 0;
12192
12533
  return {
12193
12534
  success: true,
12194
12535
  outputPath,
@@ -12197,7 +12538,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12197
12538
  fileSize
12198
12539
  };
12199
12540
  } finally {
12200
- rmSync9(input.palettePath, { force: true });
12541
+ rmSync11(input.palettePath, { force: true });
12201
12542
  }
12202
12543
  }
12203
12544
  function startEncodeProgress(job, stage, onProgress) {
@@ -12274,7 +12615,7 @@ function encodeWhileCapturing(input, totalFrames) {
12274
12615
  await new Promise((resolve11) => wake = resolve11);
12275
12616
  }
12276
12617
  if (signal.aborted) return;
12277
- const mergedPath = join27(input.framesDir, formatCaptureFrameName(next, "jpg"));
12618
+ const mergedPath = join30(input.framesDir, formatCaptureFrameName(next, "jpg"));
12278
12619
  const frame = await readLandedFrame(landed.get(next), mergedPath);
12279
12620
  landed.delete(next);
12280
12621
  if (!await stream.writeFrame(frame)) {
@@ -12349,19 +12690,19 @@ async function runEncodeStage(input) {
12349
12690
  const stage5Start = Date.now();
12350
12691
  if (isPngSequence) {
12351
12692
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
12352
- if (!existsSync19(outputPath)) mkdirSync9(outputPath, { recursive: true });
12353
- const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
12693
+ if (!existsSync19(outputPath)) mkdirSync11(outputPath, { recursive: true });
12694
+ const captured = readdirSync5(framesDir).filter((name) => name.endsWith(".png")).sort();
12354
12695
  if (captured.length === 0) {
12355
12696
  throw new Error(
12356
12697
  `[Render] png-sequence output requested but no PNGs were captured to ${framesDir}`
12357
12698
  );
12358
12699
  }
12359
12700
  captured.forEach((name, i) => {
12360
- const dst = join27(outputPath, formatExportFrameName(i, "png"));
12361
- copyFileSync4(join27(framesDir, name), dst);
12701
+ const dst = join30(outputPath, formatExportFrameName(i, "png"));
12702
+ copyFileSync4(join30(framesDir, name), dst);
12362
12703
  });
12363
12704
  if (hasAudio && audioOutputPath && existsSync19(audioOutputPath)) {
12364
- copyFileSync4(audioOutputPath, join27(outputPath, MIXED_AUDIO_FILENAME2));
12705
+ copyFileSync4(audioOutputPath, join30(outputPath, MIXED_AUDIO_FILENAME2));
12365
12706
  log.info(
12366
12707
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
12367
12708
  );
@@ -12381,7 +12722,7 @@ async function runEncodeStage(input) {
12381
12722
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
12382
12723
  fps: job.config.fps,
12383
12724
  loop,
12384
- palettePath: join27(dirname11(videoOnlyPath), "gif-palette.png"),
12725
+ palettePath: join30(dirname12(videoOnlyPath), "gif-palette.png"),
12385
12726
  preserveAlpha: needsAlpha,
12386
12727
  signal: abortSignal,
12387
12728
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -12587,14 +12928,14 @@ function sampleDirectoryBytes(dir) {
12587
12928
  if (!current) continue;
12588
12929
  let entries = [];
12589
12930
  try {
12590
- entries = readdirSync5(current);
12931
+ entries = readdirSync6(current);
12591
12932
  } catch {
12592
12933
  continue;
12593
12934
  }
12594
12935
  for (const name of entries) {
12595
- const full = join28(current, name);
12936
+ const full = join31(current, name);
12596
12937
  try {
12597
- const st = statSync8(full);
12938
+ const st = statSync9(full);
12598
12939
  if (st.isDirectory()) {
12599
12940
  stack.push(full);
12600
12941
  } else if (st.isFile()) {
@@ -12748,8 +13089,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
12748
13089
  const ranges = [];
12749
13090
  let rangeStart = null;
12750
13091
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12751
- const framePath = join28(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12752
- const missing = !existsSync20(framePath) || statSync8(framePath).size <= 8;
13092
+ const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
13093
+ const missing = !existsSync20(framePath) || statSync9(framePath).size <= 8;
12753
13094
  if (missing && rangeStart === null) {
12754
13095
  rangeStart = frameIndex;
12755
13096
  } else if (!missing && rangeStart !== null) {
@@ -12765,7 +13106,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
12765
13106
  function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
12766
13107
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
12767
13108
  const batches = [];
12768
- const outputDirFor = (batchIndex, workerId) => join28(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
13109
+ const outputDirFor = (batchIndex, workerId) => join31(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
12769
13110
  if (stride > 1) {
12770
13111
  const spans = /* @__PURE__ */ new Map();
12771
13112
  for (const range of ranges) {
@@ -12825,8 +13166,8 @@ function getNextRetryWorkerCount(currentWorkers) {
12825
13166
  return Math.max(1, Math.floor(currentWorkers / 2));
12826
13167
  }
12827
13168
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
12828
- if (platform2 === "win32" || platform2 === "darwin") return join28(systemTempDir, "hf-render-");
12829
- return join28(dirname12(outputPath), `work-${jobId}-`);
13169
+ if (platform2 === "win32" || platform2 === "darwin") return join31(systemTempDir, "hf-render-");
13170
+ return join31(dirname13(outputPath), `work-${jobId}-`);
12830
13171
  }
12831
13172
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
12832
13173
  function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
@@ -12873,7 +13214,7 @@ The composition is too large for the available memory. To reduce memory pressure
12873
13214
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
12874
13215
  let captured = 0;
12875
13216
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12876
- const framePath = join28(framesDir, formatCaptureFrameName(frameIndex, frameExt));
13217
+ const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12877
13218
  if (existsSync20(framePath)) captured++;
12878
13219
  }
12879
13220
  return captured;
@@ -12899,7 +13240,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
12899
13240
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
12900
13241
  });
12901
13242
  pendingTransientRetry = false;
12902
- const attemptWorkDir = join28(options.workDir, `capture-attempt-${attempt}`);
13243
+ const attemptWorkDir = join31(options.workDir, `capture-attempt-${attempt}`);
12903
13244
  const batches = missingRanges ? buildMissingFrameRetryBatches(
12904
13245
  missingRanges,
12905
13246
  currentWorkers,
@@ -13531,19 +13872,19 @@ function deVerifyFallbackTelemetry(err) {
13531
13872
  };
13532
13873
  }
13533
13874
  function resolveRenderDebugDir() {
13534
- const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname12(fileURLToPath4(import.meta.url)), "../..");
13535
- return join28(producerRoot, ".debug");
13875
+ const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname13(fileURLToPath4(import.meta.url)), "../..");
13876
+ return join31(producerRoot, ".debug");
13536
13877
  }
13537
13878
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
13538
13879
  validateHlsRenderConfig(job.config);
13539
13880
  validateDeliveryEncoderConfig(job.config);
13540
13881
  const debugDir = resolveRenderDebugDir();
13541
- const outputDir = dirname12(outputPath);
13542
- if (!existsSync20(outputDir)) mkdirSync10(outputDir, { recursive: true });
13543
- const workDir = job.config.debug ? join28(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
13882
+ const outputDir = dirname13(outputPath);
13883
+ if (!existsSync20(outputDir)) mkdirSync12(outputDir, { recursive: true });
13884
+ const workDir = job.config.debug ? join31(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
13544
13885
  const pipelineStart = Date.now();
13545
13886
  const baseLog = job.config.logger ?? defaultLogger;
13546
- const logPath = job.config.debug ? join28(workDir, "render.log") : null;
13887
+ const logPath = job.config.debug ? join31(workDir, "render.log") : null;
13547
13888
  const execution = new RenderExecutionContext({
13548
13889
  request: { renderJobId: job.id, projectDir, outputPath },
13549
13890
  logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
@@ -13557,7 +13898,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
13557
13898
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
13558
13899
  return;
13559
13900
  }
13560
- rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
13901
+ rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
13561
13902
  });
13562
13903
  try {
13563
13904
  await executeRenderPipeline({
@@ -13605,7 +13946,7 @@ async function executeRenderPipeline(input) {
13605
13946
  imageDecodeFailures: 0
13606
13947
  };
13607
13948
  let hdrPerf;
13608
- const perfOutputPath = join28(workDir, "perf-summary.json");
13949
+ const perfOutputPath = join31(workDir, "perf-summary.json");
13609
13950
  const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
13610
13951
  const observability = new RenderObservabilityRecorder({
13611
13952
  pipelineStartMs: pipelineStart,
@@ -13681,7 +14022,7 @@ async function executeRenderPipeline(input) {
13681
14022
  job.startedAt = /* @__PURE__ */ new Date();
13682
14023
  assertNotAborted();
13683
14024
  assertConfiguredFfmpegBinariesExist();
13684
- if (!existsSync20(workDir)) mkdirSync10(workDir, { recursive: true });
14025
+ if (!existsSync20(workDir)) mkdirSync12(workDir, { recursive: true });
13685
14026
  if (job.config.debug) {
13686
14027
  log.info("[Render] Debug artifacts enabled", { workDir, logPath });
13687
14028
  }
@@ -13710,7 +14051,7 @@ async function executeRenderPipeline(input) {
13710
14051
  requestedWorkers: job.config.workers ?? "auto"
13711
14052
  });
13712
14053
  const entryFile = job.config.entryFile || "index.html";
13713
- const htmlPath = join28(projectDir, entryFile);
14054
+ const htmlPath = join31(projectDir, entryFile);
13714
14055
  if (!existsSync20(htmlPath)) {
13715
14056
  throw new Error(`Entry file not found: ${htmlPath}`);
13716
14057
  }
@@ -13887,7 +14228,7 @@ async function executeRenderPipeline(input) {
13887
14228
  canvas: { width, height },
13888
14229
  page: probeSession.page,
13889
14230
  projectDir,
13890
- compiledDir: join28(workDir, "compiled")
14231
+ compiledDir: join31(workDir, "compiled")
13891
14232
  });
13892
14233
  if (videoLayerPlan.videoId === void 0) {
13893
14234
  log.info(`[Render] Video layer passthrough off: ${videoLayerPlan.reason}`);
@@ -13905,7 +14246,22 @@ async function executeRenderPipeline(input) {
13905
14246
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
13906
14247
  code: "extract_video"
13907
14248
  });
13908
- const compiledDir = join28(workDir, "compiled");
14249
+ const compiledDir = join31(workDir, "compiled");
14250
+ const segmentsCapture = (layeredOrEffectRoute) => shouldSegmentCapture({
14251
+ env: process.env,
14252
+ durationSeconds: probeResult.duration,
14253
+ outputFormat,
14254
+ layeredOrEffectRoute,
14255
+ streamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, probeResult.duration)
14256
+ });
14257
+ const segmentedForWindow = segmentsCapture(compiled.hasShaderTransitions);
14258
+ const windowVideoFrames = job.config.motionBlur === void 0 && shouldWindowVideoFrames({ env: process.env, segmented: segmentedForWindow });
14259
+ if (!windowVideoFrames && shouldWindowVideoFrames({ env: process.env, segmented: true })) {
14260
+ const reason = job.config.motionBlur !== void 0 ? "motion blur" : outputFormat !== "mp4" && outputFormat !== "mov" ? `${outputFormat} output` : "capture is not segmented";
14261
+ log.info("[Render] HF_VIDEO_FRAME_WINDOW is set but this render extracts every frame", {
14262
+ reason
14263
+ });
14264
+ }
13909
14265
  const extractResult = await observeRenderStage(
13910
14266
  observability,
13911
14267
  "video_extract",
@@ -13922,6 +14278,7 @@ async function executeRenderPipeline(input) {
13922
14278
  assertNotAborted,
13923
14279
  // Local staging can use links; distributed plan() alone requires real copies.
13924
14280
  materializeSymlinks: false,
14281
+ deferRangeExtraction: windowVideoFrames ? true : void 0,
13925
14282
  onProgress
13926
14283
  })
13927
14284
  );
@@ -14018,7 +14375,7 @@ async function executeRenderPipeline(input) {
14018
14375
  try {
14019
14376
  fileServer = await createFileServer({
14020
14377
  projectDir,
14021
- compiledDir: join28(workDir, "compiled"),
14378
+ compiledDir: join31(workDir, "compiled"),
14022
14379
  port: 0,
14023
14380
  preHeadScripts: [VIRTUAL_TIME_SHIM],
14024
14381
  fps: job.config.fps
@@ -14036,8 +14393,8 @@ async function executeRenderPipeline(input) {
14036
14393
  if (!activeFileServer) {
14037
14394
  throw new Error("File server failed to initialize before frame capture");
14038
14395
  }
14039
- const framesDir = join28(workDir, "captured-frames");
14040
- if (!existsSync20(framesDir)) mkdirSync10(framesDir, { recursive: true });
14396
+ const framesDir = join31(workDir, "captured-frames");
14397
+ if (!existsSync20(framesDir)) mkdirSync12(framesDir, { recursive: true });
14041
14398
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
14042
14399
  code: "start_browsers"
14043
14400
  });
@@ -14115,7 +14472,55 @@ async function executeRenderPipeline(input) {
14115
14472
  });
14116
14473
  const buildPrepCaptureOptions = () => stopPrepSessionOnScriptFailure(buildCaptureOptions());
14117
14474
  const buildWorkerCaptureOptions = () => withScriptFailureStop(buildCaptureOptions(), job, log);
14118
- const createRenderVideoFrameInjector = () => createVideoFrameInjector(frameLookup, frameCacheOptions(cfg, workerCount));
14475
+ let videoFrameWindow = windowVideoFrames && (extractionResult?.extracted.some((video) => video.deferredRange) ?? false);
14476
+ if (windowVideoFrames) {
14477
+ const extracted = extractionResult?.extracted ?? [];
14478
+ log.info("[Render] Video frame window", {
14479
+ windowed: extracted.filter((video) => video.deferredRange).length,
14480
+ fullyExtracted: extracted.filter((video) => !video.deferredRange).length
14481
+ });
14482
+ }
14483
+ const buildSegmentVideoFrames = () => trackSegmentFramePeak(
14484
+ createSegmentVideoFrames({
14485
+ videos: composition.videos,
14486
+ extracted: extractionResult?.extracted ?? [],
14487
+ fps: job.config.fps,
14488
+ rootDir: workDir,
14489
+ cfg,
14490
+ injectorOptions: frameCacheOptions(cfg, workerCount),
14491
+ abortSignal: executionSignal
14492
+ }),
14493
+ workDir,
14494
+ (bytes) => updateCaptureObservability({ videoFrameWindowPeakBytes: bytes })
14495
+ );
14496
+ const createRenderVideoFrameInjector = () => createVideoFrameInjector2(frameLookup, frameCacheOptions(cfg, workerCount));
14497
+ const extractDeferredVideoFrames = async () => {
14498
+ videoFrameWindow = false;
14499
+ updateCaptureObservability({ videoFrameWindow: "off" });
14500
+ const extracted = extractionResult?.extracted ?? [];
14501
+ const videoCount = extracted.filter((video) => video.deferredRange).length;
14502
+ const startedAt = Date.now();
14503
+ log.info("[Render] Extracting the frames of the videos a window would have held back", {
14504
+ videoCount
14505
+ });
14506
+ await observeRenderStage(
14507
+ observability,
14508
+ "video_extract_deferred",
14509
+ { videoCount },
14510
+ () => extractAllDeferredFrames(extracted, {
14511
+ fps: job.config.fps,
14512
+ cfg,
14513
+ signal: executionSignal,
14514
+ maxTransientRetries: resolveVideoExtractionPolicy().maxTransientRetries,
14515
+ onProgress: (done, total) => reportExtractProgress(job, done, total, onProgress)
14516
+ })
14517
+ );
14518
+ assertNotAborted();
14519
+ log.info("[Render] Extracted the deferred video frames", {
14520
+ videoCount,
14521
+ elapsedMs: Date.now() - startedAt
14522
+ });
14523
+ };
14119
14524
  let captureCalibration;
14120
14525
  const htmlInCanvasDetected = compiled.renderModeHints.reasons.some(
14121
14526
  (r) => r.code === "htmlInCanvas"
@@ -14281,32 +14686,41 @@ async function executeRenderPipeline(input) {
14281
14686
  updateJobStatus(job, "rendering", "Measuring capture speed", 25, onProgress, {
14282
14687
  code: "start_browsers"
14283
14688
  });
14284
- const outcome = await observeRenderStage(
14285
- observability,
14286
- "capture_calibration",
14287
- captureStageObservationData({
14288
- forceScreenshot: captureForceScreenshot,
14289
- // Override the default `capturing` — calibration writes probe
14290
- // frames only, not `job.framesRendered`, so heartbeats reporting
14291
- // `framesCompleted: 0` misread as broken. Field signal
14292
- // ts=1784019503.
14293
- stagePhase: "calibrating"
14294
- }),
14295
- () => runCaptureCalibration({
14296
- cfg: calibrationCaptureConfig(cfg),
14297
- fileServer: activeFileServer,
14298
- workDir,
14299
- log,
14300
- job,
14301
- totalFrames,
14302
- forceScreenshot: captureForceScreenshot,
14303
- probeSession,
14304
- buildCaptureOptions: buildPrepCaptureOptions,
14305
- createRenderVideoFrameInjector,
14306
- assertNotAborted
14307
- }),
14308
- { heartbeatMessage: "browser calibrating (frames not started)" }
14309
- );
14689
+ const calibrationFrames = videoFrameWindow ? await buildSegmentVideoFrames().forFrames(
14690
+ selectCaptureCalibrationFrames(totalFrames),
14691
+ "calibration"
14692
+ ) : void 0;
14693
+ let outcome;
14694
+ try {
14695
+ outcome = await observeRenderStage(
14696
+ observability,
14697
+ "capture_calibration",
14698
+ captureStageObservationData({
14699
+ forceScreenshot: captureForceScreenshot,
14700
+ // Override the default `capturing` — calibration writes probe
14701
+ // frames only, not `job.framesRendered`, so heartbeats reporting
14702
+ // `framesCompleted: 0` misread as broken. Field signal
14703
+ // ts=1784019503.
14704
+ stagePhase: "calibrating"
14705
+ }),
14706
+ () => runCaptureCalibration({
14707
+ cfg: calibrationCaptureConfig(cfg),
14708
+ fileServer: activeFileServer,
14709
+ workDir,
14710
+ log,
14711
+ job,
14712
+ totalFrames,
14713
+ forceScreenshot: captureForceScreenshot,
14714
+ probeSession,
14715
+ buildCaptureOptions: buildPrepCaptureOptions,
14716
+ createRenderVideoFrameInjector: calibrationFrames ? calibrationFrames.injector : createRenderVideoFrameInjector,
14717
+ assertNotAborted
14718
+ }),
14719
+ { heartbeatMessage: "browser calibrating (frames not started)" }
14720
+ );
14721
+ } finally {
14722
+ calibrationFrames?.release();
14723
+ }
14310
14724
  captureCalibration = outcome.calibration;
14311
14725
  calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
14312
14726
  captureForceScreenshot = outcome.forceScreenshot;
@@ -14538,7 +14952,7 @@ async function executeRenderPipeline(input) {
14538
14952
  hls: ".mp4"
14539
14953
  };
14540
14954
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
14541
- const videoOnlyPath = join28(workDir, `video-only${videoExt}`);
14955
+ const videoOnlyPath = join31(workDir, `video-only${videoExt}`);
14542
14956
  const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
14543
14957
  if (usePageSideCompositingForTransitions) {
14544
14958
  activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
@@ -14639,13 +15053,7 @@ async function executeRenderPipeline(input) {
14639
15053
  // excluded routes are the ones whose concat-copy or capture loop the
14640
15054
  // segment contract does not cover (webm VP9 concat is fragile, HDR and
14641
15055
  // shader transitions run their own compositor).
14642
- useSegmentedCapture: shouldSegmentCapture({
14643
- env: process.env,
14644
- durationSeconds: job.duration,
14645
- outputFormat,
14646
- layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
14647
- streamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration)
14648
- }),
15056
+ useSegmentedCapture: segmentsCapture(hasHdrContent || compiled.hasShaderTransitions),
14649
15057
  useLayeredComposite,
14650
15058
  usePageSideCompositing: usePageSideCompositingForTransitions,
14651
15059
  hasHdrContent,
@@ -14685,10 +15093,14 @@ async function executeRenderPipeline(input) {
14685
15093
  deWorkerInversion,
14686
15094
  deParallelRouter
14687
15095
  });
15096
+ if (videoFrameWindow && capturePlan.kind !== "sdr_segmented")
15097
+ await extractDeferredVideoFrames();
15098
+ updateCaptureObservability({ videoFrameWindow: videoFrameWindow ? "on" : "off" });
14688
15099
  observability.checkpoint("capture_strategy", "resolved", {
14689
15100
  plan: capturePlan.kind,
14690
15101
  workerCount: capturePlan.workerCount,
14691
15102
  forceScreenshot: capturePlan.forceScreenshot,
15103
+ videoFrameWindow: videoFrameWindow ? "on" : "off",
14692
15104
  captureBeyondViewport: resolvedCaptureBeyondViewport ?? null,
14693
15105
  useStreamingEncode: capturePlan.kind === "sdr_streaming",
14694
15106
  useLayeredComposite: capturePlan.kind === "hdr_layered",
@@ -14826,7 +15238,7 @@ async function executeRenderPipeline(input) {
14826
15238
  pageMotionBlur: job.config.pageMotionBlur,
14827
15239
  captureQuality: captureOptions.quality
14828
15240
  });
14829
- const segmentDir = segmentDirFor(join28(projectDir, "renders"), segmentPlanHash);
15241
+ const segmentDir = segmentDirFor(join31(projectDir, "renders"), segmentPlanHash);
14830
15242
  let completedSegments = /* @__PURE__ */ new Set();
14831
15243
  if (job.config.resumeSegments === true) {
14832
15244
  const existing = readSegmentManifest(segmentDir);
@@ -14841,10 +15253,10 @@ async function executeRenderPipeline(input) {
14841
15253
  });
14842
15254
  }
14843
15255
  if (completedSegments.size === 0) {
14844
- rmSync10(segmentDir, { recursive: true, force: true });
15256
+ rmSync12(segmentDir, { recursive: true, force: true });
14845
15257
  }
14846
15258
  } else {
14847
- rmSync10(segmentDir, { recursive: true, force: true });
15259
+ rmSync12(segmentDir, { recursive: true, force: true });
14848
15260
  }
14849
15261
  const emptySegmentManifest = () => ({
14850
15262
  version: 1,
@@ -14894,6 +15306,7 @@ async function executeRenderPipeline(input) {
14894
15306
  },
14895
15307
  buildCaptureOptions: buildWorkerCaptureOptions,
14896
15308
  createRenderVideoFrameInjector,
15309
+ segmentVideoFrames: videoFrameWindow ? buildSegmentVideoFrames() : void 0,
14897
15310
  abortSignal: executionSignal,
14898
15311
  assertNotAborted,
14899
15312
  onProgress,
@@ -14945,7 +15358,7 @@ async function executeRenderPipeline(input) {
14945
15358
  probeSession = null;
14946
15359
  await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
14947
15360
  }
14948
- rmSync10(segmentDir, { recursive: true, force: true });
15361
+ rmSync12(segmentDir, { recursive: true, force: true });
14949
15362
  completedSegments = /* @__PURE__ */ new Set();
14950
15363
  segmentManifest = emptySegmentManifest();
14951
15364
  dedupPerfs.length = 0;
@@ -14984,11 +15397,12 @@ async function executeRenderPipeline(input) {
14984
15397
  );
14985
15398
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
14986
15399
  if (job.config.keepSegments !== true) {
14987
- rmSync10(segmentDir, { recursive: true, force: true });
15400
+ rmSync12(segmentDir, { recursive: true, force: true });
14988
15401
  }
14989
15402
  } else {
14990
15403
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
14991
15404
  syncCapturePlan();
15405
+ if (videoFrameWindow) await extractDeferredVideoFrames();
14992
15406
  probeSession = null;
14993
15407
  observability.checkpoint(
14994
15408
  "capture_segmented",
@@ -15259,8 +15673,8 @@ async function executeRenderPipeline(input) {
15259
15673
  "capture_disk",
15260
15674
  "drawElement capture failed; retrying with a fresh screenshot session"
15261
15675
  );
15262
- rmSync10(framesDir, { recursive: true, force: true });
15263
- mkdirSync10(framesDir, { recursive: true });
15676
+ rmSync12(framesDir, { recursive: true, force: true });
15677
+ mkdirSync12(framesDir, { recursive: true });
15264
15678
  resetCaptureAttemptProgress(job);
15265
15679
  dedupPerfs.length = 0;
15266
15680
  cfg.useDrawElement = false;
@@ -15447,7 +15861,7 @@ async function executeRenderPipeline(input) {
15447
15861
  }
15448
15862
  if (job.config.debug) {
15449
15863
  if (!isPngSequence && !isHls && existsSync20(stagedOutputPath)) {
15450
- const debugOutput = join28(workDir, `output${videoExt}`);
15864
+ const debugOutput = join31(workDir, `output${videoExt}`);
15451
15865
  copyFileSync5(stagedOutputPath, debugOutput);
15452
15866
  }
15453
15867
  }
@@ -15560,13 +15974,13 @@ async function executeRenderPipeline(input) {
15560
15974
  import {
15561
15975
  existsSync as existsSync21,
15562
15976
  readFileSync as readFileSync9,
15563
- statSync as statSync9,
15977
+ statSync as statSync10,
15564
15978
  openSync as openSync5,
15565
15979
  fstatSync as fstatSync3,
15566
15980
  closeSync as closeSync5,
15567
15981
  constants as constants3
15568
15982
  } from "node:fs";
15569
- import { resolve as resolve9, join as join29, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
15983
+ import { resolve as resolve9, join as join32, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
15570
15984
  import { lintHyperframeHtml } from "@hyperframes/lint";
15571
15985
  function isStringRecord(value) {
15572
15986
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -15600,7 +16014,7 @@ function readEntryHtml(filePath) {
15600
16014
  } catch (error) {
15601
16015
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
15602
16016
  return null;
15603
- if (!statSync9(filePath, { throwIfNoEntry: false })?.isFile()) return null;
16017
+ if (!statSync10(filePath, { throwIfNoEntry: false })?.isFile()) return null;
15604
16018
  throw error;
15605
16019
  }
15606
16020
  try {
@@ -15612,7 +16026,7 @@ function readEntryHtml(filePath) {
15612
16026
  }
15613
16027
  function readProjectEntryFile(projectDir, preferredEntryFile) {
15614
16028
  const absProjectDir = resolve9(projectDir);
15615
- if (!existsSync21(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
16029
+ if (!existsSync21(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
15616
16030
  return { error: `Project directory not found: ${absProjectDir}` };
15617
16031
  }
15618
16032
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -15634,7 +16048,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
15634
16048
  }
15635
16049
  }
15636
16050
  return {
15637
- error: `No HTML entry file found in project directory: ${join29(absProjectDir, preferredEntryFile || "index.html")}`
16051
+ error: `No HTML entry file found in project directory: ${join32(absProjectDir, preferredEntryFile || "index.html")}`
15638
16052
  };
15639
16053
  }
15640
16054
  function prepareHyperframeLintBody(body) {
@@ -15680,7 +16094,7 @@ async function runHyperframeLint(prepared) {
15680
16094
  // src/services/healthWorker.ts
15681
16095
  import { Worker as Worker2 } from "node:worker_threads";
15682
16096
  import { fileURLToPath as fileURLToPath5 } from "node:url";
15683
- import { dirname as dirname13, join as join30 } from "node:path";
16097
+ import { dirname as dirname14, join as join33 } from "node:path";
15684
16098
  import { existsSync as existsSync22 } from "node:fs";
15685
16099
  var DEFAULT_HEALTH_PORT = 9848;
15686
16100
  async function startHealthWorker(options = {}) {
@@ -15755,8 +16169,8 @@ function defaultLogger2() {
15755
16169
  };
15756
16170
  }
15757
16171
  function resolveWorkerEntry2() {
15758
- const here = dirname13(fileURLToPath5(import.meta.url));
15759
- const candidates = [join30(here, "healthWorkerThread.js"), join30(here, "healthWorkerThread.ts")];
16172
+ const here = dirname14(fileURLToPath5(import.meta.url));
16173
+ const candidates = [join33(here, "healthWorkerThread.js"), join33(here, "healthWorkerThread.ts")];
15760
16174
  for (const candidate of candidates) {
15761
16175
  if (existsSync22(candidate)) return candidate;
15762
16176
  }
@@ -15798,7 +16212,7 @@ import {
15798
16212
  createReferenceBackgroundRecorder as createReferenceBackgroundRecorder2
15799
16213
  } from "@hyperframes/engine";
15800
16214
  import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
15801
- import { extname as extname4, isAbsolute as isAbsolute4, join as join31, relative as relative4, sep as sep3 } from "node:path";
16215
+ import { extname as extname4, isAbsolute as isAbsolute4, join as join34, relative as relative4, sep as sep3 } from "node:path";
15802
16216
  import {
15803
16217
  fpsToNumber as fpsToNumber9,
15804
16218
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -15817,16 +16231,14 @@ import {
15817
16231
  import {
15818
16232
  closeCaptureSession as closeCaptureSession9,
15819
16233
  createCaptureSession as createCaptureSession9,
15820
- createFrameLookupTable as createFrameLookupTable3,
16234
+ createFrameLookupTable as createFrameLookupTable4,
15821
16235
  initializeSession as initializeSession9
15822
16236
  } from "@hyperframes/engine";
15823
16237
 
15824
16238
  // src/services/distributed/chunkFrames.ts
15825
16239
  import {
15826
16240
  compositionRequiresWebGpu as compositionRequiresWebGpu3,
15827
- createFrameLookupTable as createFrameLookupTable2,
15828
- createVideoFrameInjector as createVideoFrameInjector2,
15829
- extractVideoFramesRange
16241
+ createVideoFrameInjector as createVideoFrameInjector3
15830
16242
  } from "@hyperframes/engine";
15831
16243
 
15832
16244
  // src/services/distributed/shared.ts
@@ -15917,9 +16329,9 @@ var PLAN_ROOT_CATEGORY = {
15917
16329
  var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
15918
16330
  var FREEZE_OWNED_PLAN_FILES = [
15919
16331
  "plan.json",
15920
- join31("meta", "composition.json"),
15921
- join31("meta", "encoder.json"),
15922
- join31("meta", "chunks.json")
16332
+ join34("meta", "composition.json"),
16333
+ join34("meta", "encoder.json"),
16334
+ join34("meta", "chunks.json")
15923
16335
  ];
15924
16336
 
15925
16337
  // src/services/distributed/renderConfigValidation.ts
@@ -16307,8 +16719,8 @@ function buildRenderJobConfig(input, outputPath, log) {
16307
16719
  function resolvePreparedRenderOutput(prepared, rendersDir, log) {
16308
16720
  const { input, cleanupProjectDir } = prepared;
16309
16721
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
16310
- const outputDir = dirname14(absoluteOutputPath);
16311
- if (!existsSync23(outputDir)) mkdirSync11(outputDir, { recursive: true });
16722
+ const outputDir = dirname15(absoluteOutputPath);
16723
+ if (!existsSync23(outputDir)) mkdirSync13(outputDir, { recursive: true });
16312
16724
  return { input, cleanupProjectDir, absoluteOutputPath };
16313
16725
  }
16314
16726
  function validateRenderOverrides(body) {
@@ -16349,7 +16761,7 @@ function prepareProjectDirectory(projectDir, options) {
16349
16761
  const candidate = nonEmptyString(projectDir);
16350
16762
  if (!candidate) return null;
16351
16763
  const absProjectDir = resolve10(candidate);
16352
- if (!existsSync23(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
16764
+ if (!existsSync23(absProjectDir) || !statSync11(absProjectDir).isDirectory()) {
16353
16765
  return { error: `Project directory not found: ${absProjectDir}` };
16354
16766
  }
16355
16767
  const entry = options.entryFile || "index.html";
@@ -16374,8 +16786,8 @@ async function resolveInlineRenderHtml(body) {
16374
16786
  }
16375
16787
  function materializeInlineProject(html, options) {
16376
16788
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
16377
- const tempProjectDir = mkdtempSync4(join32(tempRoot, "producer-project-"));
16378
- writeFileSync8(join32(tempProjectDir, "index.html"), html, "utf-8");
16789
+ const tempProjectDir = mkdtempSync4(join35(tempRoot, "producer-project-"));
16790
+ writeFileSync8(join35(tempProjectDir, "index.html"), html, "utf-8");
16379
16791
  return {
16380
16792
  prepared: {
16381
16793
  input: { projectDir: tempProjectDir, ...options },
@@ -16436,7 +16848,7 @@ function createArtifactStore(ttlMs) {
16436
16848
  function cleanupTempDir(dir, log) {
16437
16849
  if (!dir) return;
16438
16850
  try {
16439
- rmSync11(dir, { recursive: true, force: true });
16851
+ rmSync13(dir, { recursive: true, force: true });
16440
16852
  } catch (error) {
16441
16853
  log.warn("Failed to cleanup temp project dir", {
16442
16854
  cleanupProjectDir: dir,
@@ -16445,7 +16857,7 @@ function cleanupTempDir(dir, log) {
16445
16857
  }
16446
16858
  }
16447
16859
  function outputFileSize(path) {
16448
- return existsSync23(path) ? statSync10(path).size : 0;
16860
+ return existsSync23(path) ? statSync11(path).size : 0;
16449
16861
  }
16450
16862
  function createBlockingProgressReporter(log, requestId) {
16451
16863
  let lastLoggedPct = -10;
@@ -16753,7 +17165,7 @@ function createRenderHandlers(options = {}) {
16753
17165
  store.delete(token);
16754
17166
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
16755
17167
  }
16756
- const stats = statSync10(artifact.path);
17168
+ const stats = statSync11(artifact.path);
16757
17169
  return new Response(createReadStream3(artifact.path), {
16758
17170
  headers: {
16759
17171
  "content-type": "video/mp4",