hyperframes 0.7.53 → 0.7.54

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.
package/dist/cli.js CHANGED
@@ -50,7 +50,7 @@ var VERSION;
50
50
  var init_version = __esm({
51
51
  "src/version.ts"() {
52
52
  "use strict";
53
- VERSION = true ? "0.7.53" : "0.0.0-dev";
53
+ VERSION = true ? "0.7.54" : "0.0.0-dev";
54
54
  }
55
55
  });
56
56
 
@@ -64600,8 +64600,13 @@ async function getCompositionDuration(session) {
64600
64600
  return window.__hf?.duration ?? 0;
64601
64601
  });
64602
64602
  }
64603
+ function computeDeVerifySampleFractions(k2) {
64604
+ if (k2 <= 0) return [];
64605
+ if (k2 === 1) return [0.95];
64606
+ return [...Array.from({ length: k2 - 1 }, (_, i2) => (i2 + 1) / k2), 0.95];
64607
+ }
64603
64608
  async function captureDeVerificationFrames(session, page, logInitPhase) {
64604
- const kRaw = Number(process.env.HF_DE_VERIFY ?? "4");
64609
+ const kRaw = Number(process.env.HF_DE_VERIFY ?? session.options.deVerifySamples ?? "4");
64605
64610
  const k2 = Number.isFinite(kRaw) ? Math.max(0, Math.min(8, Math.floor(kRaw))) : 4;
64606
64611
  if (k2 === 0 || process.env.HF_FORCE_DRAWELEMENT === "1") return;
64607
64612
  if (session.options.format === "png") return;
@@ -64623,7 +64628,7 @@ async function captureDeVerificationFrames(session, page, logInitPhase) {
64623
64628
  }
64624
64629
  }
64625
64630
  const boundary = await computeClipBoundaryFrames(page, fps);
