@hyperframes/producer 0.4.15 → 0.4.16

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.
@@ -57866,7 +57866,7 @@ var require_util2 = __commonJS({
57866
57866
  }
57867
57867
  path12 = url.path;
57868
57868
  }
57869
- var isAbsolute3 = exports.isAbsolute(path12);
57869
+ var isAbsolute5 = exports.isAbsolute(path12);
57870
57870
  var parts = path12.split(/\/+/);
57871
57871
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
57872
57872
  part = parts[i];
@@ -57886,7 +57886,7 @@ var require_util2 = __commonJS({
57886
57886
  }
57887
57887
  path12 = parts.join("/");
57888
57888
  if (path12 === "") {
57889
- path12 = isAbsolute3 ? "/" : ".";
57889
+ path12 = isAbsolute5 ? "/" : ".";
57890
57890
  }
57891
57891
  if (url) {
57892
57892
  url.path = path12;
@@ -104407,6 +104407,34 @@ function getGpuEncoderName(encoder, codec) {
104407
104407
  return codec === "h264" ? "libx264" : "libx265";
104408
104408
  }
104409
104409
  }
104410
+ var NVENC_PRESET_MAP = {
104411
+ ultrafast: "p1",
104412
+ superfast: "p1",
104413
+ veryfast: "p2",
104414
+ faster: "p3",
104415
+ fast: "p4",
104416
+ medium: "p4",
104417
+ slow: "p5",
104418
+ slower: "p6",
104419
+ veryslow: "p7",
104420
+ placebo: "p7"
104421
+ };
104422
+ var QSV_PRESET_MAP = {
104423
+ ultrafast: "veryfast",
104424
+ superfast: "veryfast",
104425
+ placebo: "veryslow"
104426
+ };
104427
+ function mapPresetForGpuEncoder(encoder, preset) {
104428
+ switch (encoder) {
104429
+ case "nvenc":
104430
+ if (/^p[1-7]$/.test(preset)) return preset;
104431
+ return NVENC_PRESET_MAP[preset] ?? "p4";
104432
+ case "qsv":
104433
+ return QSV_PRESET_MAP[preset] ?? preset;
104434
+ default:
104435
+ return preset;
104436
+ }
104437
+ }
104410
104438
 
104411
104439
  // ../engine/src/utils/hdr.ts
104412
104440
  function isHdrColorSpace(cs) {
@@ -104450,6 +104478,16 @@ function analyzeCompositionHdr(colorSpaces) {
104450
104478
  // ../engine/src/utils/runFfmpeg.ts
104451
104479
  import { spawn as spawn4 } from "child_process";
104452
104480
  var DEFAULT_TIMEOUT2 = 3e5;
104481
+ var DEFAULT_STDERR_TAIL_LINES = 15;
104482
+ function formatFfmpegError(exitCode, stderr, tailLines = DEFAULT_STDERR_TAIL_LINES) {
104483
+ const tail = (stderr ?? "").split(/\r?\n/).filter((line) => line.length > 0).slice(-tailLines).join("\n");
104484
+ if (exitCode === null) {
104485
+ return tail ? `[FFmpeg] ${tail}` : "[FFmpeg] process error";
104486
+ }
104487
+ return tail ? `FFmpeg exited with code ${exitCode}
104488
+ ffmpeg stderr (tail):
104489
+ ${tail}` : `FFmpeg exited with code ${exitCode}`;
104490
+ }
104453
104491
  async function runFfmpeg(args, opts) {
104454
104492
  const startMs = Date.now();
104455
104493
  const signal = opts?.signal;
@@ -104560,7 +104598,7 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
104560
104598
  args.push("-c:v", encoderName);
104561
104599
  switch (gpuEncoder) {
104562
104600
  case "nvenc":
104563
- args.push("-preset", preset);
104601
+ args.push("-preset", mapPresetForGpuEncoder("nvenc", preset));
104564
104602
  if (bitrate) args.push("-b:v", bitrate);
104565
104603
  else args.push("-cq", String(quality));
104566
104604
  break;
@@ -104579,7 +104617,7 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
104579
104617
  else args.push("-qp", String(quality));
104580
104618
  break;
104581
104619
  case "qsv":
104582
- args.push("-preset", preset);
104620
+ args.push("-preset", mapPresetForGpuEncoder("qsv", preset));
104583
104621
  if (bitrate) args.push("-b:v", bitrate);
104584
104622
  else args.push("-global_quality", String(quality));
104585
104623
  break;
@@ -104719,7 +104757,7 @@ async function encodeFramesFromDir(framesDir, framePattern, outputPath, options,
104719
104757
  durationMs,
104720
104758
  framesEncoded: 0,
104721
104759
  fileSize: 0,
104722
- error: `FFmpeg exited with code ${code}`
104760
+ error: formatFfmpegError(code, stderr)
104723
104761
  });
104724
104762
  return;
104725
104763
  }
@@ -104889,7 +104927,7 @@ async function muxVideoWithAudio(videoPath, audioPath, outputPath, signal, confi
104889
104927
  success: result.success,
104890
104928
  outputPath,
104891
104929
  durationMs: result.durationMs,
104892
- error: !result.success ? result.exitCode !== null ? `FFmpeg exited with code ${result.exitCode}` : `[FFmpeg] ${result.stderr}` : void 0
104930
+ error: !result.success ? formatFfmpegError(result.exitCode, result.stderr) : void 0
104893
104931
  };
104894
104932
  }
104895
104933
  async function applyFaststart(inputPath, outputPath, signal, config2) {
@@ -104912,7 +104950,7 @@ async function applyFaststart(inputPath, outputPath, signal, config2) {
104912
104950
  success: result.success,
104913
104951
  outputPath,
104914
104952
  durationMs: result.durationMs,
104915
- error: !result.success ? result.exitCode !== null ? `FFmpeg exited with code ${result.exitCode}` : `[FFmpeg] ${result.stderr}` : void 0
104953
+ error: !result.success ? formatFfmpegError(result.exitCode, result.stderr) : void 0
104916
104954
  };
104917
104955
  }
104918
104956
 
@@ -105005,7 +105043,7 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
105005
105043
  args.push("-c:v", encoderName);
105006
105044
  switch (gpuEncoder) {
105007
105045
  case "nvenc":
105008
- args.push("-preset", preset);
105046
+ args.push("-preset", mapPresetForGpuEncoder("nvenc", preset));
105009
105047
  if (bitrate) args.push("-b:v", bitrate);
105010
105048
  else args.push("-cq", String(quality));
105011
105049
  break;
@@ -105024,7 +105062,7 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
105024
105062
  else args.push("-qp", String(quality));
105025
105063
  break;
105026
105064
  case "qsv":
105027
- args.push("-preset", preset);
105065
+ args.push("-preset", mapPresetForGpuEncoder("qsv", preset));
105028
105066
  if (bitrate) args.push("-b:v", bitrate);
105029
105067
  else args.push("-global_quality", String(quality));
105030
105068
  break;
@@ -105180,7 +105218,7 @@ Process error: ${err.message}`;
105180
105218
  success: false,
105181
105219
  durationMs,
105182
105220
  fileSize: 0,
105183
- error: `FFmpeg exited with code ${exitCode}`
105221
+ error: formatFfmpegError(exitCode, stderr)
105184
105222
  };
105185
105223
  }
105186
105224
  const fileSize = existsSync6(outputPath) ? statSync4(outputPath).size : 0;
@@ -105194,7 +105232,7 @@ Process error: ${err.message}`;
105194
105232
  // ../engine/src/services/videoFrameExtractor.ts
105195
105233
  import { spawn as spawn8 } from "child_process";
105196
105234
  import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync4, rmSync } from "fs";
105197
- import { join as join8 } from "path";
105235
+ import { isAbsolute as isAbsolute2, join as join8 } from "path";
105198
105236
 
105199
105237
  // ../engine/src/utils/ffprobe.ts
105200
105238
  import { spawn as spawn7 } from "child_process";
@@ -105309,7 +105347,7 @@ function parseFrameRate(frameRateStr) {
105309
105347
  }
105310
105348
  return parseFloat(frameRateStr) || 0;
105311
105349
  }
105312
- async function extractVideoMetadata(filePath) {
105350
+ async function extractMediaMetadata(filePath) {
105313
105351
  const cached = videoMetadataCache.get(filePath);
105314
105352
  if (cached) return cached;
105315
105353
  const probePromise = (async () => {
@@ -105595,7 +105633,7 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
105595
105633
  const { fps, outputDir, quality = 95, format: format3 = "jpg" } = options;
105596
105634
  const videoOutputDir = join8(outputDir, videoId);
105597
105635
  if (!existsSync8(videoOutputDir)) mkdirSync5(videoOutputDir, { recursive: true });
105598
- const metadata = await extractVideoMetadata(videoPath);
105636
+ const metadata = await extractMediaMetadata(videoPath);
105599
105637
  const framePattern = `frame_%05d.${format3}`;
105600
105638
  const outputPattern = join8(videoOutputDir, framePattern);
105601
105639
  const isHdr = isHdrColorSpace(metadata.colorSpace);
@@ -105744,7 +105782,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config2,
105744
105782
  if (signal?.aborted) break;
105745
105783
  try {
105746
105784
  let videoPath = video.src;
105747
- if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
105785
+ if (!isAbsolute2(videoPath) && !isHttpUrl(videoPath)) {
105748
105786
  const fromCompiled = compiledDir ? join8(compiledDir, videoPath) : null;
105749
105787
  videoPath = fromCompiled && existsSync8(fromCompiled) ? fromCompiled : join8(baseDir, videoPath);
105750
105788
  }
@@ -105764,7 +105802,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config2,
105764
105802
  }
105765
105803
  const videoColorSpaces = await Promise.all(
105766
105804
  resolvedVideos.map(async ({ videoPath }) => {
105767
- const metadata = await extractVideoMetadata(videoPath);
105805
+ const metadata = await extractMediaMetadata(videoPath);
105768
105806
  return metadata.colorSpace;
105769
105807
  })
105770
105808
  );
@@ -105797,7 +105835,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config2,
105797
105835
  if (signal?.aborted) break;
105798
105836
  const entry = resolvedVideos[i];
105799
105837
  if (!entry) continue;
105800
- const metadata = await extractVideoMetadata(entry.videoPath);
105838
+ const metadata = await extractMediaMetadata(entry.videoPath);
105801
105839
  if (!metadata.isVFR) continue;
105802
105840
  let segDuration = entry.video.end - entry.video.start;
105803
105841
  if (!Number.isFinite(segDuration) || segDuration <= 0) {
@@ -105833,7 +105871,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config2,
105833
105871
  try {
105834
105872
  let videoDuration = video.end - video.start;
105835
105873
  if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
105836
- const metadata = await extractVideoMetadata(videoPath);
105874
+ const metadata = await extractMediaMetadata(videoPath);
105837
105875
  const sourceDuration = metadata.durationSeconds - video.mediaStart;
105838
105876
  videoDuration = sourceDuration > 0 ? sourceDuration : metadata.durationSeconds;
105839
105877
  video.end = video.start + videoDuration;
@@ -106208,7 +106246,7 @@ async function queryElementStacking(page, nativeHdrIds) {
106208
106246
 
106209
106247
  // ../engine/src/services/audioMixer.ts
106210
106248
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
106211
- import { join as join9, dirname as dirname7 } from "path";
106249
+ import { isAbsolute as isAbsolute3, join as join9, dirname as dirname7 } from "path";
106212
106250
  function parseAudioElements(html) {
106213
106251
  const elements = [];
106214
106252
  const { document: document2 } = parseHTML(html);
@@ -106435,7 +106473,7 @@ async function processCompositionAudio(elements, baseDir, workDir, outputPath, t
106435
106473
  }
106436
106474
  try {
106437
106475
  let srcPath = element.src;
106438
- if (!srcPath.startsWith("/") && !isHttpUrl(srcPath)) {
106476
+ if (!isAbsolute3(srcPath) && !isHttpUrl(srcPath)) {
106439
106477
  const fromCompiled = compiledDir ? join9(compiledDir, srcPath) : null;
106440
106478
  srcPath = fromCompiled && existsSync9(fromCompiled) ? fromCompiled : join9(baseDir, srcPath);
106441
106479
  }
@@ -107161,13 +107199,51 @@ function normalizeObjectFit(value) {
107161
107199
  }
107162
107200
  function parseTransformMatrix(css) {
107163
107201
  if (!css || css === "none") return null;
107164
- const match2 = css.match(
107202
+ const match2d = css.match(
107165
107203
  /^matrix\(\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,]+),\s*([^,)]+)\s*\)$/
107166
107204
  );
107167
- if (!match2) return null;
107168
- const values = match2.slice(1, 7).map(Number);
107169
- if (!values.every(Number.isFinite)) return null;
107170
- return values;
107205
+ if (match2d) {
107206
+ const values = match2d.slice(1, 7).map(Number);
107207
+ if (!values.every(Number.isFinite)) return null;
107208
+ return values;
107209
+ }
107210
+ const match3d = css.match(/^matrix3d\(\s*([^)]+)\)$/);
107211
+ if (match3d) {
107212
+ const raw2 = match3d[1];
107213
+ if (!raw2) return null;
107214
+ const parts = raw2.split(",").map((s) => Number(s.trim()));
107215
+ if (parts.length !== 16 || !parts.every(Number.isFinite)) return null;
107216
+ warnIfZSignificant(parts);
107217
+ return [
107218
+ parts[0],
107219
+ parts[1],
107220
+ parts[4],
107221
+ parts[5],
107222
+ parts[12],
107223
+ parts[13]
107224
+ ];
107225
+ }
107226
+ return null;
107227
+ }
107228
+ var warnedZSignificant = false;
107229
+ var Z_EPSILON = 1e-6;
107230
+ function warnIfZSignificant(parts) {
107231
+ if (warnedZSignificant) return;
107232
+ const a3 = parts[8] ?? 0;
107233
+ const b3 = parts[9] ?? 0;
107234
+ const c1 = parts[2] ?? 0;
107235
+ const c2 = parts[6] ?? 0;
107236
+ const c3 = parts[10] ?? 1;
107237
+ const d1 = parts[3] ?? 0;
107238
+ const d2 = parts[7] ?? 0;
107239
+ const d3 = parts[11] ?? 0;
107240
+ const d4 = parts[15] ?? 1;
107241
+ if (Math.abs(a3) > Z_EPSILON || Math.abs(b3) > Z_EPSILON || Math.abs(c1) > Z_EPSILON || Math.abs(c2) > Z_EPSILON || Math.abs(c3 - 1) > Z_EPSILON || Math.abs(d1) > Z_EPSILON || Math.abs(d2) > Z_EPSILON || Math.abs(d3) > Z_EPSILON || Math.abs(d4 - 1) > Z_EPSILON) {
107242
+ warnedZSignificant = true;
107243
+ console.warn(
107244
+ `[alphaBlit] parseTransformMatrix received a matrix3d with non-trivial 3D components (a3=${a3}, b3=${b3}, c1=${c1}, c2=${c2}, c3=${c3}, d1=${d1}, d2=${d2}, d3=${d3}, d4=${d4}). The engine projects 3D transforms to 2D (m11, m12, m21, m22, m41, m42) and silently discards perspective and out-of-plane rotation. If your composition uses real 3D (rotateX/Y, perspective), the rendered output will not match the studio preview. Z translation (translateZ) is dropped by design and does not trigger this warning. This warning is emitted once per process.`
107245
+ );
107246
+ }
107171
107247
  }
107172
107248
 
107173
107249
  // ../engine/src/utils/layerCompositor.ts
@@ -108371,14 +108447,14 @@ import { join as join14, dirname as dirname9, resolve as resolve10 } from "path"
108371
108447
  import postcss from "postcss";
108372
108448
 
108373
108449
  // src/utils/paths.ts
108374
- import { resolve as resolve9, basename as basename2, join as join12, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
108450
+ import { resolve as resolve9, basename as basename2, join as join12, relative as relative2, isAbsolute as isAbsolute4 } from "node:path";
108375
108451
  var DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? resolve9(new URL(import.meta.url).pathname, "../../..", "renders");
108376
108452
  function isPathInside2(childPath, parentPath) {
108377
108453
  const absChild = resolve9(childPath);
108378
108454
  const absParent = resolve9(parentPath);
108379
108455
  if (absChild === absParent) return true;
108380
108456
  const rel = relative2(absParent, absChild);
108381
- return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
108457
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute4(rel);
108382
108458
  }
108383
108459
  function toExternalAssetKey(absPath) {
108384
108460
  if (absPath.startsWith("hf-ext/")) return absPath;
@@ -108841,7 +108917,7 @@ async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagNa
108841
108917
  if (!existsSync14(filePath)) {
108842
108918
  return { duration: 0, resolvedPath: filePath };
108843
108919
  }
108844
- const metadata = tagName19 === "video" ? await extractVideoMetadata(filePath) : await extractAudioMetadata(filePath);
108920
+ const metadata = tagName19 === "video" ? await extractMediaMetadata(filePath) : await extractAudioMetadata(filePath);
108845
108921
  const fileDuration = metadata.durationSeconds;
108846
108922
  const effectiveDuration = fileDuration - mediaStart;
108847
108923
  const duration = effectiveDuration > 0 ? effectiveDuration : fileDuration;
@@ -109424,7 +109500,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
109424
109500
  if (isHttpUrl(video.src)) continue;
109425
109501
  const videoPath = resolve10(projectDir, video.src);
109426
109502
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
109427
- Promise.all([analyzeKeyframeIntervals(videoPath), extractVideoMetadata(videoPath)]).then(([analysis, metadata]) => {
109503
+ Promise.all([analyzeKeyframeIntervals(videoPath), extractMediaMetadata(videoPath)]).then(([analysis, metadata]) => {
109428
109504
  if (analysis.isProblematic) {
109429
109505
  console.warn(
109430
109506
  `[Compiler] WARNING: Video "${video.id}" has sparse keyframes (max interval: ${analysis.maxIntervalSeconds}s). This causes seek failures and frame freezing. Re-encode with: ${reencode}`
@@ -109623,6 +109699,21 @@ function getMaxFrameIndex(frameDir) {
109623
109699
  frameDirMaxIndexCache.set(frameDir, max);
109624
109700
  return max;
109625
109701
  }
109702
+ function countNonZeroAlpha(rgba) {
109703
+ let n = 0;
109704
+ for (let p = 3; p < rgba.length; p += 4) {
109705
+ if (rgba[p] !== 0) n++;
109706
+ }
109707
+ return n;
109708
+ }
109709
+ function countNonZeroRgb48(buf) {
109710
+ let n = 0;
109711
+ for (let p = 0; p < buf.length; p += 6) {
109712
+ if (buf[p] !== 0 || buf[p + 1] !== 0 || buf[p + 2] !== 0 || buf[p + 3] !== 0 || buf[p + 4] !== 0 || buf[p + 5] !== 0)
109713
+ n++;
109714
+ }
109715
+ return n;
109716
+ }
109626
109717
  var RenderCancelledError = class extends Error {
109627
109718
  reason;
109628
109719
  constructor(message = "render_cancelled", reason = "aborted") {
@@ -109830,6 +109921,165 @@ function blitHdrImageLayer(canvas, el, hdrImageBuffers, width, height, log, sour
109830
109921
  }
109831
109922
  }
109832
109923
  }
109924
+ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter, debugFrameIndex = -1) {
109925
+ const {
109926
+ log,
109927
+ domSession,
109928
+ beforeCaptureHook,
109929
+ width,
109930
+ height,
109931
+ fps,
109932
+ effectiveHdr,
109933
+ nativeHdrImageIds,
109934
+ hdrImageBuffers,
109935
+ hdrFrameDirs,
109936
+ hdrVideoStartTimes,
109937
+ imageTransfers,
109938
+ videoTransfers,
109939
+ debugDumpEnabled,
109940
+ debugDumpDir
109941
+ } = ctx;
109942
+ const filteredStacking = elementFilter ? fullStacking.filter((e) => elementFilter.has(e.id)) : fullStacking;
109943
+ const layers = groupIntoLayers(filteredStacking);
109944
+ const shouldLog = debugDumpEnabled && debugFrameIndex >= 0;
109945
+ if (shouldLog) {
109946
+ log.info("[diag] compositeToBuffer plan", {
109947
+ frame: debugFrameIndex,
109948
+ time: time.toFixed(3),
109949
+ filterSize: elementFilter?.size,
109950
+ fullStackingCount: fullStacking.length,
109951
+ filteredCount: filteredStacking.length,
109952
+ layerCount: layers.length,
109953
+ layers: layers.map(
109954
+ (l) => l.type === "hdr" ? {
109955
+ type: "hdr",
109956
+ id: l.element.id,
109957
+ z: l.element.zIndex,
109958
+ visible: l.element.visible,
109959
+ opacity: l.element.opacity,
109960
+ bounds: `${Math.round(l.element.x)},${Math.round(l.element.y)} ${Math.round(l.element.width)}x${Math.round(l.element.height)}`
109961
+ } : { type: "dom", ids: l.elementIds }
109962
+ )
109963
+ });
109964
+ }
109965
+ for (const [layerIdx, layer] of layers.entries()) {
109966
+ if (layer.type === "hdr") {
109967
+ const before2 = shouldLog ? countNonZeroRgb48(canvas) : 0;
109968
+ const isHdrImage = nativeHdrImageIds.has(layer.element.id);
109969
+ if (isHdrImage) {
109970
+ blitHdrImageLayer(
109971
+ canvas,
109972
+ layer.element,
109973
+ hdrImageBuffers,
109974
+ width,
109975
+ height,
109976
+ log,
109977
+ imageTransfers.get(layer.element.id),
109978
+ effectiveHdr.transfer
109979
+ );
109980
+ } else {
109981
+ blitHdrVideoLayer(
109982
+ canvas,
109983
+ layer.element,
109984
+ time,
109985
+ fps,
109986
+ hdrFrameDirs,
109987
+ hdrVideoStartTimes,
109988
+ width,
109989
+ height,
109990
+ log,
109991
+ videoTransfers.get(layer.element.id),
109992
+ effectiveHdr.transfer
109993
+ );
109994
+ }
109995
+ if (shouldLog) {
109996
+ const after2 = countNonZeroRgb48(canvas);
109997
+ if (isHdrImage) {
109998
+ const buf = hdrImageBuffers.get(layer.element.id);
109999
+ log.info("[diag] hdr layer blit", {
110000
+ frame: debugFrameIndex,
110001
+ layerIdx,
110002
+ id: layer.element.id,
110003
+ kind: "image",
110004
+ pixelsAdded: after2 - before2,
110005
+ totalNonZero: after2,
110006
+ bufferDecoded: !!buf,
110007
+ bufferDims: buf ? `${buf.width}x${buf.height}` : null
110008
+ });
110009
+ } else {
110010
+ const frameDir = hdrFrameDirs.get(layer.element.id);
110011
+ const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
110012
+ const localTime = time - startTime;
110013
+ const frameNum = Math.floor(localTime * fps) + 1;
110014
+ const expectedFrame = frameDir ? join15(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
110015
+ log.info("[diag] hdr layer blit", {
110016
+ frame: debugFrameIndex,
110017
+ layerIdx,
110018
+ id: layer.element.id,
110019
+ kind: "video",
110020
+ pixelsAdded: after2 - before2,
110021
+ totalNonZero: after2,
110022
+ startTime,
110023
+ localTime: localTime.toFixed(3),
110024
+ hdrFrameNum: frameNum,
110025
+ expectedFrame,
110026
+ expectedFrameExists: expectedFrame ? existsSync15(expectedFrame) : false
110027
+ });
110028
+ }
110029
+ }
110030
+ } else {
110031
+ const allElementIds = fullStacking.map((e) => e.id);
110032
+ const layerIds = new Set(layer.elementIds);
110033
+ const hideIds = allElementIds.filter((id) => !layerIds.has(id));
110034
+ await domSession.page.evaluate((t) => {
110035
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
110036
+ }, time);
110037
+ if (beforeCaptureHook) {
110038
+ await beforeCaptureHook(domSession.page, time);
110039
+ }
110040
+ await applyDomLayerMask(domSession.page, layer.elementIds, hideIds);
110041
+ const domPng = await captureAlphaPng(domSession.page, width, height);
110042
+ await removeDomLayerMask(domSession.page, hideIds);
110043
+ try {
110044
+ const { data: domRgba } = decodePng(domPng);
110045
+ const before2 = shouldLog ? countNonZeroRgb48(canvas) : 0;
110046
+ const alphaPixels = shouldLog ? countNonZeroAlpha(domRgba) : 0;
110047
+ blitRgba8OverRgb48le(domRgba, canvas, width, height, effectiveHdr.transfer);
110048
+ if (shouldLog && debugDumpDir) {
110049
+ const after2 = countNonZeroRgb48(canvas);
110050
+ const dumpName = `frame_${String(debugFrameIndex).padStart(4, "0")}_layer_${String(layerIdx).padStart(2, "0")}_dom.png`;
110051
+ const dumpPath = join15(debugDumpDir, dumpName);
110052
+ writeFileSync4(dumpPath, domPng);
110053
+ log.info("[diag] dom layer blit", {
110054
+ frame: debugFrameIndex,
110055
+ layerIdx,
110056
+ layerIds: layer.elementIds,
110057
+ hideCount: hideIds.length,
110058
+ pngBytes: domPng.length,
110059
+ alphaPixels,
110060
+ pixelsAdded: after2 - before2,
110061
+ totalNonZero: after2,
110062
+ dumpPath
110063
+ });
110064
+ }
110065
+ } catch (err) {
110066
+ log.warn("DOM layer decode/blit failed; skipping overlay", {
110067
+ layerIds: layer.elementIds,
110068
+ error: err instanceof Error ? err.message : String(err)
110069
+ });
110070
+ }
110071
+ }
110072
+ }
110073
+ if (shouldLog && debugDumpDir) {
110074
+ const finalNonZero = countNonZeroRgb48(canvas);
110075
+ log.info("[diag] compositeToBuffer end", {
110076
+ frame: debugFrameIndex,
110077
+ finalNonZeroPixels: finalNonZero,
110078
+ totalPixels: width * height,
110079
+ coverage: (finalNonZero / (width * height) * 100).toFixed(1) + "%"
110080
+ });
110081
+ }
110082
+ }
109833
110083
  function createRenderJob(config2) {
109834
110084
  return {
109835
110085
  id: randomUUID(),
@@ -110174,7 +110424,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110174
110424
  videoPath = fromCompiled;
110175
110425
  }
110176
110426
  if (!existsSync15(videoPath)) return;
110177
- const meta = await extractVideoMetadata(videoPath);
110427
+ const meta = await extractMediaMetadata(videoPath);
110178
110428
  if (isHdrColorSpace(meta.colorSpace)) {
110179
110429
  nativeHdrVideoIds.add(v.id);
110180
110430
  videoTransfers.set(v.id, detectTransfer(meta.colorSpace));
@@ -110195,7 +110445,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110195
110445
  imgPath = fromCompiled;
110196
110446
  }
110197
110447
  if (!existsSync15(imgPath)) return null;
110198
- const meta = await extractVideoMetadata(imgPath);
110448
+ const meta = await extractMediaMetadata(imgPath);
110199
110449
  if (isHdrColorSpace(meta.colorSpace)) {
110200
110450
  nativeHdrImageIds.add(img.id);
110201
110451
  imageTransfers.set(img.id, detectTransfer(meta.colorSpace));
@@ -110307,6 +110557,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110307
110557
  format: needsAlpha ? "png" : "jpeg",
110308
110558
  quality: needsAlpha ? void 0 : job.config.quality === "draft" ? 80 : 95
110309
110559
  };
110560
+ const buildHdrCaptureOptions = () => ({
110561
+ ...captureOptions,
110562
+ skipReadinessVideoIds: Array.from(nativeHdrVideoIds)
110563
+ });
110310
110564
  const workerCount = calculateOptimalWorkers(totalFrames, job.config.workers, cfg);
110311
110565
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
110312
110566
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
@@ -110341,7 +110595,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110341
110595
  const domSession = await createCaptureSession(
110342
110596
  fileServer.url,
110343
110597
  framesDir,
110344
- { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
110598
+ buildHdrCaptureOptions(),
110345
110599
  createVideoFrameInjector(frameLookup),
110346
110600
  cfg
110347
110601
  );
@@ -110437,6 +110691,29 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110437
110691
  }
110438
110692
  }
110439
110693
  }
110694
+ for (const [imageId, startTime] of hdrImageStartTimes) {
110695
+ if (hdrExtractionDims.has(imageId)) continue;
110696
+ const img = composition.images.find((i) => i.id === imageId);
110697
+ if (!img) continue;
110698
+ const duration = img.end - img.start;
110699
+ const retryTime = startTime + Math.min(0.5, duration * 0.1);
110700
+ await domSession.page.evaluate((t) => {
110701
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
110702
+ }, retryTime);
110703
+ if (domSession.onBeforeCapture) {
110704
+ await domSession.onBeforeCapture(domSession.page, retryTime);
110705
+ }
110706
+ const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
110707
+ for (const el of retryStacking) {
110708
+ if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
110709
+ hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
110710
+ if (!hdrImageFitInfo.has(el.id)) {
110711
+ hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
110712
+ }
110713
+ break;
110714
+ }
110715
+ }
110716
+ }
110440
110717
  for (const [videoId, srcPath] of hdrVideoSrcPaths) {
110441
110718
  const video = composition.videos.find((v) => v.id === videoId);
110442
110719
  if (!video) continue;
@@ -110519,21 +110796,6 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110519
110796
  }
110520
110797
  assertNotAborted();
110521
110798
  try {
110522
- let countNonZeroAlpha2 = function(rgba) {
110523
- let n = 0;
110524
- for (let p = 3; p < rgba.length; p += 4) {
110525
- if (rgba[p] !== 0) n++;
110526
- }
110527
- return n;
110528
- }, countNonZeroRgb482 = function(buf) {
110529
- let n = 0;
110530
- for (let p = 0; p < buf.length; p += 6) {
110531
- if (buf[p] !== 0 || buf[p + 1] !== 0 || buf[p + 2] !== 0 || buf[p + 3] !== 0 || buf[p + 4] !== 0 || buf[p + 5] !== 0)
110532
- n++;
110533
- }
110534
- return n;
110535
- };
110536
- var countNonZeroAlpha = countNonZeroAlpha2, countNonZeroRgb48 = countNonZeroRgb482;
110537
110799
  const beforeCaptureHook = domSession.onBeforeCapture;
110538
110800
  const cleanedUpVideos = /* @__PURE__ */ new Set();
110539
110801
  const hdrVideoEndTimes = /* @__PURE__ */ new Map();
@@ -110547,153 +110809,28 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110547
110809
  if (debugDumpDir && !existsSync15(debugDumpDir)) {
110548
110810
  mkdirSync10(debugDumpDir, { recursive: true });
110549
110811
  }
110550
- async function compositeToBuffer(canvas, time, fullStacking, elementFilter, debugFrameIndex = -1) {
110551
- const filteredStacking = elementFilter ? fullStacking.filter((e) => elementFilter.has(e.id)) : fullStacking;
110552
- const layers = groupIntoLayers(filteredStacking);
110553
- const shouldLog = debugDumpEnabled && debugFrameIndex >= 0;
110554
- if (shouldLog) {
110555
- log.info("[diag] compositeToBuffer plan", {
110556
- frame: debugFrameIndex,
110557
- time: time.toFixed(3),
110558
- filterSize: elementFilter?.size,
110559
- fullStackingCount: fullStacking.length,
110560
- filteredCount: filteredStacking.length,
110561
- layerCount: layers.length,
110562
- layers: layers.map(
110563
- (l) => l.type === "hdr" ? {
110564
- type: "hdr",
110565
- id: l.element.id,
110566
- z: l.element.zIndex,
110567
- visible: l.element.visible,
110568
- opacity: l.element.opacity,
110569
- bounds: `${Math.round(l.element.x)},${Math.round(l.element.y)} ${Math.round(l.element.width)}x${Math.round(l.element.height)}`
110570
- } : { type: "dom", ids: l.elementIds }
110571
- )
110572
- });
110573
- }
110574
- for (const [layerIdx, layer] of layers.entries()) {
110575
- if (layer.type === "hdr") {
110576
- const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
110577
- const isHdrImage = nativeHdrImageIds.has(layer.element.id);
110578
- if (isHdrImage) {
110579
- blitHdrImageLayer(
110580
- canvas,
110581
- layer.element,
110582
- hdrImageBuffers,
110583
- width,
110584
- height,
110585
- log,
110586
- imageTransfers.get(layer.element.id),
110587
- effectiveHdr?.transfer
110588
- );
110589
- } else {
110590
- blitHdrVideoLayer(
110591
- canvas,
110592
- layer.element,
110593
- time,
110594
- job.config.fps,
110595
- hdrFrameDirs,
110596
- hdrVideoStartTimes,
110597
- width,
110598
- height,
110599
- log,
110600
- videoTransfers.get(layer.element.id),
110601
- effectiveHdr?.transfer
110602
- );
110603
- }
110604
- if (shouldLog) {
110605
- const after2 = countNonZeroRgb482(canvas);
110606
- if (isHdrImage) {
110607
- const buf = hdrImageBuffers.get(layer.element.id);
110608
- log.info("[diag] hdr layer blit", {
110609
- frame: debugFrameIndex,
110610
- layerIdx,
110611
- id: layer.element.id,
110612
- kind: "image",
110613
- pixelsAdded: after2 - before2,
110614
- totalNonZero: after2,
110615
- bufferDecoded: !!buf,
110616
- bufferDims: buf ? `${buf.width}x${buf.height}` : null
110617
- });
110618
- } else {
110619
- const frameDir = hdrFrameDirs.get(layer.element.id);
110620
- const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
110621
- const localTime = time - startTime;
110622
- const frameNum = Math.floor(localTime * job.config.fps) + 1;
110623
- const expectedFrame = frameDir ? join15(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
110624
- log.info("[diag] hdr layer blit", {
110625
- frame: debugFrameIndex,
110626
- layerIdx,
110627
- id: layer.element.id,
110628
- kind: "video",
110629
- pixelsAdded: after2 - before2,
110630
- totalNonZero: after2,
110631
- startTime,
110632
- localTime: localTime.toFixed(3),
110633
- hdrFrameNum: frameNum,
110634
- expectedFrame,
110635
- expectedFrameExists: expectedFrame ? existsSync15(expectedFrame) : false
110636
- });
110637
- }
110638
- }
110639
- } else {
110640
- const allElementIds = fullStacking.map((e) => e.id);
110641
- const layerIds = new Set(layer.elementIds);
110642
- const hideIds = allElementIds.filter((id) => !layerIds.has(id));
110643
- await domSession.page.evaluate((t) => {
110644
- if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
110645
- }, time);
110646
- if (beforeCaptureHook) {
110647
- await beforeCaptureHook(domSession.page, time);
110648
- }
110649
- await applyDomLayerMask(domSession.page, layer.elementIds, hideIds);
110650
- const domPng = await captureAlphaPng(domSession.page, width, height);
110651
- await removeDomLayerMask(domSession.page, hideIds);
110652
- try {
110653
- const { data: domRgba } = decodePng(domPng);
110654
- if (!effectiveHdr) {
110655
- throw new Error(
110656
- "Invariant violation: effectiveHdr is undefined inside HDR layer branch"
110657
- );
110658
- }
110659
- const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
110660
- const alphaPixels = shouldLog ? countNonZeroAlpha2(domRgba) : 0;
110661
- blitRgba8OverRgb48le(domRgba, canvas, width, height, effectiveHdr.transfer);
110662
- if (shouldLog && debugDumpDir) {
110663
- const after2 = countNonZeroRgb482(canvas);
110664
- const dumpName = `frame_${String(debugFrameIndex).padStart(4, "0")}_layer_${String(layerIdx).padStart(2, "0")}_dom.png`;
110665
- const dumpPath = join15(debugDumpDir, dumpName);
110666
- writeFileSync4(dumpPath, domPng);
110667
- log.info("[diag] dom layer blit", {
110668
- frame: debugFrameIndex,
110669
- layerIdx,
110670
- layerIds: layer.elementIds,
110671
- hideCount: hideIds.length,
110672
- pngBytes: domPng.length,
110673
- alphaPixels,
110674
- pixelsAdded: after2 - before2,
110675
- totalNonZero: after2,
110676
- dumpPath
110677
- });
110678
- }
110679
- } catch (err) {
110680
- log.warn("DOM layer decode/blit failed; skipping overlay", {
110681
- layerIds: layer.elementIds,
110682
- error: err instanceof Error ? err.message : String(err)
110683
- });
110684
- }
110685
- }
110686
- }
110687
- if (shouldLog && debugDumpDir) {
110688
- const finalNonZero = countNonZeroRgb482(canvas);
110689
- log.info("[diag] compositeToBuffer end", {
110690
- frame: debugFrameIndex,
110691
- finalNonZeroPixels: finalNonZero,
110692
- totalPixels: width * height,
110693
- coverage: (finalNonZero / (width * height) * 100).toFixed(1) + "%"
110694
- });
110695
- }
110812
+ if (!effectiveHdr) {
110813
+ throw new Error(
110814
+ "Internal: HDR render path entered without effectiveHdr \u2014 this is a bug."
110815
+ );
110696
110816
  }
110817
+ const hdrCompositeCtx = {
110818
+ log,
110819
+ domSession,
110820
+ beforeCaptureHook,
110821
+ width,
110822
+ height,
110823
+ fps: job.config.fps,
110824
+ effectiveHdr,
110825
+ nativeHdrImageIds,
110826
+ hdrImageBuffers,
110827
+ hdrFrameDirs,
110828
+ hdrVideoStartTimes,
110829
+ imageTransfers,
110830
+ videoTransfers,
110831
+ debugDumpEnabled,
110832
+ debugDumpDir
110833
+ };
110697
110834
  const bufSize = width * height * 6;
110698
110835
  const hasTransitions = transitionRanges.length > 0;
110699
110836
  const transBufferA = hasTransitions ? Buffer.alloc(bufSize) : null;
@@ -110801,7 +110938,14 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110801
110938
  hdrEncoder.writeFrame(transOutput);
110802
110939
  } else {
110803
110940
  normalCanvas.fill(0);
110804
- await compositeToBuffer(normalCanvas, time, stackingInfo, void 0, i);
110941
+ await compositeHdrFrame(
110942
+ hdrCompositeCtx,
110943
+ normalCanvas,
110944
+ time,
110945
+ stackingInfo,
110946
+ void 0,
110947
+ i
110948
+ );
110805
110949
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
110806
110950
  const previewPath = join15(
110807
110951
  debugDumpDir,
@@ -110920,7 +111064,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110920
111064
  fileServer.url,
110921
111065
  workDir,
110922
111066
  tasks,
110923
- { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
111067
+ buildHdrCaptureOptions(),
110924
111068
  () => createVideoFrameInjector(frameLookup),
110925
111069
  abortSignal,
110926
111070
  (progress) => {
@@ -110950,7 +111094,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110950
111094
  const session = probeSession ?? await createCaptureSession(
110951
111095
  fileServer.url,
110952
111096
  framesDir,
110953
- { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
111097
+ buildHdrCaptureOptions(),
110954
111098
  videoInjector,
110955
111099
  cfg
110956
111100
  );
@@ -111002,7 +111146,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111002
111146
  fileServer.url,
111003
111147
  workDir,
111004
111148
  tasks,
111005
- { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
111149
+ buildHdrCaptureOptions(),
111006
111150
  () => createVideoFrameInjector(frameLookup),
111007
111151
  abortSignal,
111008
111152
  (progress) => {
@@ -111033,7 +111177,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
111033
111177
  const session = probeSession ?? await createCaptureSession(
111034
111178
  fileServer.url,
111035
111179
  framesDir,
111036
- { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
111180
+ buildHdrCaptureOptions(),
111037
111181
  videoInjector,
111038
111182
  cfg
111039
111183
  );