@hyperframes/producer 0.8.224 → 0.8.226

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/server.ts
4
4
  import {
5
- existsSync as existsSync19,
5
+ existsSync as existsSync20,
6
6
  mkdirSync as mkdirSync11,
7
7
  statSync as statSync10,
8
8
  mkdtempSync as mkdtempSync4,
@@ -22,9 +22,8 @@ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engi
22
22
  // src/services/renderOrchestrator.ts
23
23
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
24
24
  import {
25
- existsSync as existsSync16,
25
+ existsSync as existsSync17,
26
26
  mkdirSync as mkdirSync10,
27
- readFileSync as readFileSync9,
28
27
  readdirSync as readdirSync5,
29
28
  rmSync as rmSync10,
30
29
  statSync as statSync8,
@@ -45,6 +44,7 @@ import {
45
44
  import { HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
46
45
  import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
47
46
  import { HTML_BODY_CSS_HEIGHT_FIRST_RE, HTML_BODY_CSS_WIDTH_FIRST_RE } from "@hyperframes/parsers";
47
+ import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
48
48
  import {
49
49
  extractMediaMetadata as extractMediaMetadata4,
50
50
  resolveConfig as resolveConfig2,
@@ -2151,6 +2151,14 @@ function pushWorkerDedupPerfs(results, sink) {
2151
2151
  if (r.perf) sink.push(r.perf);
2152
2152
  }
2153
2153
  }
2154
+ function capturedBeyondViewport(perfs, fallback) {
2155
+ const used = perfs.flatMap((perf) => perf.captureBeyondViewport ?? []);
2156
+ return used.length > 0 ? used.includes(true) : fallback;
2157
+ }
2158
+ function withCapturedBeyondViewport(capture, perfs) {
2159
+ const used = capturedBeyondViewport(perfs, capture.captureBeyondViewport);
2160
+ return used === void 0 ? capture : { ...capture, captureBeyondViewport: used };
2161
+ }
2154
2162
  function pushWorkerPageErrors(results, sink) {
2155
2163
  for (const result of results) {
2156
2164
  for (const line of result.pageErrorLines ?? []) if (!sink.includes(line)) sink.push(line);
@@ -5783,7 +5791,7 @@ import { suggestMatchingPreset } from "@hyperframes/core";
5783
5791
  // src/services/htmlCompiler.ts
5784
5792
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5785
5793
  import { createHash as createHash6 } from "node:crypto";
5786
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
5794
+ import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
5787
5795
  import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
5788
5796
  import { parseHTML as parseHTML5 } from "linkedom";
5789
5797
  import {
@@ -5836,7 +5844,8 @@ import {
5836
5844
  } from "@hyperframes/parsers/sub-composition-validity";
5837
5845
  import {
5838
5846
  isUnresolvedAssetPlaceholder,
5839
- readProjectFile
5847
+ readProjectFile,
5848
+ readTextFile
5840
5849
  } from "@hyperframes/parsers/asset-resolution";
5841
5850
 
5842
5851
  // src/services/renderMediaCollector.ts
@@ -7925,11 +7934,7 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
7925
7934
  return document2.toString();
7926
7935
  }
7927
7936
  async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
7928
- const entryHtml = rebaseDirectEntryAssetPaths(
7929
- readFileSync7(htmlPath, "utf-8"),
7930
- projectDir,
7931
- htmlPath
7932
- );
7937
+ const entryHtml = rebaseDirectEntryAssetPaths(readTextFile(htmlPath), projectDir, htmlPath);
7933
7938
  const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
7934
7939
  assertSubCompositionsUsable(rawHtml, projectDir);
7935
7940
  const probes = /* @__PURE__ */ new Map();
@@ -8780,7 +8785,7 @@ async function runProbeStage(input) {
8780
8785
  );
8781
8786
  const hasVariableMedia = hasVariableBoundMedia(compiled.html, job.config.variables);
8782
8787
  const hasInsertedMedia = hasRuntimeMediaChanges(compiled.html);
8783
- const needsBrowser = probeRequiresBrowser({
8788
+ const needsBrowser = cfg.videoLayerPassthrough || probeRequiresBrowser({
8784
8789
  durationSeconds: composition.duration,
8785
8790
  unresolvedCompositionCount: compiled.unresolvedCompositions.length,
8786
8791
  hasAutoStart,
@@ -9380,17 +9385,23 @@ function resolveVideoExtractionPolicy(env = process.env) {
9380
9385
  const maxTransientRetries = failureMode !== "off" && env.HF_VIDEO_EXTRACTION_MAX_RETRIES?.trim() === "1" ? 1 : 0;
9381
9386
  return { failureMode, maxTransientRetries };
9382
9387
  }
9388
+ var MAX_NAMED_SOURCES = 5;
9383
9389
  var VideoExtractionStageError = class extends Error {
9384
9390
  constructor(code, retryable, failures, extractionFailure = buildExtractionFailureMetadata(
9385
9391
  failures.map((failure) => ({
9386
9392
  kind: failure.kind,
9387
9393
  affectedElementCount: failure.count
9388
9394
  }))
9389
- ), ffmpegReason) {
9395
+ ), ffmpegReason, sources = []) {
9390
9396
  const total = failures.reduce((sum, failure) => sum + failure.count, 0);
9391
9397
  const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
9392
9398
  const reason = ffmpegReason ? `: ffmpeg said: ${ffmpegReason}` : "";
9393
- super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]${reason}`);
9399
+ const shown = sources.slice(0, MAX_NAMED_SOURCES).join(", ");
9400
+ const more = sources.length > MAX_NAMED_SOURCES ? ` and ${sources.length - MAX_NAMED_SOURCES} more` : "";
9401
+ const files = sources.length > 0 ? ` (${shown}${more})` : "";
9402
+ super(
9403
+ `Video extraction failed for ${total} source(s) [${code}; ${breakdown}]${files}${reason}`
9404
+ );
9394
9405
  this.code = code;
9395
9406
  this.retryable = retryable;
9396
9407
  this.failures = failures;
@@ -9433,7 +9444,10 @@ function buildVideoExtractionStageError(result) {
9433
9444
  retryable,
9434
9445
  failures,
9435
9446
  extractionFailureMetadataFromResult(result),
9436
- result.errors.find((failure) => failure.ffmpegReason)?.ffmpegReason
9447
+ result.errors.find((failure) => failure.ffmpegReason)?.ffmpegReason,
9448
+ [
9449
+ ...new Set(result.errors.flatMap((failure) => failure.source ? [failure.source] : []))
9450
+ ].sort()
9437
9451
  );
9438
9452
  }
9439
9453
  function buildHdrProbeStageError(failures) {
@@ -9512,7 +9526,8 @@ async function runExtractVideosStage(input) {
9512
9526
  assertNotAborted,
9513
9527
  materializeSymlinks,
9514
9528
  deferRangeExtraction,
9515
- urlByAbsentLocalPath
9529
+ urlByAbsentLocalPath,
9530
+ skipFrameExtractionIds = []
9516
9531
  } = input;
9517
9532
  const stage2Start = Date.now();
9518
9533
  const extractionPolicy = resolveVideoExtractionPolicy();
@@ -9583,7 +9598,10 @@ async function runExtractVideosStage(input) {
9583
9598
  );
9584
9599
  imageColorSpaces.push(...probed);
9585
9600
  }
9586
- if (composition.videos.length > 0) {
9601
+ if (skipFrameExtractionIds.length > 0) {
9602
+ await appendVideoAudioFromProbe({ projectDir, compiledDir, composition, urlByAbsentLocalPath });
9603
+ videoReadinessSkipIds = [...skipFrameExtractionIds];
9604
+ } else if (composition.videos.length > 0) {
9587
9605
  const totalVideos = composition.videos.length;
9588
9606
  for (let i = 0; i < totalVideos; i++) {
9589
9607
  const v = composition.videos[i];
@@ -9679,6 +9697,116 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
9679
9697
  existingAudioSrcs.add(video.src);
9680
9698
  }
9681
9699
  }
9700
+ async function appendVideoAudioFromProbe(input) {
9701
+ const { projectDir, compiledDir, composition, urlByAbsentLocalPath } = input;
9702
+ const videos = composition.videos.filter(
9703
+ (video) => !isVideoPastTimelineEnd(video, composition.duration)
9704
+ );
9705
+ const probed = await Promise.all(
9706
+ videos.map(async (video) => {
9707
+ const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
9708
+ const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
9709
+ if (!isHttpUrl2(readFrom) && !existsSync11(readFrom)) return null;
9710
+ const metadata = await extractMediaMetadata3(readFrom);
9711
+ const window2 = await resolveFinalFrameExtractionWindow2(
9712
+ readFrom,
9713
+ video,
9714
+ metadata,
9715
+ resolveVideoExtractionWindow2(video, metadata, composition.duration)
9716
+ );
9717
+ if (window2.durationSeconds <= 0) return null;
9718
+ rebaseVideoToWindow(video, window2);
9719
+ return { videoId: video.id, metadata };
9720
+ })
9721
+ );
9722
+ appendAutoDetectedVideoAudio(
9723
+ composition,
9724
+ probed.filter((entry) => entry !== null)
9725
+ );
9726
+ }
9727
+
9728
+ // src/services/render/videoLayerPassthroughPlan.ts
9729
+ import { existsSync as existsSync12 } from "node:fs";
9730
+ import {
9731
+ buildVideoLayerInput,
9732
+ isHttpUrl as isHttpUrl3,
9733
+ qualifyVideoLayer,
9734
+ resolveProjectRelativeSrc as resolveProjectRelativeSrc4
9735
+ } from "@hyperframes/engine";
9736
+ var SAMPLES = 9;
9737
+ function inspectVideoLayout(id, w, h, time) {
9738
+ window.__hf?.seek?.(time);
9739
+ const video = document.getElementById(id);
9740
+ if (!video) return { reason: "video_missing" };
9741
+ const r = video.getBoundingClientRect();
9742
+ if (Math.abs(r.left) > 0.5 || Math.abs(r.top) > 0.5) return { reason: "not_full_canvas" };
9743
+ if (Math.abs(r.width - w) > 0.5 || Math.abs(r.height - h) > 0.5) {
9744
+ return { reason: "not_full_canvas" };
9745
+ }
9746
+ const painted = (s) => s.backgroundColor !== "" && s.backgroundColor !== "transparent" && s.backgroundColor !== "rgba(0, 0, 0, 0)" || s.backgroundImage !== "" && s.backgroundImage !== "none";
9747
+ for (let e = video; e && e !== document.documentElement; ) {
9748
+ const s = getComputedStyle(e);
9749
+ if (s.opacity !== "1") return { reason: "opacity" };
9750
+ if (s.transform !== "none" || s.filter !== "none") return { reason: "transform_or_filter" };
9751
+ if (s.clipPath !== "none" || s.maskImage !== "none") return { reason: "clip_or_mask" };
9752
+ if (s.mixBlendMode !== "normal") return { reason: "blend_mode" };
9753
+ if (s.visibility !== "visible" || s.display === "none") return { reason: "not_shown" };
9754
+ const cleared = e === document.body || e.hasAttribute("data-composition-id");
9755
+ if (e !== video && painted(s) && !cleared) return { reason: "ancestor_background" };
9756
+ e = e.parentElement;
9757
+ }
9758
+ for (const root of Array.from(document.querySelectorAll("[data-composition-id]"))) {
9759
+ if (root.parentElement?.closest("[data-composition-id]") && painted(getComputedStyle(root))) {
9760
+ return { reason: "subcomposition_background" };
9761
+ }
9762
+ }
9763
+ const fitStyle = getComputedStyle(video).objectFit;
9764
+ if (fitStyle !== "fill" && fitStyle !== "cover") return { reason: "object_fit" };
9765
+ const own = Number.parseInt(getComputedStyle(video).zIndex, 10) || 0;
9766
+ for (const other of Array.from(document.body.querySelectorAll("*"))) {
9767
+ if (other === video || other.contains(video) || video.contains(other)) continue;
9768
+ if (other.tagName === "SCRIPT" || other.tagName === "STYLE") continue;
9769
+ const z = Number.parseInt(getComputedStyle(other).zIndex, 10) || 0;
9770
+ const before = video.compareDocumentPosition(other) & Node.DOCUMENT_POSITION_PRECEDING;
9771
+ if (z < own || z === own && before) {
9772
+ const box = other.getBoundingClientRect();
9773
+ if (box.width > 0 && box.height > 0) return { reason: "paints_under" };
9774
+ }
9775
+ }
9776
+ return { fit: fitStyle === "cover" ? "cover" : "fill" };
9777
+ }
9778
+ async function probeVideoLayout(page, videoId, canvas, duration) {
9779
+ let fit = "fill";
9780
+ for (let i = 0; i < SAMPLES; i++) {
9781
+ const t = duration * i / SAMPLES;
9782
+ const seen = await page.evaluate(inspectVideoLayout, videoId, canvas.width, canvas.height, t);
9783
+ if ("reason" in seen) return seen;
9784
+ if (seen.fit === "cover") fit = "cover";
9785
+ }
9786
+ await page.evaluate(
9787
+ () => window.__hf?.seek?.(0)
9788
+ );
9789
+ return { fit };
9790
+ }
9791
+ async function planVideoLayerPassthrough(input) {
9792
+ const q = qualifyVideoLayer(input.videos, input.duration);
9793
+ if (!q.qualifies) return { reason: q.reason };
9794
+ const { video } = q;
9795
+ if (isHttpUrl3(video.src)) return { reason: "remote_source" };
9796
+ const path = resolveProjectRelativeSrc4(video.src, input.projectDir, input.compiledDir);
9797
+ if (!existsSync12(path)) return { reason: "source_missing" };
9798
+ const layout = await probeVideoLayout(input.page, video.id, input.canvas, input.duration);
9799
+ if ("reason" in layout) return layout;
9800
+ const layer = await buildVideoLayerInput(
9801
+ path,
9802
+ video.mediaStart,
9803
+ input.duration,
9804
+ input.fps,
9805
+ input.canvas,
9806
+ layout.fit
9807
+ );
9808
+ return layer ? { videoId: video.id, layer } : { reason: "source_unsupported" };
9809
+ }
9682
9810
 
9683
9811
  // src/services/render/stages/audioStage.ts
9684
9812
  import { join as join20 } from "node:path";
@@ -10290,7 +10418,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10290
10418
  }
10291
10419
 
10292
10420
  // src/services/render/stages/captureStage.ts
10293
- import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
10421
+ import { existsSync as existsSync13, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
10294
10422
  import { join as join21 } from "node:path";
10295
10423
  import {
10296
10424
  classifyCaptureFailure as classifyCaptureFailure2,
@@ -10361,7 +10489,7 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
10361
10489
  }
10362
10490
  function measureCaptureFrameBytes(workDir, framesDir) {
10363
10491
  let total = sampleDirectoryBytes(framesDir);
10364
- if (!existsSync12(workDir)) return total;
10492
+ if (!existsSync13(workDir)) return total;
10365
10493
  for (const attempt of readdirSync3(workDir)) {
10366
10494
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
10367
10495
  total += sampleDirectoryBytes(join21(workDir, attempt));
@@ -10664,7 +10792,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
10664
10792
  }
10665
10793
 
10666
10794
  // src/services/render/stages/captureHdrStage.ts
10667
- import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "node:fs";
10795
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8 } from "node:fs";
10668
10796
  import { join as join25 } from "node:path";
10669
10797
  import {
10670
10798
  closeCaptureSession as closeCaptureSession7,
@@ -10910,7 +11038,7 @@ import { Worker } from "node:worker_threads";
10910
11038
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
10911
11039
  import { dirname as dirname9, join as join23 } from "node:path";
10912
11040
  import { createRequire } from "node:module";
10913
- import { existsSync as existsSync13 } from "node:fs";
11041
+ import { existsSync as existsSync14 } from "node:fs";
10914
11042
  import { cpus as cpus2 } from "node:os";
10915
11043
  function resolveWorkerEntry(explicit) {
10916
11044
  if (explicit && explicit.length > 0) {
@@ -10923,7 +11051,7 @@ function resolveWorkerEntry(explicit) {
10923
11051
  }
10924
11052
  const moduleDir = dirname9(fileURLToPath3(import.meta.url));
10925
11053
  const jsPath = join23(moduleDir, "shaderTransitionWorker.js");
10926
- if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
11054
+ if (existsSync14(jsPath)) return { path: jsPath, isTs: false };
10927
11055
  const tsPath = join23(moduleDir, "shaderTransitionWorker.ts");
10928
11056
  return { path: tsPath, isTs: true };
10929
11057
  }
@@ -11484,7 +11612,7 @@ async function runCaptureHdrStage(input) {
11484
11612
  }
11485
11613
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
11486
11614
  const debugDumpDir = debugDumpEnabled ? join25(framesDir, "debug-composite") : null;
11487
- if (debugDumpDir && !existsSync14(debugDumpDir)) {
11615
+ if (debugDumpDir && !existsSync15(debugDumpDir)) {
11488
11616
  mkdirSync8(debugDumpDir, { recursive: true });
11489
11617
  }
11490
11618
  const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
@@ -11641,10 +11769,10 @@ async function runCaptureHdrStage(input) {
11641
11769
  // src/services/render/stages/encodeStage.ts
11642
11770
  import {
11643
11771
  copyFileSync as copyFileSync4,
11644
- existsSync as existsSync15,
11772
+ existsSync as existsSync16,
11645
11773
  mkdirSync as mkdirSync9,
11646
11774
  readdirSync as readdirSync4,
11647
- readFileSync as readFileSync8,
11775
+ readFileSync as readFileSync7,
11648
11776
  rmSync as rmSync9,
11649
11777
  statSync as statSync7,
11650
11778
  writeFileSync as writeFileSync5
@@ -11738,7 +11866,7 @@ async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
11738
11866
  const error = formatFfmpegError(result.exitCode, result.stderr);
11739
11867
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
11740
11868
  }
11741
- const gif = readFileSync8(outputPath);
11869
+ const gif = readFileSync7(outputPath);
11742
11870
  if (!clearGifFramesBeforeNext(gif)) {
11743
11871
  const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
11744
11872
  return gifEncodeFailure(startTime, outputPath, error);
@@ -11771,11 +11899,11 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
11771
11899
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
11772
11900
  }
11773
11901
  }
11774
- if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync8(outputPath)) !== false) {
11902
+ if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync7(outputPath)) !== false) {
11775
11903
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
11776
11904
  if (failure) return failure;
11777
11905
  }
11778
- const fileSize = existsSync15(outputPath) ? statSync7(outputPath).size : 0;
11906
+ const fileSize = existsSync16(outputPath) ? statSync7(outputPath).size : 0;
11779
11907
  return {
11780
11908
  success: true,
11781
11909
  outputPath,
@@ -11936,7 +12064,7 @@ async function runEncodeStage(input) {
11936
12064
  const stage5Start = Date.now();
11937
12065
  if (isPngSequence) {
11938
12066
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
11939
- if (!existsSync15(outputPath)) mkdirSync9(outputPath, { recursive: true });
12067
+ if (!existsSync16(outputPath)) mkdirSync9(outputPath, { recursive: true });
11940
12068
  const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
11941
12069
  if (captured.length === 0) {
11942
12070
  throw new Error(
@@ -11947,7 +12075,7 @@ async function runEncodeStage(input) {
11947
12075
  const dst = join26(outputPath, formatExportFrameName(i, "png"));
11948
12076
  copyFileSync4(join26(framesDir, name), dst);
11949
12077
  });
11950
- if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
12078
+ if (hasAudio && audioOutputPath && existsSync16(audioOutputPath)) {
11951
12079
  copyFileSync4(audioOutputPath, join26(outputPath, MIXED_AUDIO_FILENAME2));
11952
12080
  log.info(
11953
12081
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
@@ -12328,7 +12456,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
12328
12456
  let rangeStart = null;
12329
12457
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12330
12458
  const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12331
- const missing = !existsSync16(framePath) || statSync8(framePath).size <= 8;
12459
+ const missing = !existsSync17(framePath) || statSync8(framePath).size <= 8;
12332
12460
  if (missing && rangeStart === null) {
12333
12461
  rangeStart = frameIndex;
12334
12462
  } else if (!missing && rangeStart !== null) {
@@ -12453,7 +12581,7 @@ function countCapturedFrames(totalFrames, framesDir, frameExt) {
12453
12581
  let captured = 0;
12454
12582
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12455
12583
  const framePath = join27(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12456
- if (existsSync16(framePath)) captured++;
12584
+ if (existsSync17(framePath)) captured++;
12457
12585
  }
12458
12586
  return captured;
12459
12587
  }
@@ -13116,7 +13244,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
13116
13244
  validateDeliveryEncoderConfig(job.config);
13117
13245
  const debugDir = resolveRenderDebugDir();
13118
13246
  const outputDir = dirname11(outputPath);
13119
- if (!existsSync16(outputDir)) mkdirSync10(outputDir, { recursive: true });
13247
+ if (!existsSync17(outputDir)) mkdirSync10(outputDir, { recursive: true });
13120
13248
  const workDir = job.config.debug ? join27(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
13121
13249
  const pipelineStart = Date.now();
13122
13250
  const baseLog = job.config.logger ?? defaultLogger;
@@ -13257,7 +13385,7 @@ async function executeRenderPipeline(input) {
13257
13385
  job.startedAt = /* @__PURE__ */ new Date();
13258
13386
  assertNotAborted();
13259
13387
  assertConfiguredFfmpegBinariesExist();
13260
- if (!existsSync16(workDir)) mkdirSync10(workDir, { recursive: true });
13388
+ if (!existsSync17(workDir)) mkdirSync10(workDir, { recursive: true });
13261
13389
  if (job.config.debug) {
13262
13390
  log.info("[Render] Debug artifacts enabled", { workDir, logPath });
13263
13391
  }
@@ -13287,21 +13415,21 @@ async function executeRenderPipeline(input) {
13287
13415
  });
13288
13416
  const entryFile = job.config.entryFile || "index.html";
13289
13417
  let htmlPath = join27(projectDir, entryFile);
13290
- if (!existsSync16(htmlPath)) {
13418
+ if (!existsSync17(htmlPath)) {
13291
13419
  throw new Error(`Entry file not found: ${htmlPath}`);
13292
13420
  }
13293
13421
  assertNotAborted();
13294
- const rawEntry = readFileSync9(htmlPath, "utf-8");
13422
+ const rawEntry = readTextFile2(htmlPath);
13295
13423
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
13296
13424
  const wrapperPath = join27(workDir, "standalone-entry.html");
13297
13425
  const projectIndexPath = join27(projectDir, "index.html");
13298
- if (!existsSync16(projectIndexPath)) {
13426
+ if (!existsSync17(projectIndexPath)) {
13299
13427
  throw new Error(
13300
13428
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
13301
13429
  );
13302
13430
  }
13303
13431
  const standaloneHtml = extractStandaloneEntryFromIndex(
13304
- readFileSync9(projectIndexPath, "utf-8"),
13432
+ readTextFile2(projectIndexPath),
13305
13433
  entryFile,
13306
13434
  rawEntry
13307
13435
  );
@@ -13464,6 +13592,42 @@ async function executeRenderPipeline(input) {
13464
13592
  compositionHash,
13465
13593
  beginFrameStalled: probeResult.beginFrameStalled
13466
13594
  });
13595
+ let videoLayerPlan;
13596
+ if (cfg.videoLayerPassthrough) {
13597
+ const gates = {
13598
+ mp4: outputFormat === "mp4",
13599
+ software_encode: !job.config.useGpu,
13600
+ opaque_output: !needsAlpha,
13601
+ sdr: rendersSdrOnly(job.config, composition) || (job.config.hdrMode === void 0 || job.config.hdrMode === "auto") && composition.images.length === 0,
13602
+ streaming: shouldUseStreamingEncode(cfg, outputFormat, 1, probeResult.duration),
13603
+ probe_page: probeSession?.isInitialized === true
13604
+ };
13605
+ const failed = Object.entries(gates).find(([, ok]) => !ok)?.[0];
13606
+ if (failed || !probeSession) {
13607
+ log.info(`[Render] Video layer passthrough off: ${failed ?? "probe_page"}`);
13608
+ } else {
13609
+ videoLayerPlan = await planVideoLayerPassthrough({
13610
+ videos: composition.videos,
13611
+ duration: probeResult.duration,
13612
+ fps: job.config.fps,
13613
+ canvas: { width, height },
13614
+ page: probeSession.page,
13615
+ projectDir,
13616
+ compiledDir: join27(workDir, "compiled")
13617
+ });
13618
+ if (videoLayerPlan.videoId === void 0) {
13619
+ log.info(`[Render] Video layer passthrough off: ${videoLayerPlan.reason}`);
13620
+ } else {
13621
+ cfg.useDrawElement = false;
13622
+ cfg.forceScreenshot = true;
13623
+ captureForceScreenshot = true;
13624
+ const opened = probeSession;
13625
+ probeSession = null;
13626
+ await closeCaptureSession8(opened);
13627
+ }
13628
+ }
13629
+ }
13630
+ const videoLayer = videoLayerPlan?.videoId === void 0 ? void 0 : videoLayerPlan;
13467
13631
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
13468
13632
  code: "extract_video"
13469
13633
  });
@@ -13479,6 +13643,7 @@ async function executeRenderPipeline(input) {
13479
13643
  cfg,
13480
13644
  log,
13481
13645
  composition,
13646
+ skipFrameExtractionIds: videoLayer ? [videoLayer.videoId] : [],
13482
13647
  abortSignal: executionSignal,
13483
13648
  assertNotAborted,
13484
13649
  // Local staging can use links; distributed plan() alone requires real copies.
@@ -13597,7 +13762,7 @@ async function executeRenderPipeline(input) {
13597
13762
  throw new Error("File server failed to initialize before frame capture");
13598
13763
  }
13599
13764
  const framesDir = join27(workDir, "captured-frames");
13600
- if (!existsSync16(framesDir)) mkdirSync10(framesDir, { recursive: true });
13765
+ if (!existsSync17(framesDir)) mkdirSync10(framesDir, { recursive: true });
13601
13766
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
13602
13767
  code: "start_browsers"
13603
13768
  });
@@ -13623,8 +13788,9 @@ async function executeRenderPipeline(input) {
13623
13788
  width,
13624
13789
  height,
13625
13790
  fps: job.config.fps,
13626
- format: captureImageFormat,
13627
- transparent: needsAlpha,
13791
+ format: videoLayer ? "png" : captureImageFormat,
13792
+ transparent: needsAlpha || videoLayer !== void 0,
13793
+ ...videoLayer ? { hiddenElementIds: [videoLayer.videoId] } : {},
13628
13794
  quality: captureImageFormat === "png" ? void 0 : resolveCaptureJpegQuality(job.config.quality),
13629
13795
  variables: job.config.variables,
13630
13796
  deviceScaleFactor,
@@ -13921,6 +14087,7 @@ async function executeRenderPipeline(input) {
13921
14087
  }
13922
14088
  }
13923
14089
  );
14090
+ if (videoLayer) workerCount = 1;
13924
14091
  const preRoutingWorkerCount = workerCount;
13925
14092
  if (deParallelRouterEligible && workerCount > 1) {
13926
14093
  deParallelRouter = "routed";
@@ -14586,6 +14753,7 @@ async function executeRenderPipeline(input) {
14586
14753
  vp9CpuUsed: cfg.vp9CpuUsed,
14587
14754
  useGpu: job.config.useGpu,
14588
14755
  imageFormat: captureOptions.format || "jpeg",
14756
+ ...videoLayer ? { videoLayer: videoLayer.layer } : {},
14589
14757
  hdr: preset.hdr,
14590
14758
  // HLS only: force an IDR every `gopSize` frames so the
14591
14759
  // assemble stage's `-c copy` segmentation cuts exactly on
@@ -14916,12 +15084,12 @@ async function executeRenderPipeline(input) {
14916
15084
  })
14917
15085
  );
14918
15086
  const totalElapsed = Date.now() - pipelineStart;
14919
- const tmpPeakBytes = existsSync16(workDir) ? sampleDirectoryBytes(workDir) : 0;
15087
+ const tmpPeakBytes = existsSync17(workDir) ? sampleDirectoryBytes(workDir) : 0;
14920
15088
  recordTransientRetryObservability();
14921
15089
  observability.checkpoint("pipeline", "artifact validated", { totalElapsedMs: totalElapsed });
14922
15090
  const observabilitySummary = observability.summary({
14923
15091
  lastBrowserConsole,
14924
- capture: captureObservability,
15092
+ capture: withCapturedBeyondViewport(captureObservability, dedupPerfs),
14925
15093
  initFallback: mergeWorkerInitObservability(dedupPerfs),
14926
15094
  extraction: extractionObservability,
14927
15095
  compositionHash
@@ -14997,7 +15165,7 @@ async function executeRenderPipeline(input) {
14997
15165
  }
14998
15166
  }
14999
15167
  if (job.config.debug) {
15000
- if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
15168
+ if (!isPngSequence && !isHls && existsSync17(stagedOutputPath)) {
15001
15169
  const debugOutput = join27(workDir, `output${videoExt}`);
15002
15170
  copyFileSync5(stagedOutputPath, debugOutput);
15003
15171
  }
@@ -15057,7 +15225,7 @@ async function executeRenderPipeline(input) {
15057
15225
  const failedStage = job.currentStage || "pipeline";
15058
15226
  const observabilitySummary = observability.summary({
15059
15227
  lastBrowserConsole,
15060
- capture: captureObservability,
15228
+ capture: withCapturedBeyondViewport(captureObservability, dedupPerfs),
15061
15229
  initFallback: mergeWorkerInitObservability(dedupPerfs),
15062
15230
  extraction: extractionObservability,
15063
15231
  compositionHash
@@ -15104,8 +15272,8 @@ async function executeRenderPipeline(input) {
15104
15272
 
15105
15273
  // src/services/hyperframeLint.ts
15106
15274
  import {
15107
- existsSync as existsSync17,
15108
- readFileSync as readFileSync10,
15275
+ existsSync as existsSync18,
15276
+ readFileSync as readFileSync8,
15109
15277
  statSync as statSync9,
15110
15278
  openSync as openSync5,
15111
15279
  fstatSync as fstatSync3,
@@ -15151,14 +15319,14 @@ function readEntryHtml(filePath) {
15151
15319
  }
15152
15320
  try {
15153
15321
  if (!fstatSync3(fd).isFile()) return null;
15154
- return readFileSync10(fd, "utf-8");
15322
+ return readFileSync8(fd, "utf-8");
15155
15323
  } finally {
15156
15324
  closeSync5(fd);
15157
15325
  }
15158
15326
  }
15159
15327
  function readProjectEntryFile(projectDir, preferredEntryFile) {
15160
15328
  const absProjectDir = resolve9(projectDir);
15161
- if (!existsSync17(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
15329
+ if (!existsSync18(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
15162
15330
  return { error: `Project directory not found: ${absProjectDir}` };
15163
15331
  }
15164
15332
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -15227,7 +15395,7 @@ async function runHyperframeLint(prepared) {
15227
15395
  import { Worker as Worker2 } from "node:worker_threads";
15228
15396
  import { fileURLToPath as fileURLToPath5 } from "node:url";
15229
15397
  import { dirname as dirname12, join as join29 } from "node:path";
15230
- import { existsSync as existsSync18 } from "node:fs";
15398
+ import { existsSync as existsSync19 } from "node:fs";
15231
15399
  var DEFAULT_HEALTH_PORT = 9848;
15232
15400
  async function startHealthWorker(options = {}) {
15233
15401
  const log = options.logger ?? defaultLogger2();
@@ -15304,7 +15472,7 @@ function resolveWorkerEntry2() {
15304
15472
  const here = dirname12(fileURLToPath5(import.meta.url));
15305
15473
  const candidates = [join29(here, "healthWorkerThread.js"), join29(here, "healthWorkerThread.ts")];
15306
15474
  for (const candidate of candidates) {
15307
- if (existsSync18(candidate)) return candidate;
15475
+ if (existsSync19(candidate)) return candidate;
15308
15476
  }
15309
15477
  return void 0;
15310
15478
  }
@@ -15831,7 +15999,7 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
15831
15999
  const { input, cleanupProjectDir } = prepared;
15832
16000
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
15833
16001
  const outputDir = dirname13(absoluteOutputPath);
15834
- if (!existsSync19(outputDir)) mkdirSync11(outputDir, { recursive: true });
16002
+ if (!existsSync20(outputDir)) mkdirSync11(outputDir, { recursive: true });
15835
16003
  return { input, cleanupProjectDir, absoluteOutputPath };
15836
16004
  }
15837
16005
  function validateRenderOverrides(body) {
@@ -15872,11 +16040,11 @@ function prepareProjectDirectory(projectDir, options) {
15872
16040
  const candidate = nonEmptyString(projectDir);
15873
16041
  if (!candidate) return null;
15874
16042
  const absProjectDir = resolve10(candidate);
15875
- if (!existsSync19(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
16043
+ if (!existsSync20(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
15876
16044
  return { error: `Project directory not found: ${absProjectDir}` };
15877
16045
  }
15878
16046
  const entry = options.entryFile || "index.html";
15879
- if (!existsSync19(resolve10(absProjectDir, entry))) {
16047
+ if (!existsSync20(resolve10(absProjectDir, entry))) {
15880
16048
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
15881
16049
  }
15882
16050
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -15968,7 +16136,7 @@ function cleanupTempDir(dir, log) {
15968
16136
  }
15969
16137
  }
15970
16138
  function outputFileSize(path) {
15971
- return existsSync19(path) ? statSync10(path).size : 0;
16139
+ return existsSync20(path) ? statSync10(path).size : 0;
15972
16140
  }
15973
16141
  function createBlockingProgressReporter(log, requestId) {
15974
16142
  let lastLoggedPct = -10;
@@ -16270,7 +16438,7 @@ function createRenderHandlers(options = {}) {
16270
16438
  if (!artifact) {
16271
16439
  return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
16272
16440
  }
16273
- if (!existsSync19(artifact.path)) {
16441
+ if (!existsSync20(artifact.path)) {
16274
16442
  store.delete(token);
16275
16443
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
16276
16444
  }
@@ -7,7 +7,7 @@ import type { CapturePerfSummary, SubTimelineWaitOutcome, WorkerSizing } from "@
7
7
  import type { CaptureCalibrationSample, CaptureCostEstimate } from "./captureCost.js";
8
8
  import type { CaptureAttemptSummary, HdrDiagnostics, RenderJob, RenderPerfSummary } from "../renderOrchestrator.js";
9
9
  import { type HdrPerfCollector } from "./hdrPerf.js";
10
- import type { RenderObservabilitySummary } from "./observability.js";
10
+ import type { RenderCaptureObservability, RenderObservabilitySummary } from "./observability.js";
11
11
  /**
12
12
  * Worst sub-composition timeline wait outcome across sessions: script_failure
13
13
  * > timeout > ready. Shared by the success-path perf summary and the error
@@ -24,6 +24,10 @@ export declare function worstSubTimelineWaitOutcome(perfs: CapturePerfSummary[])
24
24
  export declare function pushWorkerDedupPerfs(results: ReadonlyArray<{
25
25
  perf?: CapturePerfSummary;
26
26
  }>, sink: CapturePerfSummary[]): void;
27
+ /** Beyond-viewport capture as the workers' sessions used it: on if any kept it, `fallback` when none reported. */
28
+ export declare function capturedBeyondViewport(perfs: ReadonlyArray<CapturePerfSummary>, fallback: boolean | undefined): boolean | undefined;
29
+ /** The capture facts with beyond-viewport capture as the sessions used it, on every capture path. */
30
+ export declare function withCapturedBeyondViewport(capture: RenderCaptureObservability, perfs: ReadonlyArray<CapturePerfSummary>): RenderCaptureObservability;
27
31
  /** Adds each worker's page-error console lines to `sink`, once per distinct line. */
28
32
  export declare function pushWorkerPageErrors(results: ReadonlyArray<{
29
33
  pageErrorLines?: string[];
@@ -52,6 +52,7 @@ export interface ExtractVideosStageInput {
52
52
  */
53
53
  materializeSymlinks?: boolean;
54
54
  deferRangeExtraction?: boolean;
55
+ skipFrameExtractionIds?: readonly string[];
55
56
  urlByAbsentLocalPath?: ReadonlyMap<string, string>;
56
57
  onProgress?: ProgressCallback;
57
58
  }
@@ -119,7 +120,7 @@ export declare class VideoExtractionStageError extends Error {
119
120
  * validating and applying policy to it.
120
121
  */
121
122
  readonly publicMetadata: Readonly<Record<string, unknown>>;
122
- constructor(code: VideoExtractionStageErrorCode, retryable: boolean, failures: readonly VideoExtractionStageFailureSummary[], extractionFailure?: ExtractionFailureMetadataV1, ffmpegReason?: string);
123
+ constructor(code: VideoExtractionStageErrorCode, retryable: boolean, failures: readonly VideoExtractionStageFailureSummary[], extractionFailure?: ExtractionFailureMetadataV1, ffmpegReason?: string, sources?: readonly string[]);
123
124
  }
124
125
  export declare function assertVideoExtractionSucceeded(result: ExtractionResult): void;
125
126
  export declare function buildHdrProbeStageError(failures: readonly Pick<ReturnType<typeof classifyVideoExtractionError>, "kind" | "retryable">[]): VideoExtractionStageError;