64626
- const fractions = Array.from({ length: k2 }, (_, i2) => (i2 + 1) / (k2 + 1));
64631
+ const fractions = computeDeVerifySampleFractions(k2);
64627
64632
  const seekTo = async (t2) => {
64628
64633
  await page.evaluate((tt3) => {
64629
64634
  const hf = window.__hf;
@@ -64672,6 +64677,8 @@ function getCapturePerfSummary(session) {
64672
64677
  staticDedupArmed: (session.staticFrames?.size ?? 0) > 0,
64673
64678
  staticDedupPredicted: session.staticFrames?.size ?? 0,
64674
64679
  staticDedupSkipReason: session.staticDedupSkipReason,
64680
+ beginFrameNoDamage: session.beginFrameNoDamageCount,
64681
+ beginFrameHasDamage: session.beginFrameHasDamageCount,
64675
64682
  captureMode: session.captureMode,
64676
64683
  deGateReason: session.deGateReason,
64677
64684
  deWorkerEncode: session.workerEncodeEnabled ?? false,
@@ -70849,10 +70856,11 @@ var init_dist4 = __esm({
70849
70856
  ];
70850
70857
  },
70851
70858
  // missing_timeline_registry + timeline_registry_missing_init
70852
- ({ source, rawSource, options }) => {
70859
+ ({ source, rawSource, rootTag, options }) => {
70853
70860
  if (options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template")) {
70854
70861
  return [];
70855
70862
  }
70863
+ if (/(?:^|\s)data-no-timeline(?=[\s=/]|$)/i.test(rootTag?.attrs || "")) return [];
70856
70864
  const findings = [];
70857
70865
  if (!TIMELINE_REGISTRY_INIT_PATTERN.test(source) && !TIMELINE_REGISTRY_ASSIGN_PATTERN.test(source) && !TIMELINE_REGISTRY_OBJECT_LITERAL_PATTERN.test(source)) {
70858
70866
  findings.push({
@@ -76850,6 +76858,11 @@ async function executeWorkerTask(task, serverUrl, captureOptions, createBeforeCa
76850
76858
  });
76851
76859
  }
76852
76860
  }
76861
+ function resolveParallelDeVerifySamples(callerValue, workerCount) {
76862
+ if (callerValue !== void 0) return callerValue;
76863
+ if (workerCount <= 1) return void 0;
76864
+ return Math.min(8, 4 + 2 * (workerCount - 1));
76865
+ }
76853
76866
  async function executeParallelCapture(serverUrl, workDir, tasks, captureOptions, createBeforeCaptureHook, signal, onProgress, onFrameBuffer, config) {
76854
76867
  const totalFrames = tasks.reduce(
76855
76868
  (sum, t2) => sum + Math.ceil((t2.endFrame - t2.startFrame) / (t2.frameStride ?? 1)),
@@ -76871,12 +76884,17 @@ async function executeParallelCapture(serverUrl, workDir, tasks, captureOptions,
76871
76884
  }
76872
76885
  };
76873
76886
  const parallel = tasks.length > 1;
76887
+ const deVerifySamples = resolveParallelDeVerifySamples(
76888
+ captureOptions.deVerifySamples,
76889
+ tasks.length
76890
+ );
76891
+ const workerCaptureOptions = deVerifySamples === captureOptions.deVerifySamples ? captureOptions : { ...captureOptions, deVerifySamples };
76874
76892
  const results = await Promise.all(
76875
76893
  tasks.map(
76876
76894
  (task) => executeWorkerTask(
76877
76895
  task,
76878
76896
  serverUrl,
76879
- captureOptions,
76897
+ workerCaptureOptions,
76880
76898
  createBeforeCaptureHook,
76881
76899
  signal,
76882
76900
  onFrameCaptured,
@@ -79261,6 +79279,7 @@ function renderObservabilityEventProperties(props) {
79261
79279
  de_pre_router_workers: props.captureDePreRouterWorkers,
79262
79280
  de_self_verify_fallback: props.captureDeSelfVerifyFallback,
79263
79281
  de_fallback_reason: props.captureDeFallbackReason,
79282
+ capture_parallel_stream: props.captureParallelStream,
79264
79283
  observability_extract_video_count: props.observabilityExtractVideoCount,
79265
79284
  observability_extracted_video_count: props.observabilityExtractedVideoCount,
79266
79285
  observability_extract_total_frames: props.observabilityExtractTotalFrames,
@@ -79305,6 +79324,8 @@ function trackRenderComplete(props) {
79305
79324
  static_dedup_skip_reason: props.staticDedupSkipReason,
79306
79325
  static_dedup_predicted_frames: props.staticDedupPredictedFrames,
79307
79326
  static_dedup_reused_frames: props.staticDedupReusedFrames,
79327
+ begin_frame_no_damage_frames: props.beginFrameNoDamageFrames,
79328
+ begin_frame_has_damage_frames: props.beginFrameHasDamageFrames,
79308
79329
  de_capture_mode: props.deCaptureMode,
79309
79330
  de_compile_gate: props.deCompileGate,
79310
79331
  de_clamp_reason: props.deClampReason,
@@ -84448,6 +84469,9 @@ function buildBrowserArgs(url, options) {
84448
84469
  if (options.remoteDebuggingPort !== void 0 && options.userDataDir) {
84449
84470
  args.push(`--remote-debugging-port=${options.remoteDebuggingPort}`);
84450
84471
  }
84472
+ if (options.disableGpu) {
84473
+ args.push("--disable-gpu");
84474
+ }
84451
84475
  args.push(url);
84452
84476
  return args;
84453
84477
  }
@@ -85312,6 +85336,7 @@ function renderObservabilityTelemetryPayload(observability) {
85312
85336
  captureDePreRouterWorkers: capture2.dePreRouterWorkers,
85313
85337
  captureDeSelfVerifyFallback: capture2.deSelfVerifyFallback,
85314
85338
  captureDeFallbackReason: capture2.deFallbackReason,
85339
+ captureParallelStream: capture2.captureParallelStream,
85315
85340
  observabilityExtractVideoCount: extraction?.videoCount,
85316
85341
  observabilityExtractedVideoCount: extraction?.extractedVideoCount,
85317
85342
  observabilityExtractTotalFrames: extraction?.totalFramesExtracted,
@@ -101069,6 +101094,12 @@ function aggregateDedup(perfs) {
101069
101094
  skipReason: skipReasons.length > 0 ? skipReasons.join("|") : void 0
101070
101095
  };
101071
101096
  }
101097
+ function aggregateBeginFrameReuse(perfs) {
101098
+ const noDamageFrames = perfs.reduce((sum, p2) => sum + (p2.beginFrameNoDamage ?? 0), 0);
101099
+ const hasDamageFrames = perfs.reduce((sum, p2) => sum + (p2.beginFrameHasDamage ?? 0), 0);
101100
+ if (noDamageFrames + hasDamageFrames === 0) return void 0;
101101
+ return { noDamageFrames, hasDamageFrames };
101102
+ }
101072
101103
  function buildRenderPerfSummary(input2) {
101073
101104
  return {
101074
101105
  renderId: input2.job.id,
@@ -101112,6 +101143,7 @@ function buildRenderPerfSummary(input2) {
101112
101143
  peakRssMb: Math.round(input2.peakRssBytes / (1024 * 1024)),
101113
101144
  peakHeapUsedMb: Math.round(input2.peakHeapUsedBytes / (1024 * 1024)),
101114
101145
  staticDedup: aggregateDedup(input2.dedupPerfs),
101146
+ beginFrameReuse: aggregateBeginFrameReuse(input2.dedupPerfs),
101115
101147
  drawElement: aggregateDrawElement(
101116
101148
  input2.dedupPerfs,
101117
101149
  input2.drawElement ?? { selfVerifyFallback: false }
@@ -106318,6 +106350,39 @@ function detectThreeDTransformUsage(html) {
106318
106350
  function detectMixBlendModeUsage(html) {
106319
106351
  return MIX_BLEND_MODE_PATTERN.test(html);
106320
106352
  }
106353
+ function detectAncestorBackgroundImage(html) {
106354
+ const { document: document2 } = parseHTML(html);
106355
+ const root = document2.querySelector("[data-composition-id]");
106356
+ if (!root) return false;
106357
+ const ancestors = [];
106358
+ for (let el = root.parentElement; el; el = el.parentElement) ancestors.push(el);
106359
+ if (document2.documentElement && !ancestors.includes(document2.documentElement)) {
106360
+ ancestors.push(document2.documentElement);
106361
+ }
106362
+ for (const el of ancestors) {
106363
+ const style = el.getAttribute("style");
106364
+ if (style && BACKGROUND_IMAGE_DECL_PATTERN.test(`{${style}}`)) return true;
106365
+ }
106366
+ for (const styleEl of document2.querySelectorAll("style")) {
106367
+ const css = styleEl.textContent ?? "";
106368
+ for (const rule of css.matchAll(/([^{}]+)\{([^}]*)\}/g)) {
106369
+ const [, selectorList = "", declarations = ""] = rule;
106370
+ if (!BACKGROUND_IMAGE_DECL_PATTERN.test(`{${declarations}}`)) continue;
106371
+ for (const selector of selectorList.split(",")) {
106372
+ const sel = selector.trim();
106373
+ if (!sel || sel.startsWith("@")) continue;
106374
+ if (/^(?:html|:root)$/i.test(sel)) return true;
106375
+ try {
106376
+ for (const matched of document2.querySelectorAll(sel)) {
106377
+ if (ancestors.includes(matched)) return true;
106378
+ }
106379
+ } catch {
106380
+ }
106381
+ }
106382
+ }
106383
+ }
106384
+ return false;
106385
+ }
106321
106386
  function detectShaderTransitionUsage(html) {
106322
106387
  let scriptMatch;
106323
106388
  const scriptPattern = new RegExp(INLINE_SCRIPT_PATTERN.source, INLINE_SCRIPT_PATTERN.flags);
@@ -107150,6 +107215,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
107150
107215
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
107151
107216
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
107152
107217
  const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
107218
+ const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
107153
107219
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
107154
107220
  injectTextRenderingRule2(
107155
107221
  coalesceHeadStylesAndBodyScripts2(promoteCssImportsToLinkTags(sanitizedHtml))
@@ -107256,7 +107322,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
107256
107322
  renderModeHints,
107257
107323
  hasShaderTransitions,
107258
107324
  usesThreeDTransforms,
107259
- usesMixBlendMode
107325
+ usesMixBlendMode,
107326
+ hasAncestorBackgroundImage
107260
107327
  };
107261
107328
  }
107262
107329
  async function discoverMediaFromBrowser(page) {
@@ -107495,7 +107562,7 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
107495
107562
  hasShaderTransitions: compiled.hasShaderTransitions
107496
107563
  };
107497
107564
  }
107498
- var EmptyCompositionError, INLINE_SCRIPT_PATTERN, COMPILER_MOUNT_BLOCK_START, COMPILER_MOUNT_BLOCK_END, THREE_D_CONTEXT_PATTERN, MIX_BLEND_MODE_PATTERN, SHADER_TRANSITION_USAGE_PATTERN, REMOTE_MEDIA_SUBDIR, REMOTE_MEDIA_TAG_RE, REMOTE_IMG_TAG_RE, REMOTE_BG_URL_RE, REMOTE_FONTFACE_URL_RE, MAX_STYLESHEET_BYTES, LOCAL_FONTFACE_URL_RE, GSAP_CDN_BASE;
107565
+ var EmptyCompositionError, INLINE_SCRIPT_PATTERN, COMPILER_MOUNT_BLOCK_START, COMPILER_MOUNT_BLOCK_END, THREE_D_CONTEXT_PATTERN, MIX_BLEND_MODE_PATTERN, BACKGROUND_IMAGE_DECL_PATTERN, SHADER_TRANSITION_USAGE_PATTERN, REMOTE_MEDIA_SUBDIR, REMOTE_MEDIA_TAG_RE, REMOTE_IMG_TAG_RE, REMOTE_BG_URL_RE, REMOTE_FONTFACE_URL_RE, MAX_STYLESHEET_BYTES, LOCAL_FONTFACE_URL_RE, GSAP_CDN_BASE;
107499
107566
  var init_htmlCompiler2 = __esm({
107500
107567
  "../producer/src/services/htmlCompiler.ts"() {
107501
107568
  "use strict";
@@ -107532,6 +107599,7 @@ Check that each file referenced by data-composition-src contains valid HTML with
107532
107599
  COMPILER_MOUNT_BLOCK_END = "/* __HF_COMPILER_MOUNT_END__ */";
107533
107600
  THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibility\s*:|perspective\s*:\s*[0-9]|perspective\s*\(|matrix3d\s*\(|rotate3d\s*\(|\btransformPerspective\b/i;
107534
107601
  MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
107602
+ BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
107535
107603
  SHADER_TRANSITION_USAGE_PATTERN = /\b(?:(?:window|globalThis)\s*\.\s*)?HyperShader\s*\.\s*init\s*\(|\b__hf\s*\.\s*transitions\s*=/;
107536
107604
  REMOTE_MEDIA_SUBDIR = "_remote_media";
107537
107605
  REMOTE_MEDIA_TAG_RE = /<(?:video|audio)\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
@@ -107586,6 +107654,13 @@ async function runCompileStage(input2) {
107586
107654
  "[Render] Fast capture: composition uses mix-blend-mode \u2014 disabling drawElementImage for this render. Capture uses the platform's baseline route."
107587
107655
  );
107588
107656
  }
107657
+ if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
107658
+ cfg.useDrawElement = false;
107659
+ deCompileGate = "ancestor_background_image";
107660
+ log2.info(
107661
+ "[Render] Fast capture: composition root's ancestors (body/html/wrapper) carry a background-image \u2014 disabling drawElementImage for this render. Capture uses the platform's baseline route."
107662
+ );
107663
+ }
107589
107664
  if (cfg.useDrawElement && process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE !== "true" && compiled.hasShaderTransitions) {
107590
107665
  cfg.useDrawElement = false;
107591
107666
  deCompileGate = "shader_transitions";
@@ -109632,7 +109707,8 @@ import {
109632
109707
  mkdirSync as mkdirSync19,
109633
109708
  mkdtempSync as mkdtempSync3,
109634
109709
  openSync as openSync3,
109635
- readFileSync as readFileSync25
109710
+ readFileSync as readFileSync25,
109711
+ statfsSync as statfsSync2
109636
109712
  } from "fs";
109637
109713
  import { join as join43 } from "path";
109638
109714
  function tempDirSafePrefix(id) {
@@ -109715,9 +109791,52 @@ async function probeHdrExtractionDims(args) {
109715
109791
  }
109716
109792
  }
109717
109793
  }
109794
+ function estimateHdrExtractionBytes(videos, fps) {
109795
+ let total = 0;
109796
+ for (const v2 of videos) {
109797
+ const frames = Math.ceil(Math.max(0, v2.durationSeconds) * fps);
109798
+ total += frames * v2.width * v2.height * 6;
109799
+ }
109800
+ return total;
109801
+ }
109802
+ function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log2) {
109803
+ const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
109804
+ let freeBytes;
109805
+ try {
109806
+ const stat3 = statfsSync2(framesDir);
109807
+ freeBytes = stat3.bavail * stat3.bsize;
109808
+ } catch {
109809
+ return;
109810
+ }
109811
+ const estimatedGb = (estimatedBytes / 1e9).toFixed(1);
109812
+ if (estimatedBytes > freeBytes * HDR_EXTRACTION_HEADROOM_FRACTION) {
109813
+ throw new Error(
109814
+ `HDR pre-extraction needs ~${estimatedGb} GB of raw 16-bit frames but only ${(freeBytes / 1e9).toFixed(1)} GB is free at ${framesDir}. If the composition doesn't need HDR output, re-run with --sdr; otherwise free up disk space and retry.`
109815
+ );
109816
+ }
109817
+ if (estimatedBytes > HDR_EXTRACTION_WARN_BYTES) {
109818
+ log2.warn(
109819
+ `HDR pre-extraction will write ~${estimatedGb} GB of raw 16-bit frames (pass --sdr to skip if HDR output isn't needed)`,
109820
+ { estimatedBytes, freeBytes }
109821
+ );
109822
+ }
109823
+ }
109718
109824
  async function extractHdrVideoFrames(args) {
109719
109825
  const { job, log: log2, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } = args;
109720
109826
  const out = /* @__PURE__ */ new Map();
109827
+ mkdirSync19(framesDir, { recursive: true });
109828
+ const plannedVideos = [];
109829
+ for (const [videoId] of prep.hdrVideoSrcPaths) {
109830
+ const video = composition.videos.find((v2) => v2.id === videoId);
109831
+ if (!video) continue;
109832
+ const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
109833
+ plannedVideos.push({
109834
+ durationSeconds: video.end - video.start,
109835
+ width: dims.width,
109836
+ height: dims.height
109837
+ });
109838
+ }
109839
+ assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fpsToNumber(job.config.fps), log2);
109721
109840
  for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
109722
109841
  const video = composition.videos.find((v2) => v2.id === videoId);
109723
109842
  if (!video) continue;
@@ -109825,13 +109944,15 @@ function decodeHdrImageBuffers(args) {
109825
109944
  }
109826
109945
  return out;
109827
109946
  }
109828
- var NO_FOLLOW_FLAG;
109947
+ var NO_FOLLOW_FLAG, HDR_EXTRACTION_HEADROOM_FRACTION, HDR_EXTRACTION_WARN_BYTES;
109829
109948
  var init_captureHdrResources = __esm({
109830
109949
  "../producer/src/services/render/stages/captureHdrResources.ts"() {
109831
109950
  "use strict";
109832
109951
  init_src();
109833
109952
  init_dist3();
109834
109953
  NO_FOLLOW_FLAG = constants3.O_NOFOLLOW ?? 0;
109954
+ HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
109955
+ HDR_EXTRACTION_WARN_BYTES = 1e10;
109835
109956
  }
109836
109957
  });
109837
109958
 
@@ -110940,6 +111061,8 @@ async function runEncodeStage(input2) {
110940
111061
  return { encodeMs: Date.now() - stage5Start };
110941
111062
  }
110942
111063
  updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
111064
+ const scaledEncodeTimeout = Math.ceil((job.duration ?? 0) * 4e3);
111065
+ const videoEngineCfg = scaledEncodeTimeout > engineCfg.ffmpegEncodeTimeout ? { ...engineCfg, ffmpegEncodeTimeout: scaledEncodeTimeout } : engineCfg;
110943
111066
  const frameExt = needsAlpha ? "png" : "jpg";
110944
111067
  const framePattern = `frame_%06d.${frameExt}`;
110945
111068
  const encoderOpts = {
@@ -110967,14 +111090,14 @@ async function runEncodeStage(input2) {
110967
111090
  encoderOpts,
110968
111091
  chunkedEncodeSize,
110969
111092
  abortSignal,
110970
- engineCfg
111093
+ videoEngineCfg
110971
111094
  ) : await encodeFramesFromDir(
110972
111095
  framesDir,
110973
111096
  framePattern,
110974
111097
  videoOnlyPath,
110975
111098
  encoderOpts,
110976
111099
  abortSignal,
110977
- engineCfg
111100
+ videoEngineCfg
110978
111101
  );
110979
111102
  assertNotAborted();
110980
111103
  if (!encodeResult.success) {
@@ -111056,6 +111179,7 @@ import {
111056
111179
  appendFileSync
111057
111180
  } from "fs";
111058
111181
  import { join as join50, dirname as dirname19, resolve as resolve26 } from "path";
111182
+ import { totalmem as totalmem2 } from "os";
111059
111183
  import { randomUUID as randomUUID4 } from "crypto";
111060
111184
  import { fileURLToPath as fileURLToPath5 } from "url";
111061
111185
  function sampleDirectoryBytes(dir) {
@@ -111380,11 +111504,24 @@ function createRenderJob(config) {
111380
111504
  function normalizeCompositionSrcPath(srcPath) {
111381
111505
  return srcPath.replace(/\\/g, "/").replace(/^\.\//, "");
111382
111506
  }
111383
- function createStandaloneEntryRenderClone(root, host) {
111507
+ function readSceneRootDuration(entryHtml) {
111508
+ if (!entryHtml) return null;
111509
+ const { document: document2 } = parseHTML(entryHtml);
111510
+ const template = document2.querySelector("template");
111511
+ const scope = template ? parseHTML(template.innerHTML).document : document2;
111512
+ const root = scope.querySelector("[data-composition-id]");
111513
+ return root?.getAttribute("data-duration") ?? null;
111514
+ }
111515
+ function createStandaloneEntryRenderClone(root, host, sceneDuration) {
111384
111516
  const hostClone = host.cloneNode(true);
111385
111517
  hostClone.setAttribute("data-start", "0");
111386
111518
  if (root === host) return hostClone;
111387
111519
  const rootClone = root.cloneNode(false);
111520
+ if (sceneDuration != null) {
111521
+ rootClone.setAttribute("data-duration", sceneDuration);
111522
+ } else {
111523
+ rootClone.removeAttribute("data-duration");
111524
+ }
111388
111525
  rootClone.appendChild(hostClone);
111389
111526
  return rootClone;
111390
111527
  }
@@ -111421,7 +111558,13 @@ function resolveInversionRetryPlan(args) {
111421
111558
  };
111422
111559
  }
111423
111560
  function shouldPreferParallelDrawElement(args) {
111424
- return args.routerEnabled && args.workerCount > 1 && typeof args.requestedWorkers !== "number" && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && args.outputFormat === "mp4" && args.minFrames > 0 && args.totalFrames >= args.minFrames && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
111561
+ return args.routerEnabled && args.workerCount > 1 && typeof args.requestedWorkers !== "number" && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && args.outputFormat === "mp4" && args.minFrames > 0 && args.totalFrames >= args.minFrames && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn && // RAM floor: routed parallel DE runs 3 concurrent hardware-GPU Chrome
111562
+ // instances. On a 16 GB machine that produced vertical black slabs in the
111563
+ // final MP4 (wild report, CLI 0.7.52) — compositor tiles evicted under
111564
+ // GPU/memory pressure, and sampled self-verify can miss partial-frame
111565
+ // damage. Single-worker DE (the inversion) stays available below the
111566
+ // floor; only the parallel bet is withheld.
111567
+ (args.minMemoryMb <= 0 || args.totalMemoryMb >= args.minMemoryMb);
111425
111568
  }
111426
111569
  function resolveParallelRouterRetryPlan(args) {
111427
111570
  if (args.deParallelRouter !== "routed") return null;
@@ -111442,13 +111585,16 @@ function shouldRetryViaPinnedFallback(args) {
111442
111585
  if (args.isVerifyError) return true;
111443
111586
  return args.deWorkerInversion === "inverted" || args.deParallelRouter === "routed";
111444
111587
  }
111588
+ function shouldStreamParallelCapture(args) {
111589
+ return args.routerEnabled && args.workerCount > 1 && !args.useDrawElement && args.outputFormat === "mp4" && args.streamingOk && !args.layeredOrEffectRoute;
111590
+ }
111445
111591
  function resolveCaptureForceScreenshotForPageSideCompositing(args) {
111446
111592
  return args.usePageSideCompositing ? true : args.forceScreenshot;
111447
111593
  }
111448
111594
  function shouldDiscardProbeSessionForPageSideCompositing(args) {
111449
111595
  return args.hasProbeSession && args.usePageSideCompositing;
111450
111596
  }
111451
- function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
111597
+ function extractStandaloneEntryFromIndex(indexHtml, entryFile, entryHtml) {
111452
111598
  const normalizedEntryFile = normalizeCompositionSrcPath(entryFile);
111453
111599
  const { document: document2 } = parseHTML(indexHtml);
111454
111600
  const body = document2.querySelector("body");
@@ -111462,7 +111608,8 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
111462
111608
  (candidate) => candidate.hasAttribute("data-composition-id")
111463
111609
  ) ?? null;
111464
111610
  if (!root) return null;
111465
- const renderClone = createStandaloneEntryRenderClone(root, host);
111611
+ const sceneDuration = readSceneRootDuration(entryHtml) ?? host.getAttribute("data-duration");
111612
+ const renderClone = createStandaloneEntryRenderClone(root, host, sceneDuration);
111466
111613
  replaceBodyWithRenderClone(body, renderClone);
111467
111614
  return document2.toString();
111468
111615
  }
@@ -111582,7 +111729,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111582
111729
  }
111583
111730
  const standaloneHtml = extractStandaloneEntryFromIndex(
111584
111731
  readFileSync26(projectIndexPath, "utf-8"),
111585
- entryFile
111732
+ entryFile,
111733
+ rawEntry
111586
111734
  );
111587
111735
  if (!standaloneHtml) {
111588
111736
  throw new Error(
@@ -111627,6 +111775,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111627
111775
  let deClampReason;
111628
111776
  let deWorkerInversion;
111629
111777
  let deParallelStreamForced = false;
111778
+ let captureParallelStreamForced = false;
111630
111779
  let deSelfVerifyFallback = false;
111631
111780
  let deFallbackReason;
111632
111781
  let deDrainStats;
@@ -111882,6 +112031,9 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111882
112031
  const deParallelMinFramesRaw = process.env.HF_DE_PARALLEL_MIN_FRAMES;
111883
112032
  const deParallelMinFramesNum = deParallelMinFramesRaw === void 0 || deParallelMinFramesRaw.trim() === "" ? 2e3 : Number(deParallelMinFramesRaw);
111884
112033
  const deParallelMinFrames = Number.isFinite(deParallelMinFramesNum) ? deParallelMinFramesNum : 2e3;
112034
+ const deParallelMinMemRaw = process.env.HF_DE_PARALLEL_MIN_MEM_MB;
112035
+ const deParallelMinMemNum = deParallelMinMemRaw === void 0 || deParallelMinMemRaw.trim() === "" ? 24576 : Number(deParallelMinMemRaw);
112036
+ const deParallelMinMemoryMb = Number.isFinite(deParallelMinMemNum) ? deParallelMinMemNum : 24576;
111885
112037
  const deParallelRouterEligible = shouldPreferParallelDrawElement({
111886
112038
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
111887
112039
  requestedWorkers: job.config.workers,
@@ -111895,7 +112047,9 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111895
112047
  supersampling: deviceScaleFactor > 1,
111896
112048
  probeDeGated: probeSession !== null && probeSession.captureMode !== "drawelement" && !probeSession.deInitDeferred,
111897
112049
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
111898
- routerEnabled: deParallelRouterEnabled
112050
+ routerEnabled: deParallelRouterEnabled,
112051
+ totalMemoryMb: Math.round(totalmem2() / (1024 * 1024)),
112052
+ minMemoryMb: deParallelMinMemoryMb
111899
112053
  });
111900
112054
  let workerCount;
111901
112055
  const captureStageObservationData = (extra = {}) => ({
@@ -112005,6 +112159,32 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112005
112159
  deWorkerInversion: deWorkerInversion ?? "none",
112006
112160
  deParallelRouter: deParallelRouter ?? "none"
112007
112161
  });
112162
+ const captureParallelStreamRouterEnabled = process.env.HF_CAPTURE_PARALLEL_STREAM === "true";
112163
+ const captureParallelStreamArgs = {
112164
+ workerCount,
112165
+ useDrawElement: cfg.useDrawElement,
112166
+ outputFormat,
112167
+ streamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration),
112168
+ layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions
112169
+ };
112170
+ const captureParallelStreamEligible = shouldStreamParallelCapture({
112171
+ routerEnabled: captureParallelStreamRouterEnabled,
112172
+ ...captureParallelStreamArgs
112173
+ });
112174
+ if (captureParallelStreamEligible) {
112175
+ captureParallelStreamForced = true;
112176
+ const captureParallelStream = process.platform === "linux" && !captureForceScreenshot ? "beginframe" : "screenshot";
112177
+ log2.info(
112178
+ `[Render] Parallel ${captureParallelStream} capture will stream to the encoder (interleaved, ${workerCount} workers) instead of the disk path. Set HF_CAPTURE_PARALLEL_STREAM=false to disable.`
112179
+ );
112180
+ updateCaptureObservability({ captureParallelStream });
112181
+ observability.checkpoint(
112182
+ "worker_resolution",
112183
+ `parallel ${captureParallelStream} capture routed to streaming`
112184
+ );
112185
+ } else if (shouldStreamParallelCapture({ routerEnabled: true, ...captureParallelStreamArgs })) {
112186
+ updateCaptureObservability({ captureParallelStream: "eligible_off" });
112187
+ }
112008
112188
  if (workerCount > 1 && probeSession) {
112009
112189
  lastBrowserConsole = probeSession.browserConsoleBuffer;
112010
112190
  await closeCaptureSession(probeSession);
@@ -112015,7 +112195,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112015
112195
  outputFormat,
112016
112196
  workerCount,
112017
112197
  job.duration,
112018
- deParallelStreamForced
112198
+ deParallelStreamForced || captureParallelStreamForced
112019
112199
  );
112020
112200
  log2.info("streaming-encode gate", {
112021
112201
  enabled: useStreamingEncode,
@@ -112172,7 +112352,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
112172
112352
  workerCount,
112173
112353
  probeSession,
112174
112354
  outputFormat,
112175
- forceParallelStream: deParallelStreamForced,
112355
+ forceParallelStream: deParallelStreamForced || captureParallelStreamForced,
112176
112356
  streamingEncoderOptions: {
112177
112357
  fps: job.config.fps,
112178
112358
  width,
@@ -115270,6 +115450,7 @@ __export(src_exports2, {
115270
115450
  getCapturePerfSummary: () => getCapturePerfSummary,
115271
115451
  getCompositionDuration: () => getCompositionDuration,
115272
115452
  initializeSession: () => initializeSession,
115453
+ injectDeterministicFontFaces: () => injectDeterministicFontFaces,
115273
115454
  localizeRemoteFontFaces: () => localizeRemoteFontFaces,
115274
115455
  localizeRemoteImageSources: () => localizeRemoteImageSources,
115275
115456
  localizeRemoteMediaSources: () => localizeRemoteMediaSources,
@@ -115296,6 +115477,7 @@ var init_src2 = __esm({
115296
115477
  init_logger();
115297
115478
  init_server();
115298
115479
  init_errorMessage2();
115480
+ init_deterministicFonts();
115299
115481
  init_parityContract2();
115300
115482
  init_paths();
115301
115483
  init_hyperframeLint();
@@ -117206,7 +117388,8 @@ function openStudioBrowser(url, projectName, options) {
117206
117388
  openBrowser(`${url}#project/${projectName}`, {
117207
117389
  browserPath: options?.browserPath,
117208
117390
  userDataDir: options?.userDataDir,
117209
- remoteDebuggingPort: options?.remoteDebuggingPort
117391
+ remoteDebuggingPort: options?.remoteDebuggingPort,
117392
+ disableGpu: options?.browserNoGpu
117210
117393
  });
117211
117394
  }
117212
117395
  function printStudioSummary(projectName, url, opts = {}) {
@@ -117528,6 +117711,11 @@ var init_preview = __esm({
117528
117711
  "remote-debugging-port": {
117529
117712
  type: "string",
117530
117713
  description: "Chromium remote debugging port (requires --browser-path and --user-data-dir)"
117714
+ },
117715
+ "browser-no-gpu": {
117716
+ type: "boolean",
117717
+ default: false,
117718
+ description: "Launch the opened browser with --disable-gpu (requires --browser-path). For hosts where hardware acceleration crashes the graphics driver (e.g. NVIDIA Xid resets); with the system default browser use --no-open instead."
117531
117719
  }
117532
117720
  },
117533
117721
  async run({ args }) {
@@ -117617,6 +117805,14 @@ var init_preview = __esm({
117617
117805
  }
117618
117806
  const noOpen = !args.open;
117619
117807
  const browserPath = args["browser-path"];
117808
+ const browserNoGpu = !!args["browser-no-gpu"];
117809
+ if (browserNoGpu && !browserPath) {
117810
+ R2.error(
117811
+ "--browser-no-gpu requires --browser-path (the system default browser cannot receive Chromium flags \u2014 use --no-open on GPU-unstable hosts)"
117812
+ );
117813
+ process.exitCode = 1;
117814
+ return;
117815
+ }
117620
117816
  const userDataDir = args["user-data-dir"];
117621
117817
  let remoteDebuggingPort;
117622
117818
  try {
@@ -117634,7 +117830,8 @@ var init_preview = __esm({
117634
117830
  noOpen,
117635
117831
  browserPath,
117636
117832
  userDataDir,
117637
- remoteDebuggingPort
117833
+ remoteDebuggingPort,
117834
+ browserNoGpu
117638
117835
  });
117639
117836
  }
117640
117837
  if (hasLocalStudio(dir)) {
@@ -117643,7 +117840,8 @@ var init_preview = __esm({
117643
117840
  noOpen,
117644
117841
  browserPath,
117645
117842
  userDataDir,
117646
- remoteDebuggingPort
117843
+ remoteDebuggingPort,
117844
+ browserNoGpu
117647
117845
  });
117648
117846
  }
117649
117847
  const forceNew = !!args["force-new"];
@@ -117653,7 +117851,8 @@ var init_preview = __esm({
117653
117851
  noOpen,
117654
117852
  browserPath,
117655
117853
  userDataDir,
117656
- remoteDebuggingPort
117854
+ remoteDebuggingPort,
117855
+ browserNoGpu
117657
117856
  });
117658
117857
  }
117659
117858
  });
@@ -121735,6 +121934,8 @@ function trackRenderMetrics(job, elapsedMs, options, docker) {
121735
121934
  staticDedupSkipReason: perf?.staticDedup?.skipReason,
121736
121935
  staticDedupPredictedFrames: perf?.staticDedup?.predictedFrames,
121737
121936
  staticDedupReusedFrames: perf?.staticDedup?.reusedFrames,
121937
+ beginFrameNoDamageFrames: perf?.beginFrameReuse?.noDamageFrames,
121938
+ beginFrameHasDamageFrames: perf?.beginFrameReuse?.hasDamageFrames,
121738
121939
  deCaptureMode: perf?.drawElement?.mode,
121739
121940
  deCompileGate: perf?.drawElement?.compileGate,
121740
121941
  deClampReason: perf?.drawElement?.clampReason,
@@ -123419,8 +123620,9 @@ async function seekCompositionTimeline(page, timeSeconds, options = {}) {
123419
123620
  )
123420
123621
  );
123421
123622
  }
123422
- if (options.waitForFontsMs !== void 0) {
123423
- await waitForCompositionFonts(page, options.waitForFontsMs);
123623
+ const waitForFontsMs = options.waitForFontsMs ?? DEFAULT_POST_SEEK_FONT_WAIT_MS;
123624
+ if (waitForFontsMs > 0) {
123625
+ await waitForCompositionFonts(page, waitForFontsMs);
123424
123626
  }
123425
123627
  if (options.settleMs !== void 0) {
123426
123628
  const settleMs = Math.max(0, options.settleMs);
@@ -123447,12 +123649,23 @@ async function waitForCompositionFonts(page, timeoutMs) {
123447
123649
  await page.evaluate((ms) => {
123448
123650
  const fonts = Reflect.get(document, "fonts");
123449
123651
  if (typeof fonts !== "object" || fonts === null) return Promise.resolve();
123450
- const ready = Reflect.get(fonts, "ready");
123451
- if (!ready) return Promise.resolve();
123452
- return Promise.race([
123453
- Promise.resolve(ready).then(() => void 0),
123454
- new Promise((resolve64) => setTimeout(resolve64, ms))
123455
- ]);
123652
+ void document.body?.offsetHeight;
123653
+ return new Promise((resolveWait) => {
123654
+ const deadline = setTimeout(resolveWait, ms);
123655
+ requestAnimationFrame(() => {
123656
+ const status = Reflect.get(fonts, "status");
123657
+ const ready = Reflect.get(fonts, "ready");
123658
+ if (status !== "loading" || !ready) {
123659
+ clearTimeout(deadline);
123660
+ resolveWait();
123661
+ return;
123662
+ }
123663
+ Promise.resolve(ready).then(() => {
123664
+ clearTimeout(deadline);
123665
+ resolveWait();
123666
+ });
123667
+ });
123668
+ });
123456
123669
  }, timeoutMs).catch(() => {
123457
123670
  });
123458
123671
  }
@@ -123527,7 +123740,7 @@ async function runFfmpegOnce(ffmpegPath, args, timeoutMs) {
123527
123740
  });
123528
123741
  });
123529
123742
  }
123530
- var SHADER_TRANSITIONS_TIMEOUT_MS, CAPTURE_SETTLE_MS, PREFERRED_SEEK_TARGET_WAIT_MS, AUDIT_SEEK_OPTIONS, ZOOM_REGION_PATTERN, DEFAULT_ZOOM_PADDING_PX, DEFAULT_ZOOM_SCALE, MIN_CROP_REGION_PX;
123743
+ var SHADER_TRANSITIONS_TIMEOUT_MS, CAPTURE_SETTLE_MS, PREFERRED_SEEK_TARGET_WAIT_MS, DEFAULT_POST_SEEK_FONT_WAIT_MS, AUDIT_SEEK_OPTIONS, ZOOM_REGION_PATTERN, DEFAULT_ZOOM_PADDING_PX, DEFAULT_ZOOM_SCALE, MIN_CROP_REGION_PX;
123531
123744
  var init_captureCompositionFrame = __esm({
123532
123745
  "src/capture/captureCompositionFrame.ts"() {
123533
123746
  "use strict";
@@ -123536,6 +123749,7 @@ var init_captureCompositionFrame = __esm({
123536
123749
  SHADER_TRANSITIONS_TIMEOUT_MS = 9e4;
123537
123750
  CAPTURE_SETTLE_MS = 1500;
123538
123751
  PREFERRED_SEEK_TARGET_WAIT_MS = 500;
123752
+ DEFAULT_POST_SEEK_FONT_WAIT_MS = 500;
123539
123753
  AUDIT_SEEK_OPTIONS = {
123540
123754
  fallbackToBridgeAndTimelines: true,
123541
123755
  animationFrameSettle: "double",
@@ -124512,6 +124726,57 @@ Examples:
124512
124726
  }
124513
124727
  });
124514
124728
 
124729
+ // src/utils/bundleWithLocalizedFonts.ts
124730
+ var bundleWithLocalizedFonts_exports = {};
124731
+ __export(bundleWithLocalizedFonts_exports, {
124732
+ __resetFontLocalizationWarningsForTests: () => __resetFontLocalizationWarningsForTests,
124733
+ bundleWithLocalizedFonts: () => bundleWithLocalizedFonts,
124734
+ localizeWithProducer: () => localizeWithProducer
124735
+ });
124736
+ async function bundleWithLocalizedFonts(projectDir, localizeFonts = localizeWithProducer) {
124737
+ const { bundleToSingleHtml: bundleToSingleHtml2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
124738
+ const html = await bundleToSingleHtml2(projectDir);
124739
+ return localizeFonts(html);
124740
+ }
124741
+ async function loadFontInjector() {
124742
+ try {
124743
+ const producerSpecifier = "@hyperframes/producer";
124744
+ const mod = await import(
124745
+ /* @vite-ignore */
124746
+ producerSpecifier
124747
+ );
124748
+ return mod.injectDeterministicFontFaces ?? null;
124749
+ } catch {
124750
+ return null;
124751
+ }
124752
+ }
124753
+ function __resetFontLocalizationWarningsForTests() {
124754
+ warnedFontLocalizationFailures.clear();
124755
+ }
124756
+ async function localizeWithProducer(html, loadInjector = loadFontInjector, warn = (m2) => console.warn(` ${c.warn("\u26A0")} ${m2}`)) {
124757
+ const inject = await loadInjector();
124758
+ if (!inject) return html;
124759
+ try {
124760
+ return await inject(html);
124761
+ } catch (err) {
124762
+ const message = `Font localization failed; capturing with remote/fallback fonts instead: ${normalizeErrorMessage(err)}`;
124763
+ if (!warnedFontLocalizationFailures.has(message)) {
124764
+ warnedFontLocalizationFailures.add(message);
124765
+ warn(message);
124766
+ }
124767
+ return html;
124768
+ }
124769
+ }
124770
+ var warnedFontLocalizationFailures;
124771
+ var init_bundleWithLocalizedFonts = __esm({
124772
+ "src/utils/bundleWithLocalizedFonts.ts"() {
124773
+ "use strict";
124774
+ init_errorMessage();
124775
+ init_colors();
124776
+ warnedFontLocalizationFailures = /* @__PURE__ */ new Set();
124777
+ }
124778
+ });
124779
+
124515
124780
  // src/utils/checkBrowser.ts
124516
124781
  var checkBrowser_exports = {};
124517
124782
  __export(checkBrowser_exports, {
@@ -124522,8 +124787,8 @@ __export(checkBrowser_exports, {
124522
124787
  import { mkdirSync as mkdirSync36, writeFileSync as writeFileSync26 } from "fs";
124523
124788
  import { join as join76 } from "path";
124524
124789
  async function runBrowserCheck(project, options, motion, runGrid) {
124525
- const { bundleToSingleHtml: bundleToSingleHtml2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
124526
- const html = await bundleToSingleHtml2(project.dir);
124790
+ const { bundleWithLocalizedFonts: bundleWithLocalizedFonts2 } = await Promise.resolve().then(() => (init_bundleWithLocalizedFonts(), bundleWithLocalizedFonts_exports));
124791
+ const html = await bundleWithLocalizedFonts2(project.dir);
124527
124792
  const server = await serveStaticProjectHtml(project.dir, html, "Failed to bind check server");
124528
124793
  const drafts = [];
124529
124794
  let currentTime = 0;
@@ -124561,8 +124826,8 @@ async function runBrowserCheck(project, options, motion, runGrid) {
124561
124826
  }
124562
124827
  async function captureFindingCrops(project, options, requests) {
124563
124828
  if (requests.length === 0) return [];
124564
- const { bundleToSingleHtml: bundleToSingleHtml2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
124565
- const html = await bundleToSingleHtml2(project.dir);
124829
+ const { bundleWithLocalizedFonts: bundleWithLocalizedFonts2 } = await Promise.resolve().then(() => (init_bundleWithLocalizedFonts(), bundleWithLocalizedFonts_exports));
124830
+ const html = await bundleWithLocalizedFonts2(project.dir);
124566
124831
  const server = await serveStaticProjectHtml(project.dir, html, "Failed to bind check server");
124567
124832
  let chromeBrowser;
124568
124833
  const written = [];
@@ -125245,6 +125510,7 @@ var init_checkBrowser = __esm({
125245
125510
  "container_overflow",
125246
125511
  "content_overlap",
125247
125512
  "text_occluded",
125513
+ "text_not_painted",
125248
125514
  "caption_zone_collision",
125249
125515
  "frame_out_of_frame",
125250
125516
  "motion_appears_late",
@@ -172136,9 +172402,9 @@ function computeSnapshotTimes(duration, opts) {
172136
172402
  return { times: times.map(round5), appendedTail: false };
172137
172403
  }
172138
172404
  async function captureSnapshots(projectDir, opts) {
172139
- const { bundleToSingleHtml: bundleToSingleHtml2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
172405
+ const { bundleWithLocalizedFonts: bundleWithLocalizedFonts2 } = await Promise.resolve().then(() => (init_bundleWithLocalizedFonts(), bundleWithLocalizedFonts_exports));
172140
172406
  const numFrames = opts.frames ?? 5;
172141
- const html = await bundleToSingleHtml2(projectDir);
172407
+ const html = await bundleWithLocalizedFonts2(projectDir);
172142
172408
  const server = await serveStaticProjectHtml(projectDir, html);
172143
172409
  const savedPaths = [];
172144
172410
  try {
@@ -172213,6 +172479,9 @@ async function captureSnapshots(projectDir, opts) {
172213
172479
  syncVideoFrameVisibility2 = engine.syncVideoFrameVisibility;
172214
172480
  extractMediaMetadata2 = engine.extractMediaMetadata;
172215
172481
  } catch {
172482
+ console.warn(
172483
+ ` ${c.warn("\u26A0")} @hyperframes/engine unavailable \u2014 <video> elements will appear black in snapshots. Verify media via a draft render's extracted frames instead.`
172484
+ );
172216
172485
  }
172217
172486
  const alphaDecoderCache = /* @__PURE__ */ new Map();
172218
172487
  const shouldUseVp9AlphaDecoder = (filePath) => {
@@ -172285,6 +172554,12 @@ async function captureSnapshots(projectDir, opts) {
172285
172554
  dataUri: `data:image/png;base64,${png.toString("base64")}`
172286
172555
  });
172287
172556
  }
172557
+ if (active.length > 0 && updates.length < active.length) {
172558
+ const missed = active.length - updates.length;
172559
+ console.warn(
172560
+ ` ${c.warn("\u26A0")} ${missed}/${active.length} active <video> frame(s) could not be extracted at ${time.toFixed(1)}s \u2014 those videos will appear black/stale in this snapshot`
172561
+ );
172562
+ }
172288
172563
  try {
172289
172564
  if (updates.length > 0) {
172290
172565
  await injectVideoFramesBatch2(page, updates);
@@ -172294,6 +172569,9 @@ async function captureSnapshots(projectDir, opts) {
172294
172569
  active.map((a) => a.id)
172295
172570
  );
172296
172571
  } catch {
172572
+ console.warn(
172573
+ ` ${c.warn("\u26A0")} video frame injection failed at ${time.toFixed(1)}s \u2014 <video> elements will appear black/stale in this snapshot`
172574
+ );
172297
172575
  }
172298
172576
  }
172299
172577
  const timeLabel = `${time.toFixed(1)}s`;