@hyperframes/producer 0.4.10 → 0.4.11

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/index.js CHANGED
@@ -78348,16 +78348,16 @@ var require_buffer_crc32 = __commonJS({
78348
78348
  }
78349
78349
  return crc ^ -1;
78350
78350
  }
78351
- function crc32() {
78351
+ function crc322() {
78352
78352
  return bufferizeInt(_crc32.apply(null, arguments));
78353
78353
  }
78354
- crc32.signed = function() {
78354
+ crc322.signed = function() {
78355
78355
  return _crc32.apply(null, arguments);
78356
78356
  };
78357
- crc32.unsigned = function() {
78357
+ crc322.unsigned = function() {
78358
78358
  return _crc32.apply(null, arguments) >>> 0;
78359
78359
  };
78360
- module.exports = crc32;
78360
+ module.exports = crc322;
78361
78361
  }
78362
78362
  });
78363
78363
 
@@ -78367,7 +78367,7 @@ var require_yauzl = __commonJS({
78367
78367
  var fs8 = __require("fs");
78368
78368
  var zlib = __require("zlib");
78369
78369
  var fd_slicer = require_fd_slicer();
78370
- var crc32 = require_buffer_crc32();
78370
+ var crc322 = require_buffer_crc32();
78371
78371
  var util = __require("util");
78372
78372
  var EventEmitter4 = __require("events").EventEmitter;
78373
78373
  var Transform = __require("stream").Transform;
@@ -78653,7 +78653,7 @@ var require_yauzl = __commonJS({
78653
78653
  continue;
78654
78654
  }
78655
78655
  var oldNameCrc32 = extraField.data.readUInt32LE(1);
78656
- if (crc32.unsigned(buffer.slice(0, entry.fileNameLength)) !== oldNameCrc32) {
78656
+ if (crc322.unsigned(buffer.slice(0, entry.fileNameLength)) !== oldNameCrc32) {
78657
78657
  continue;
78658
78658
  }
78659
78659
  entry.fileName = decodeBuffer(extraField.data, 5, extraField.data.length, true);
@@ -89420,7 +89420,7 @@ import {
89420
89420
  existsSync as existsSync15,
89421
89421
  mkdirSync as mkdirSync10,
89422
89422
  rmSync as rmSync3,
89423
- readFileSync as readFileSync9,
89423
+ readFileSync as readFileSync10,
89424
89424
  readdirSync as readdirSync6,
89425
89425
  writeFileSync as writeFileSync4,
89426
89426
  copyFileSync as copyFileSync2,
@@ -99641,9 +99641,25 @@ var mediaRules = [
99641
99641
  // video_nested_in_timed_element
99642
99642
  ({ source: source2, tags }) => {
99643
99643
  const findings = [];
99644
+ const voidElements3 = /* @__PURE__ */ new Set([
99645
+ "area",
99646
+ "base",
99647
+ "br",
99648
+ "col",
99649
+ "embed",
99650
+ "hr",
99651
+ "img",
99652
+ "input",
99653
+ "link",
99654
+ "meta",
99655
+ "source",
99656
+ "track",
99657
+ "wbr"
99658
+ ]);
99644
99659
  const timedTagPositions = [];
99645
99660
  for (const tag of tags) {
99646
99661
  if (tag.name === "video" || tag.name === "audio") continue;
99662
+ if (voidElements3.has(tag.name)) continue;
99647
99663
  if (readAttr(tag.raw, "data-composition-id")) continue;
99648
99664
  if (readAttr(tag.raw, "data-start")) {
99649
99665
  timedTagPositions.push({
@@ -99810,7 +99826,9 @@ function extractGsapWindows(script) {
99810
99826
  let index = 0;
99811
99827
  while ((match2 = methodPattern.exec(script)) !== null && index < parsed.animations.length) {
99812
99828
  const raw2 = match2[0];
99813
- const meta = parseGsapWindowMeta(match2[1] ?? "", match2[2] ?? "");
99829
+ const args = match2[2] ?? "";
99830
+ if (!/^\s*["']/.test(args)) continue;
99831
+ const meta = parseGsapWindowMeta(match2[1] ?? "", args);
99814
99832
  const animation = parsed.animations[index];
99815
99833
  index += 1;
99816
99834
  if (!animation) continue;
@@ -101104,6 +101122,12 @@ async function createCaptureSession(serverUrl, outputDir, options, onBeforeCaptu
101104
101122
  );
101105
101123
  const { browser, captureMode } = await acquireBrowser(chromeArgs, config2);
101106
101124
  const page = await browser.newPage();
101125
+ await page.evaluateOnNewDocument(() => {
101126
+ const w = window;
101127
+ if (typeof w.__name !== "function") {
101128
+ w.__name = (fn, _name) => fn;
101129
+ }
101130
+ });
101107
101131
  const browserVersion = await browser.version();
101108
101132
  const expectedMajor = config2?.expectedChromiumMajor;
101109
101133
  if (Number.isFinite(expectedMajor)) {
@@ -101208,9 +101232,10 @@ async function initializeSession(session) {
101208
101232
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
101209
101233
  );
101210
101234
  }
101235
+ const skipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
101211
101236
  const videosReady = await pollPageExpression(
101212
101237
  page,
101213
- `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`,
101238
+ `(() => { const skip = new Set(${skipIdsLiteral}); const vids = Array.from(document.querySelectorAll("video")).filter(v => !skip.has(v.id)); return vids.length === 0 || vids.every(v => v.readyState >= 1); })()`,
101214
101239
  pageReadyTimeout2
101215
101240
  );
101216
101241
  if (!videosReady) {
@@ -101270,10 +101295,11 @@ async function initializeSession(session) {
101270
101295
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout}ms. Page must expose window.__hf = { duration, seek }.`
101271
101296
  );
101272
101297
  }
101298
+ const beginframeSkipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
101273
101299
  const videoDeadline = Date.now() + (session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout);
101274
101300
  while (Date.now() < videoDeadline) {
101275
101301
  const videosReady = await page.evaluate(
101276
- `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`
101302
+ `(() => { const skip = new Set(${beginframeSkipIdsLiteral}); const vids = Array.from(document.querySelectorAll("video")).filter(v => !skip.has(v.id)); return vids.length === 0 || vids.every(v => v.readyState >= 1); })()`
101277
101303
  );
101278
101304
  if (videosReady) break;
101279
101305
  await new Promise((r) => setTimeout(r, 100));
@@ -102253,6 +102279,8 @@ import { join as join8 } from "path";
102253
102279
 
102254
102280
  // ../engine/src/utils/ffprobe.ts
102255
102281
  import { spawn as spawn7 } from "child_process";
102282
+ import { readFileSync as readFileSync5 } from "fs";
102283
+ import { extname as extname2 } from "path";
102256
102284
  function runFfprobe(args) {
102257
102285
  return new Promise((resolve13, reject) => {
102258
102286
  const proc = spawn7("ffprobe", args);
@@ -102291,6 +102319,67 @@ function parseProbeJson(stdout) {
102291
102319
  }
102292
102320
  var videoMetadataCache = /* @__PURE__ */ new Map();
102293
102321
  var audioMetadataCache = /* @__PURE__ */ new Map();
102322
+ function crc32(buf) {
102323
+ let crc = 4294967295;
102324
+ for (let i = 0; i < buf.length; i++) {
102325
+ crc ^= buf[i] ?? 0;
102326
+ for (let bit = 0; bit < 8; bit++) {
102327
+ const mask = -(crc & 1);
102328
+ crc = crc >>> 1 ^ 3988292384 & mask;
102329
+ }
102330
+ }
102331
+ return (crc ^ 4294967295) >>> 0;
102332
+ }
102333
+ function extractPngMetadataFromBuffer(buf) {
102334
+ if (buf.length < 8 || buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71 || buf[4] !== 13 || buf[5] !== 10 || buf[6] !== 26 || buf[7] !== 10) {
102335
+ return null;
102336
+ }
102337
+ let width = 0;
102338
+ let height = 0;
102339
+ let seenIdat = false;
102340
+ let pos = 8;
102341
+ while (pos + 12 <= buf.length) {
102342
+ const chunkLen = buf.readUInt32BE(pos);
102343
+ const chunkType = buf.toString("ascii", pos + 4, pos + 8);
102344
+ if (pos + 12 + chunkLen > buf.length) return null;
102345
+ const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
102346
+ const chunkCrc = buf.readUInt32BE(pos + 8 + chunkLen);
102347
+ const chunkBytes = Buffer.concat([Buffer.from(chunkType, "ascii"), chunkData]);
102348
+ if (crc32(chunkBytes) !== chunkCrc) return null;
102349
+ if (chunkType === "IHDR" && chunkLen >= 8) {
102350
+ width = buf.readUInt32BE(pos + 8);
102351
+ height = buf.readUInt32BE(pos + 12);
102352
+ }
102353
+ if (chunkType === "IDAT") {
102354
+ seenIdat = true;
102355
+ }
102356
+ if (chunkType === "cICP" && chunkLen === 4 && !seenIdat) {
102357
+ const primariesCode = chunkData[0] ?? 0;
102358
+ const transferCode = chunkData[1] ?? 0;
102359
+ const matrixCode = chunkData[2] ?? 0;
102360
+ return {
102361
+ width,
102362
+ height,
102363
+ colorSpace: {
102364
+ colorPrimaries: primariesCode === 9 ? "bt2020" : primariesCode === 1 ? "bt709" : `unknown-${primariesCode}`,
102365
+ colorTransfer: transferCode === 16 ? "smpte2084" : transferCode === 18 ? "arib-std-b67" : transferCode === 1 ? "bt709" : `unknown-${transferCode}`,
102366
+ colorSpace: matrixCode === 9 ? "bt2020nc" : matrixCode === 0 ? "gbr" : `unknown-${matrixCode}`
102367
+ }
102368
+ };
102369
+ }
102370
+ if (chunkType === "IEND") break;
102371
+ pos += 12 + chunkLen;
102372
+ }
102373
+ return width > 0 && height > 0 ? { width, height, colorSpace: null } : null;
102374
+ }
102375
+ function extractStillImageMetadata(filePath) {
102376
+ if (extname2(filePath).toLowerCase() !== ".png") return null;
102377
+ try {
102378
+ return extractPngMetadataFromBuffer(readFileSync5(filePath));
102379
+ } catch {
102380
+ return null;
102381
+ }
102382
+ }
102294
102383
  function parseFrameRate(frameRateStr) {
102295
102384
  if (!frameRateStr) return 0;
102296
102385
  const parts = frameRateStr.split("/");
@@ -102305,18 +102394,38 @@ async function extractVideoMetadata(filePath) {
102305
102394
  const cached = videoMetadataCache.get(filePath);
102306
102395
  if (cached) return cached;
102307
102396
  const probePromise = (async () => {
102308
- const stdout = await runFfprobe([
102309
- "-v",
102310
- "quiet",
102311
- "-print_format",
102312
- "json",
102313
- "-show_format",
102314
- "-show_streams",
102315
- filePath
102316
- ]);
102317
- const output2 = parseProbeJson(stdout);
102318
- const videoStream = output2.streams.find((s) => s.codec_type === "video");
102319
- if (!videoStream) throw new Error("[FFmpeg] No video stream found");
102397
+ const stillImageMeta = extractStillImageMetadata(filePath);
102398
+ let output2 = null;
102399
+ try {
102400
+ const stdout = await runFfprobe([
102401
+ "-v",
102402
+ "quiet",
102403
+ "-print_format",
102404
+ "json",
102405
+ "-show_format",
102406
+ "-show_streams",
102407
+ filePath
102408
+ ]);
102409
+ output2 = parseProbeJson(stdout);
102410
+ } catch (error) {
102411
+ if (!stillImageMeta) throw error;
102412
+ }
102413
+ const videoStream = output2?.streams.find((s) => s.codec_type === "video");
102414
+ if (!videoStream) {
102415
+ if (stillImageMeta) {
102416
+ return {
102417
+ durationSeconds: 0,
102418
+ width: stillImageMeta.width,
102419
+ height: stillImageMeta.height,
102420
+ fps: 0,
102421
+ videoCodec: "png",
102422
+ hasAudio: false,
102423
+ isVFR: false,
102424
+ colorSpace: stillImageMeta.colorSpace
102425
+ };
102426
+ }
102427
+ throw new Error("[FFmpeg] No video stream found");
102428
+ }
102320
102429
  const rFps = parseFrameRate(videoStream.r_frame_rate);
102321
102430
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
102322
102431
  const fps = avgFps || rFps;
@@ -102324,16 +102433,17 @@ async function extractVideoMetadata(filePath) {
102324
102433
  const colorTransfer = videoStream.color_transfer || "";
102325
102434
  const colorPrimaries = videoStream.color_primaries || "";
102326
102435
  const colorSpaceVal = videoStream.color_space || "";
102327
- const hasColorInfo = !!(colorTransfer || colorPrimaries || colorSpaceVal);
102436
+ const ffprobeColorSpace = colorTransfer || colorPrimaries || colorSpaceVal ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null;
102437
+ const colorSpace = ffprobeColorSpace ?? stillImageMeta?.colorSpace ?? null;
102328
102438
  return {
102329
- durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
102330
- width: videoStream.width || 0,
102331
- height: videoStream.height || 0,
102439
+ durationSeconds: output2?.format.duration ? parseFloat(output2.format.duration) : 0,
102440
+ width: videoStream.width || stillImageMeta?.width || 0,
102441
+ height: videoStream.height || stillImageMeta?.height || 0,
102332
102442
  fps,
102333
102443
  videoCodec: videoStream.codec_name || "unknown",
102334
- hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
102444
+ hasAudio: output2?.streams.some((s) => s.codec_type === "audio") ?? false,
102335
102445
  isVFR,
102336
- colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
102446
+ colorSpace
102337
102447
  };
102338
102448
  })();
102339
102449
  videoMetadataCache.set(filePath, probePromise);
@@ -102432,7 +102542,7 @@ async function analyzeKeyframeIntervalsUncached(filePath) {
102432
102542
  // ../engine/src/utils/urlDownloader.ts
102433
102543
  import { createWriteStream as createWriteStream2, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
102434
102544
  import { createHash } from "crypto";
102435
- import { join as join7, extname as extname2 } from "path";
102545
+ import { join as join7, extname as extname3 } from "path";
102436
102546
  import { Readable } from "stream";
102437
102547
  import { finished } from "stream/promises";
102438
102548
  var downloadPathCache = /* @__PURE__ */ new Map();
@@ -102440,7 +102550,7 @@ var inFlightDownloads = /* @__PURE__ */ new Map();
102440
102550
  function getFilenameFromUrl(url) {
102441
102551
  const hash2 = createHash("md5").update(url).digest("hex").slice(0, 12);
102442
102552
  const urlObj = new URL(url);
102443
- const ext = extname2(urlObj.pathname) || ".mp4";
102553
+ const ext = extname3(urlObj.pathname) || ".mp4";
102444
102554
  return `download_${hash2}${ext}`;
102445
102555
  }
102446
102556
  async function downloadToTemp(url, destDir, timeoutMs = 3e5) {
@@ -102533,6 +102643,34 @@ function parseVideoElements(html) {
102533
102643
  }
102534
102644
  return videos;
102535
102645
  }
102646
+ function parseImageElements(html) {
102647
+ const images = [];
102648
+ const { document: document2 } = parseHTML(html);
102649
+ const imgEls = document2.querySelectorAll("img[src]");
102650
+ let autoIdCounter = 0;
102651
+ for (const el of imgEls) {
102652
+ const src = el.getAttribute("src");
102653
+ if (!src) continue;
102654
+ const id = el.getAttribute("id") || `hf-img-${autoIdCounter++}`;
102655
+ if (!el.getAttribute("id")) {
102656
+ el.setAttribute("id", id);
102657
+ }
102658
+ const startAttr = el.getAttribute("data-start");
102659
+ const endAttr = el.getAttribute("data-end");
102660
+ const durationAttr = el.getAttribute("data-duration");
102661
+ const start = startAttr ? parseFloat(startAttr) : 0;
102662
+ let end = 0;
102663
+ if (endAttr) {
102664
+ end = parseFloat(endAttr);
102665
+ } else if (durationAttr) {
102666
+ end = start + parseFloat(durationAttr);
102667
+ } else {
102668
+ end = Infinity;
102669
+ }
102670
+ images.push({ id, src, start, end });
102671
+ }
102672
+ return images;
102673
+ }
102536
102674
  async function extractVideoFramesRange(videoPath, videoId, startTime, duration, options, signal, config2) {
102537
102675
  const ffmpegProcessTimeout = config2?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
102538
102676
  const { fps, outputDir, quality = 95, format: format3 = "jpg" } = options;
@@ -102942,8 +103080,8 @@ function createVideoFrameInjector(frameLookup, config2) {
102942
103080
  }
102943
103081
  };
102944
103082
  }
102945
- async function queryElementStacking(page, nativeHdrVideoIds) {
102946
- const hdrIds = Array.from(nativeHdrVideoIds);
103083
+ async function queryElementStacking(page, nativeHdrIds) {
103084
+ const hdrIds = Array.from(nativeHdrIds);
102947
103085
  return page.evaluate((hdrIdList) => {
102948
103086
  const hdrSet = new Set(hdrIdList);
102949
103087
  const elements = document.querySelectorAll("[data-start]");
@@ -103072,7 +103210,12 @@ async function queryElementStacking(page, nativeHdrVideoIds) {
103072
103210
  // affine blit can apply rotation/scale/translate properly. For DOM
103073
103211
  // elements, the element-level transform is sufficient for reference.
103074
103212
  transform: isHdrEl ? getViewportMatrix(el) : style.transform || "none",
103075
- borderRadius: isHdrEl ? getEffectiveBorderRadius(el) : [0, 0, 0, 0]
103213
+ borderRadius: isHdrEl ? getEffectiveBorderRadius(el) : [0, 0, 0, 0],
103214
+ // `getComputedStyle` returns "" when the property doesn't apply (e.g.
103215
+ // for non-replaced elements); normalize to the CSS defaults so callers
103216
+ // can rely on a populated value.
103217
+ objectFit: style.objectFit || "fill",
103218
+ objectPosition: style.objectPosition || "50% 50%"
103076
103219
  });
103077
103220
  }
103078
103221
  return results;
@@ -106537,6 +106680,125 @@ function blitRgb48leAffine(canvas, source2, matrix, srcW, srcH, canvasW, canvasH
106537
106680
  }
106538
106681
  }
106539
106682
  }
106683
+ function parseObjectPositionAxis(value, axis) {
106684
+ const lower = value.trim().toLowerCase();
106685
+ if (lower === "left" || lower === "top") return 0;
106686
+ if (lower === "right" || lower === "bottom") return 1;
106687
+ if (lower === "center" || lower === "") return 0.5;
106688
+ if (lower.endsWith("%")) {
106689
+ const pct = parseFloat(lower) / 100;
106690
+ return Number.isFinite(pct) ? Math.max(0, Math.min(1, pct)) : 0.5;
106691
+ }
106692
+ if (axis === "x" || axis === "y") return 0.5;
106693
+ return 0.5;
106694
+ }
106695
+ function parseObjectPosition(css) {
106696
+ if (!css || !css.trim()) return { x: 0.5, y: 0.5 };
106697
+ const tokens = css.trim().split(/\s+/);
106698
+ if (tokens.length === 1) {
106699
+ const single = tokens[0] ?? "";
106700
+ const v = parseObjectPositionAxis(single, "x");
106701
+ return { x: v, y: 0.5 };
106702
+ }
106703
+ return {
106704
+ x: parseObjectPositionAxis(tokens[0] ?? "", "x"),
106705
+ y: parseObjectPositionAxis(tokens[1] ?? "", "y")
106706
+ };
106707
+ }
106708
+ function computeObjectFitRect(srcW, srcH, dstW, dstH, fit, pos) {
106709
+ let renderedW = dstW;
106710
+ let renderedH = dstH;
106711
+ if (fit === "fill") {
106712
+ return { dx: 0, dy: 0, dw: dstW, dh: dstH };
106713
+ }
106714
+ if (fit === "none") {
106715
+ renderedW = srcW;
106716
+ renderedH = srcH;
106717
+ } else if (fit === "scale-down") {
106718
+ const scale = Math.min(dstW / srcW, dstH / srcH, 1);
106719
+ renderedW = srcW * scale;
106720
+ renderedH = srcH * scale;
106721
+ } else if (fit === "cover") {
106722
+ const scale = Math.max(dstW / srcW, dstH / srcH);
106723
+ renderedW = srcW * scale;
106724
+ renderedH = srcH * scale;
106725
+ } else {
106726
+ const scale = Math.min(dstW / srcW, dstH / srcH);
106727
+ renderedW = srcW * scale;
106728
+ renderedH = srcH * scale;
106729
+ }
106730
+ const dx = (dstW - renderedW) * pos.x;
106731
+ const dy = (dstH - renderedH) * pos.y;
106732
+ return { dx, dy, dw: renderedW, dh: renderedH };
106733
+ }
106734
+ function resampleRgb48leObjectFit(source2, srcW, srcH, dstW, dstH, fit = "fill", objectPosition) {
106735
+ if (srcW <= 0 || srcH <= 0 || dstW <= 0 || dstH <= 0) {
106736
+ return source2;
106737
+ }
106738
+ if (fit === "fill" && srcW === dstW && srcH === dstH) {
106739
+ return source2;
106740
+ }
106741
+ const pos = parseObjectPosition(objectPosition);
106742
+ const rect = computeObjectFitRect(srcW, srcH, dstW, dstH, fit, pos);
106743
+ const dst = Buffer.alloc(dstW * dstH * 6);
106744
+ const stride = dstW * 6;
106745
+ const xMin = Math.max(0, Math.floor(rect.dx));
106746
+ const yMin = Math.max(0, Math.floor(rect.dy));
106747
+ const xMax = Math.min(dstW, Math.ceil(rect.dx + rect.dw));
106748
+ const yMax = Math.min(dstH, Math.ceil(rect.dy + rect.dh));
106749
+ if (rect.dw <= 0 || rect.dh <= 0) {
106750
+ return dst;
106751
+ }
106752
+ const invScaleX = srcW / rect.dw;
106753
+ const invScaleY = srcH / rect.dh;
106754
+ for (let dy = yMin; dy < yMax; dy++) {
106755
+ const rowOff = dy * stride;
106756
+ const sy = (dy + 0.5 - rect.dy) * invScaleY - 0.5;
106757
+ const syc = Math.max(0, Math.min(srcH - 1, sy));
106758
+ const y0 = Math.floor(syc);
106759
+ const y1 = Math.min(y0 + 1, srcH - 1);
106760
+ const fy = syc - y0;
106761
+ const ify = 1 - fy;
106762
+ for (let dx = xMin; dx < xMax; dx++) {
106763
+ const sx = (dx + 0.5 - rect.dx) * invScaleX - 0.5;
106764
+ const sxc = Math.max(0, Math.min(srcW - 1, sx));
106765
+ const x0 = Math.floor(sxc);
106766
+ const x1 = Math.min(x0 + 1, srcW - 1);
106767
+ const fx = sxc - x0;
106768
+ const ifx = 1 - fx;
106769
+ const off00 = (y0 * srcW + x0) * 6;
106770
+ const off10 = (y0 * srcW + x1) * 6;
106771
+ const off01 = (y1 * srcW + x0) * 6;
106772
+ const off11 = (y1 * srcW + x1) * 6;
106773
+ const w00 = ifx * ify;
106774
+ const w10 = fx * ify;
106775
+ const w01 = ifx * fy;
106776
+ const w11 = fx * fy;
106777
+ const r = source2.readUInt16LE(off00) * w00 + source2.readUInt16LE(off10) * w10 + source2.readUInt16LE(off01) * w01 + source2.readUInt16LE(off11) * w11;
106778
+ const g = source2.readUInt16LE(off00 + 2) * w00 + source2.readUInt16LE(off10 + 2) * w10 + source2.readUInt16LE(off01 + 2) * w01 + source2.readUInt16LE(off11 + 2) * w11;
106779
+ const b = source2.readUInt16LE(off00 + 4) * w00 + source2.readUInt16LE(off10 + 4) * w10 + source2.readUInt16LE(off01 + 4) * w01 + source2.readUInt16LE(off11 + 4) * w11;
106780
+ const dstOff = rowOff + dx * 6;
106781
+ dst.writeUInt16LE(Math.round(r), dstOff);
106782
+ dst.writeUInt16LE(Math.round(g), dstOff + 2);
106783
+ dst.writeUInt16LE(Math.round(b), dstOff + 4);
106784
+ }
106785
+ }
106786
+ return dst;
106787
+ }
106788
+ function normalizeObjectFit(value) {
106789
+ switch ((value ?? "").trim().toLowerCase()) {
106790
+ case "cover":
106791
+ return "cover";
106792
+ case "contain":
106793
+ return "contain";
106794
+ case "none":
106795
+ return "none";
106796
+ case "scale-down":
106797
+ return "scale-down";
106798
+ default:
106799
+ return "fill";
106800
+ }
106801
+ }
106540
106802
  function parseTransformMatrix(css) {
106541
106803
  if (!css || css === "none") return null;
106542
106804
  const match2 = css.match(
@@ -107205,12 +107467,12 @@ import { freemem as freemem2 } from "os";
107205
107467
  import { fileURLToPath as fileURLToPath3 } from "url";
107206
107468
 
107207
107469
  // src/services/fileServer.ts
107208
- import { readFileSync as readFileSync6, existsSync as existsSync12, statSync as statSync5 } from "node:fs";
107209
- import { join as join11, extname as extname3 } from "node:path";
107470
+ import { readFileSync as readFileSync7, existsSync as existsSync12, statSync as statSync5 } from "node:fs";
107471
+ import { join as join11, extname as extname4 } from "node:path";
107210
107472
 
107211
107473
  // src/services/hyperframeRuntimeLoader.ts
107212
107474
  import { createHash as createHash2 } from "node:crypto";
107213
- import { existsSync as existsSync11, readFileSync as readFileSync5 } from "node:fs";
107475
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "node:fs";
107214
107476
  import { dirname as dirname8, resolve as resolve7 } from "node:path";
107215
107477
  import { fileURLToPath as fileURLToPath2 } from "node:url";
107216
107478
  var PRODUCER_DIR = dirname8(fileURLToPath2(import.meta.url));
@@ -107253,7 +107515,7 @@ function resolveVerifiedHyperframeRuntime() {
107253
107515
  `[HyperframeRuntimeLoader] Missing manifest at ${manifestPath}. Build core runtime artifacts before rendering.`
107254
107516
  );
107255
107517
  }
107256
- const manifestRaw = readFileSync5(manifestPath, "utf8");
107518
+ const manifestRaw = readFileSync6(manifestPath, "utf8");
107257
107519
  const manifest = JSON.parse(manifestRaw);
107258
107520
  const runtimeFileName = manifest.artifacts?.iife;
107259
107521
  if (!runtimeFileName || !manifest.sha256) {
@@ -107265,7 +107527,7 @@ function resolveVerifiedHyperframeRuntime() {
107265
107527
  if (!existsSync11(runtimePath)) {
107266
107528
  throw new Error(`[HyperframeRuntimeLoader] Missing runtime artifact at ${runtimePath}.`);
107267
107529
  }
107268
- const runtimeSource = readFileSync5(runtimePath, "utf8");
107530
+ const runtimeSource = readFileSync6(runtimePath, "utf8");
107269
107531
  const runtimeSha = createHash2("sha256").update(runtimeSource, "utf8").digest("hex");
107270
107532
  if (runtimeSha !== manifest.sha256) {
107271
107533
  throw new Error(
@@ -107685,10 +107947,10 @@ function createFileServer2(options) {
107685
107947
  }
107686
107948
  return c.text("Not found", 404);
107687
107949
  }
107688
- const ext = extname3(filePath).toLowerCase();
107950
+ const ext = extname4(filePath).toLowerCase();
107689
107951
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
107690
107952
  if (ext === ".html") {
107691
- const rawHtml = readFileSync6(filePath, "utf-8");
107953
+ const rawHtml = readFileSync7(filePath, "utf-8");
107692
107954
  const isIndex = relativePath === "index.html";
107693
107955
  let html = rawHtml;
107694
107956
  if (preHeadScripts.length > 0) {
@@ -107697,7 +107959,7 @@ function createFileServer2(options) {
107697
107959
  html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
107698
107960
  return c.text(html, 200, { "Content-Type": contentType });
107699
107961
  }
107700
- const content = readFileSync6(filePath);
107962
+ const content = readFileSync7(filePath);
107701
107963
  return new Response(content, {
107702
107964
  status: 200,
107703
107965
  headers: { "Content-Type": contentType }
@@ -107724,7 +107986,7 @@ function createFileServer2(options) {
107724
107986
  }
107725
107987
 
107726
107988
  // src/services/htmlCompiler.ts
107727
- import { readFileSync as readFileSync8, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
107989
+ import { readFileSync as readFileSync9, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
107728
107990
  import { join as join14, dirname as dirname9, resolve as resolve9 } from "path";
107729
107991
  import postcss from "postcss";
107730
107992
 
@@ -107757,7 +108019,7 @@ function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS
107757
108019
  }
107758
108020
 
107759
108021
  // src/services/deterministicFonts.ts
107760
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
108022
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
107761
108023
  import { homedir as homedir2 } from "node:os";
107762
108024
  import { join as join13 } from "node:path";
107763
108025
 
@@ -108086,7 +108348,7 @@ async function fetchGoogleFont(familyName) {
108086
108348
  continue;
108087
108349
  }
108088
108350
  }
108089
- const fontBytes = readFileSync7(cachePath);
108351
+ const fontBytes = readFileSync8(cachePath);
108090
108352
  const dataUri = `data:font/woff2;base64,${fontBytes.toString("base64")}`;
108091
108353
  faces.push({ weight, style, dataUri });
108092
108354
  }
@@ -108237,6 +108499,7 @@ async function compileHtmlFile(html, baseDir, downloadDir) {
108237
108499
  async function parseSubCompositions(html, projectDir, downloadDir, parentOffset = 0, parentEnd = Infinity, visited = /* @__PURE__ */ new Set()) {
108238
108500
  const videos = [];
108239
108501
  const audios = [];
108502
+ const images = [];
108240
108503
  const subCompositions = /* @__PURE__ */ new Map();
108241
108504
  const { document: document2 } = parseHTML(html);
108242
108505
  const compEls = document2.querySelectorAll("[data-composition-src]");
@@ -108256,7 +108519,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
108256
108519
  if (!existsSync14(filePath)) {
108257
108520
  continue;
108258
108521
  }
108259
- const rawSubHtml = readFileSync8(filePath, "utf-8");
108522
+ const rawSubHtml = readFileSync9(filePath, "utf-8");
108260
108523
  const nestedVisited = new Set(visited);
108261
108524
  nestedVisited.add(filePath);
108262
108525
  workItems.push({ srcPath, absoluteStart, absoluteEnd, filePath, rawSubHtml, nestedVisited });
@@ -108278,12 +108541,14 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
108278
108541
  );
108279
108542
  const subVideos = parseVideoElements(compiledSub);
108280
108543
  const subAudios = parseAudioElements(compiledSub);
108544
+ const subImages = parseImageElements(compiledSub);
108281
108545
  return {
108282
108546
  srcPath: item.srcPath,
108283
108547
  compiledSub,
108284
108548
  nested,
108285
108549
  subVideos,
108286
108550
  subAudios,
108551
+ subImages,
108287
108552
  absoluteStart: item.absoluteStart,
108288
108553
  absoluteEnd: item.absoluteEnd
108289
108554
  };
@@ -108296,6 +108561,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
108296
108561
  }
108297
108562
  videos.push(...r.nested.videos);
108298
108563
  audios.push(...r.nested.audios);
108564
+ images.push(...r.nested.images);
108299
108565
  for (const v of r.subVideos) {
108300
108566
  v.start += r.absoluteStart;
108301
108567
  v.end += r.absoluteStart;
@@ -108316,10 +108582,20 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
108316
108582
  audios.push(a);
108317
108583
  }
108318
108584
  }
108319
- if (r.subVideos.length > 0 || r.subAudios.length > 0 || r.nested.videos.length > 0 || r.nested.audios.length > 0) {
108585
+ for (const img of r.subImages) {
108586
+ img.start += r.absoluteStart;
108587
+ img.end += r.absoluteStart;
108588
+ if (img.end > r.absoluteEnd) {
108589
+ img.end = r.absoluteEnd;
108590
+ }
108591
+ if (img.start < r.absoluteEnd) {
108592
+ images.push(img);
108593
+ }
108594
+ }
108595
+ if (r.subVideos.length > 0 || r.subAudios.length > 0 || r.subImages.length > 0 || r.nested.videos.length > 0 || r.nested.audios.length > 0 || r.nested.images.length > 0) {
108320
108596
  }
108321
108597
  }
108322
- return { videos, audios, subCompositions };
108598
+ return { videos, audios, images, subCompositions };
108323
108599
  }
108324
108600
  function promoteCssImportsToLinkTags(html) {
108325
108601
  const { document: document2 } = parseHTML(html);
@@ -108451,7 +108727,7 @@ function inlineSubCompositions(html, subCompositions, projectDir) {
108451
108727
  if (!compHtml) {
108452
108728
  const filePath = resolve9(projectDir, srcPath);
108453
108729
  if (existsSync14(filePath)) {
108454
- compHtml = readFileSync8(filePath, "utf-8");
108730
+ compHtml = readFileSync9(filePath, "utf-8");
108455
108731
  }
108456
108732
  }
108457
108733
  if (!compHtml) {
@@ -108619,7 +108895,9 @@ ${html}
108619
108895
  </html>`;
108620
108896
  }
108621
108897
  async function inlineExternalScripts(html) {
108622
- const { document: document2 } = parseHTML(html);
108898
+ const fullHtml = ensureFullDocument(html);
108899
+ const wrappedFragment = fullHtml !== html;
108900
+ const { document: document2 } = parseHTML(fullHtml);
108623
108901
  const scripts = document2.querySelectorAll("script[src]");
108624
108902
  const externalScripts = [];
108625
108903
  for (const el of scripts) {
@@ -108638,20 +108916,20 @@ async function inlineExternalScripts(html) {
108638
108916
  return { src, text: await response.text() };
108639
108917
  })
108640
108918
  );
108641
- let result = html;
108642
108919
  for (let i = 0; i < downloads.length; i++) {
108643
108920
  const download = downloads[i];
108644
- const { src } = externalScripts[i];
108921
+ const { el, src } = externalScripts[i];
108645
108922
  if (download.status === "fulfilled") {
108646
- const escapedSrc = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
108647
- const scriptTagRe = new RegExp(
108648
- `<script\\b[^>]*\\bsrc=["']${escapedSrc}["'][^>]*>\\s*</script>`,
108649
- "is"
108650
- );
108651
108923
  const safeText = download.value.text.replace(/<\/script/gi, "<\\/script");
108652
- result = result.replace(scriptTagRe, `<script>/* inlined: ${src} */
108924
+ const inlineScript = document2.createElement("script");
108925
+ for (const attr of Array.from(el.attributes)) {
108926
+ if (attr.name.toLowerCase() === "src") continue;
108927
+ inlineScript.setAttribute(attr.name, attr.value);
108928
+ }
108929
+ inlineScript.textContent = `/* inlined: ${src} */
108653
108930
  ${safeText}
108654
- </script>`);
108931
+ `;
108932
+ el.replaceWith(inlineScript);
108655
108933
  console.log(`[Compiler] Inlined CDN script: ${src}`);
108656
108934
  } else {
108657
108935
  console.warn(
@@ -108659,7 +108937,7 @@ ${safeText}
108659
108937
  );
108660
108938
  }
108661
108939
  }
108662
- return result;
108940
+ return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
108663
108941
  }
108664
108942
  function collectExternalAssets(html, projectDir) {
108665
108943
  const absProjectDir = resolve9(projectDir);
@@ -108719,7 +108997,7 @@ function collectExternalAssets(html, projectDir) {
108719
108997
  };
108720
108998
  }
108721
108999
  async function compileForRender(projectDir, htmlPath, downloadDir) {
108722
- const rawHtml = readFileSync8(htmlPath, "utf-8");
109000
+ const rawHtml = readFileSync9(htmlPath, "utf-8");
108723
109001
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
108724
109002
  rawHtml,
108725
109003
  projectDir,
@@ -108728,6 +109006,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
108728
109006
  const {
108729
109007
  videos: subVideos,
108730
109008
  audios: subAudios,
109009
+ images: subImages,
108731
109010
  subCompositions
108732
109011
  } = await parseSubCompositions(compiledHtml, projectDir, downloadDir);
108733
109012
  const fullHtml = ensureFullDocument(compiledHtml);
@@ -108744,8 +109023,10 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
108744
109023
  const { html, externalAssets } = collectExternalAssets(assembledHtml, projectDir);
108745
109024
  const mainVideos = parseVideoElements(html);
108746
109025
  const mainAudios = parseAudioElements(html);
109026
+ const mainImages = parseImageElements(html);
108747
109027
  const videos = dedupeElementsById([...mainVideos, ...subVideos]);
108748
109028
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
109029
+ const images = dedupeElementsById([...mainImages, ...subImages]);
108749
109030
  for (const video of videos) {
108750
109031
  if (isHttpUrl(video.src)) continue;
108751
109032
  const videoPath = resolve9(projectDir, video.src);
@@ -108776,6 +109057,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
108776
109057
  subCompositions,
108777
109058
  videos,
108778
109059
  audios,
109060
+ images,
108779
109061
  unresolvedCompositions,
108780
109062
  externalAssets,
108781
109063
  width,
@@ -108860,12 +109142,15 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
108860
109142
  const {
108861
109143
  videos: subVideos,
108862
109144
  audios: subAudios,
109145
+ images: subImages,
108863
109146
  subCompositions
108864
109147
  } = await parseSubCompositions(html, projectDir, downloadDir);
108865
109148
  const mainVideos = parseVideoElements(html);
108866
109149
  const mainAudios = parseAudioElements(html);
109150
+ const mainImages = parseImageElements(html);
108867
109151
  const videos = dedupeElementsById([...mainVideos, ...subVideos]);
108868
109152
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
109153
+ const images = dedupeElementsById([...mainImages, ...subImages]);
108869
109154
  const remaining = compiled.unresolvedCompositions.filter(
108870
109155
  (c) => !resolutions.some((r) => r.id === c.id)
108871
109156
  );
@@ -108875,6 +109160,7 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
108875
109160
  subCompositions,
108876
109161
  videos,
108877
109162
  audios,
109163
+ images,
108878
109164
  unresolvedCompositions: remaining,
108879
109165
  renderModeHints: compiled.renderModeHints
108880
109166
  };
@@ -109060,7 +109346,7 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, w
109060
109346
  return;
109061
109347
  }
109062
109348
  try {
109063
- const { data: hdrRgb, width: srcW, height: srcH } = decodePngToRgb48le(readFileSync9(framePath));
109349
+ const { data: hdrRgb, width: srcW, height: srcH } = decodePngToRgb48le(readFileSync10(framePath));
109064
109350
  if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
109065
109351
  convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
109066
109352
  }
@@ -109102,6 +109388,55 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, w
109102
109388
  }
109103
109389
  }
109104
109390
  }
109391
+ function blitHdrImageLayer(canvas, el, hdrImageBuffers, width, height, log, sourceTransfer, targetTransfer) {
109392
+ const buf = hdrImageBuffers.get(el.id);
109393
+ if (!buf) {
109394
+ return;
109395
+ }
109396
+ try {
109397
+ let hdrRgb = buf.data;
109398
+ if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
109399
+ hdrRgb = Buffer.from(buf.data);
109400
+ convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
109401
+ }
109402
+ const viewportMatrix = parseTransformMatrix(el.transform);
109403
+ const br = el.borderRadius;
109404
+ const hasBorderRadius = br[0] > 0 || br[1] > 0 || br[2] > 0 || br[3] > 0;
109405
+ const borderRadiusParam = hasBorderRadius ? br : void 0;
109406
+ if (viewportMatrix) {
109407
+ blitRgb48leAffine(
109408
+ canvas,
109409
+ hdrRgb,
109410
+ viewportMatrix,
109411
+ buf.width,
109412
+ buf.height,
109413
+ width,
109414
+ height,
109415
+ el.opacity < 0.999 ? el.opacity : void 0,
109416
+ borderRadiusParam
109417
+ );
109418
+ } else {
109419
+ blitRgb48leRegion(
109420
+ canvas,
109421
+ hdrRgb,
109422
+ el.x,
109423
+ el.y,
109424
+ buf.width,
109425
+ buf.height,
109426
+ width,
109427
+ height,
109428
+ el.opacity < 0.999 ? el.opacity : void 0,
109429
+ borderRadiusParam
109430
+ );
109431
+ }
109432
+ } catch (err) {
109433
+ if (log) {
109434
+ log.debug(`HDR image blit failed for ${el.id}`, {
109435
+ error: err instanceof Error ? err.message : String(err)
109436
+ });
109437
+ }
109438
+ }
109439
+ }
109105
109440
  function createRenderJob(config2) {
109106
109441
  return {
109107
109442
  id: randomUUID(),
@@ -109153,6 +109488,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109153
109488
  let lastBrowserConsole = [];
109154
109489
  let restoreLogger = null;
109155
109490
  const perfStages = {};
109491
+ const hdrDiagnostics = {
109492
+ videoExtractionFailures: 0,
109493
+ imageDecodeFailures: 0
109494
+ };
109156
109495
  const perfOutputPath = join15(workDir, "perf-summary.json");
109157
109496
  const cfg = { ...job.config.producerConfig ?? resolveConfig() };
109158
109497
  const outputFormat = job.config.format ?? "mp4";
@@ -109184,7 +109523,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109184
109523
  throw new Error(`Entry file not found: ${htmlPath}`);
109185
109524
  }
109186
109525
  assertNotAborted();
109187
- const rawEntry = readFileSync9(htmlPath, "utf-8");
109526
+ const rawEntry = readFileSync10(htmlPath, "utf-8");
109188
109527
  if (entryFile !== "index.html" && rawEntry.trimStart().startsWith("<template")) {
109189
109528
  const wrapperPath = join15(workDir, "standalone-entry.html");
109190
109529
  const projectIndexPath = join15(projectDir, "index.html");
@@ -109194,7 +109533,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109194
109533
  );
109195
109534
  }
109196
109535
  const standaloneHtml = extractStandaloneEntryFromIndex(
109197
- readFileSync9(projectIndexPath, "utf-8"),
109536
+ readFileSync10(projectIndexPath, "utf-8"),
109198
109537
  entryFile
109199
109538
  );
109200
109539
  if (!standaloneHtml) {
@@ -109229,6 +109568,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109229
109568
  duration: compiled.staticDuration,
109230
109569
  videos: compiled.videos,
109231
109570
  audios: compiled.audios,
109571
+ images: compiled.images,
109232
109572
  width: compiled.width,
109233
109573
  height: compiled.height
109234
109574
  };
@@ -109293,6 +109633,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109293
109633
  assertNotAborted();
109294
109634
  composition.videos = compiled.videos;
109295
109635
  composition.audios = compiled.audios;
109636
+ composition.images = compiled.images;
109296
109637
  writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
109297
109638
  }
109298
109639
  }
@@ -109448,6 +109789,30 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109448
109789
  })
109449
109790
  );
109450
109791
  }
109792
+ const nativeHdrImageIds = /* @__PURE__ */ new Set();
109793
+ const imageTransfers = /* @__PURE__ */ new Map();
109794
+ const hdrImageSrcPaths = /* @__PURE__ */ new Map();
109795
+ const imageColorSpaces = [];
109796
+ if (job.config.hdr && composition.images.length > 0) {
109797
+ const probed = await Promise.all(
109798
+ composition.images.map(async (img) => {
109799
+ let imgPath = img.src;
109800
+ if (!imgPath.startsWith("/")) {
109801
+ const fromCompiled = existsSync15(join15(compiledDir, imgPath)) ? join15(compiledDir, imgPath) : join15(projectDir, imgPath);
109802
+ imgPath = fromCompiled;
109803
+ }
109804
+ if (!existsSync15(imgPath)) return null;
109805
+ const meta = await extractVideoMetadata(imgPath);
109806
+ if (isHdrColorSpace(meta.colorSpace)) {
109807
+ nativeHdrImageIds.add(img.id);
109808
+ imageTransfers.set(img.id, detectTransfer(meta.colorSpace));
109809
+ hdrImageSrcPaths.set(img.id, imgPath);
109810
+ }
109811
+ return meta.colorSpace;
109812
+ })
109813
+ );
109814
+ imageColorSpaces.push(...probed);
109815
+ }
109451
109816
  if (composition.videos.length > 0) {
109452
109817
  extractionResult = await extractAllVideoFrames(
109453
109818
  composition.videos,
@@ -109485,21 +109850,22 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109485
109850
  perfStages.videoExtractMs = Date.now() - stage2Start;
109486
109851
  }
109487
109852
  let effectiveHdr;
109488
- if (job.config.hdr && frameLookup) {
109489
- const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
109490
- const info = analyzeCompositionHdr(colorSpaces);
109491
- if (info.hasHdr && info.dominantTransfer) {
109492
- effectiveHdr = { transfer: info.dominantTransfer };
109493
- }
109494
- }
109495
- if (job.config.hdr && !effectiveHdr && nativeHdrVideoIds.size > 0) {
109496
- const firstTransfer = videoTransfers.values().next().value;
109497
- if (firstTransfer) {
109498
- effectiveHdr = { transfer: firstTransfer };
109853
+ if (job.config.hdr) {
109854
+ const videoColorSpaces = (extractionResult?.extracted ?? []).map(
109855
+ (ext) => ext.metadata.colorSpace
109856
+ );
109857
+ const allColorSpaces = [...videoColorSpaces, ...imageColorSpaces];
109858
+ if (allColorSpaces.length > 0) {
109859
+ const info = analyzeCompositionHdr(allColorSpaces);
109860
+ if (info.hasHdr && info.dominantTransfer) {
109861
+ effectiveHdr = { transfer: info.dominantTransfer };
109862
+ }
109499
109863
  }
109500
109864
  }
109501
109865
  if (effectiveHdr && outputFormat !== "mp4") {
109502
- log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
109866
+ log.warn(
109867
+ `[Render] HDR source detected but format is ${outputFormat} \u2014 falling back to SDR. Use --format mp4 for HDR10 output.`
109868
+ );
109503
109869
  effectiveHdr = void 0;
109504
109870
  }
109505
109871
  if (effectiveHdr) {
@@ -109552,12 +109918,14 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109552
109918
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
109553
109919
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
109554
109920
  const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
109555
- const hasHdrContent = effectiveHdr && nativeHdrVideoIds.size > 0;
109921
+ const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
109922
+ const hasHdrContent = effectiveHdr && nativeHdrIds.size > 0;
109556
109923
  const encoderHdr = hasHdrContent ? effectiveHdr : void 0;
109557
109924
  const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
109558
109925
  job.framesRendered = 0;
109559
109926
  if (hasHdrContent) {
109560
109927
  log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
109928
+ cfg.forceScreenshot = true;
109561
109929
  const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
109562
109930
  const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
109563
109931
  for (const v of composition.videos) {
@@ -109573,7 +109941,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109573
109941
  const domSession = await createCaptureSession(
109574
109942
  fileServer.url,
109575
109943
  framesDir,
109576
- captureOptions,
109944
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
109577
109945
  createVideoFrameInjector(frameLookup),
109578
109946
  cfg
109579
109947
  );
@@ -109627,13 +109995,22 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109627
109995
  );
109628
109996
  assertNotAborted();
109629
109997
  const hdrExtractionDims = /* @__PURE__ */ new Map();
109998
+ const hdrImageFitInfo = /* @__PURE__ */ new Map();
109630
109999
  const hdrVideoStartTimes = /* @__PURE__ */ new Map();
109631
110000
  for (const v of composition.videos) {
109632
110001
  if (hdrVideoIds.includes(v.id)) {
109633
110002
  hdrVideoStartTimes.set(v.id, v.start);
109634
110003
  }
109635
110004
  }
109636
- const uniqueStartTimes = [...new Set(hdrVideoStartTimes.values())].sort((a, b) => a - b);
110005
+ const hdrImageStartTimes = /* @__PURE__ */ new Map();
110006
+ for (const img of composition.images) {
110007
+ if (nativeHdrImageIds.has(img.id)) {
110008
+ hdrImageStartTimes.set(img.id, img.start);
110009
+ }
110010
+ }
110011
+ const uniqueStartTimes = [
110012
+ .../* @__PURE__ */ new Set([...hdrVideoStartTimes.values(), ...hdrImageStartTimes.values()])
110013
+ ].sort((a, b) => a - b);
109637
110014
  for (const seekTime of uniqueStartTimes) {
109638
110015
  await domSession.page.evaluate((t) => {
109639
110016
  if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
@@ -109641,11 +110018,17 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109641
110018
  if (domSession.onBeforeCapture) {
109642
110019
  await domSession.onBeforeCapture(domSession.page, seekTime);
109643
110020
  }
109644
- const stacking = await queryElementStacking(domSession.page, nativeHdrVideoIds);
110021
+ const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
109645
110022
  for (const el of stacking) {
109646
110023
  if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !hdrExtractionDims.has(el.id)) {
109647
110024
  hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
109648
110025
  }
110026
+ if (el.isHdr && nativeHdrImageIds.has(el.id) && !hdrImageFitInfo.has(el.id)) {
110027
+ hdrImageFitInfo.set(el.id, {
110028
+ fit: el.objectFit,
110029
+ position: el.objectPosition
110030
+ });
110031
+ }
109649
110032
  }
109650
110033
  }
109651
110034
  const hdrFrameDirs = /* @__PURE__ */ new Map();
@@ -109676,14 +110059,59 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109676
110059
  ];
109677
110060
  const result = await runFfmpeg(ffmpegArgs, { signal: abortSignal });
109678
110061
  if (!result.success) {
109679
- log.warn("HDR frame pre-extraction failed; loop will fill with black", {
110062
+ hdrDiagnostics.videoExtractionFailures += 1;
110063
+ log.error("HDR frame pre-extraction failed; aborting render", {
109680
110064
  videoId,
109681
110065
  srcPath,
109682
110066
  stderr: result.stderr.slice(-400)
109683
110067
  });
110068
+ throw new Error(
110069
+ `HDR frame extraction failed for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
110070
+ );
109684
110071
  }
109685
110072
  hdrFrameDirs.set(videoId, frameDir);
109686
110073
  }
110074
+ const hdrImageBuffers = /* @__PURE__ */ new Map();
110075
+ for (const [imageId, srcPath] of hdrImageSrcPaths) {
110076
+ try {
110077
+ const decoded = decodePngToRgb48le(readFileSync10(srcPath));
110078
+ const layout2 = hdrExtractionDims.get(imageId);
110079
+ const fitInfo = hdrImageFitInfo.get(imageId);
110080
+ if (layout2 && (layout2.width !== decoded.width || layout2.height !== decoded.height)) {
110081
+ const fit = normalizeObjectFit(fitInfo?.fit);
110082
+ const resampled = resampleRgb48leObjectFit(
110083
+ decoded.data,
110084
+ decoded.width,
110085
+ decoded.height,
110086
+ layout2.width,
110087
+ layout2.height,
110088
+ fit,
110089
+ fitInfo?.position
110090
+ );
110091
+ hdrImageBuffers.set(imageId, {
110092
+ data: resampled,
110093
+ width: layout2.width,
110094
+ height: layout2.height
110095
+ });
110096
+ } else {
110097
+ hdrImageBuffers.set(imageId, {
110098
+ data: Buffer.from(decoded.data),
110099
+ width: decoded.width,
110100
+ height: decoded.height
110101
+ });
110102
+ }
110103
+ } catch (err) {
110104
+ hdrDiagnostics.imageDecodeFailures += 1;
110105
+ log.error("HDR image decode failed; aborting render", {
110106
+ imageId,
110107
+ srcPath,
110108
+ error: err instanceof Error ? err.message : String(err)
110109
+ });
110110
+ throw new Error(
110111
+ `HDR image decode failed for image "${imageId}". Aborting render to avoid shipping missing HDR image layers.`
110112
+ );
110113
+ }
110114
+ }
109687
110115
  assertNotAborted();
109688
110116
  try {
109689
110117
  let countNonZeroAlpha2 = function(rgba) {
@@ -109741,38 +110169,67 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109741
110169
  const layer = layers[layerIdx];
109742
110170
  if (layer.type === "hdr") {
109743
110171
  const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
109744
- blitHdrVideoLayer(
109745
- canvas,
109746
- layer.element,
109747
- time,
109748
- job.config.fps,
109749
- hdrFrameDirs,
109750
- hdrVideoStartTimes,
109751
- width,
109752
- height,
109753
- log,
109754
- videoTransfers.get(layer.element.id),
109755
- effectiveHdr?.transfer
109756
- );
110172
+ const isHdrImage = nativeHdrImageIds.has(layer.element.id);
110173
+ if (isHdrImage) {
110174
+ blitHdrImageLayer(
110175
+ canvas,
110176
+ layer.element,
110177
+ hdrImageBuffers,
110178
+ width,
110179
+ height,
110180
+ log,
110181
+ imageTransfers.get(layer.element.id),
110182
+ effectiveHdr?.transfer
110183
+ );
110184
+ } else {
110185
+ blitHdrVideoLayer(
110186
+ canvas,
110187
+ layer.element,
110188
+ time,
110189
+ job.config.fps,
110190
+ hdrFrameDirs,
110191
+ hdrVideoStartTimes,
110192
+ width,
110193
+ height,
110194
+ log,
110195
+ videoTransfers.get(layer.element.id),
110196
+ effectiveHdr?.transfer
110197
+ );
110198
+ }
109757
110199
  if (shouldLog) {
109758
110200
  const after2 = countNonZeroRgb482(canvas);
109759
- const frameDir = hdrFrameDirs.get(layer.element.id);
109760
- const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
109761
- const localTime = time - startTime;
109762
- const frameNum = Math.floor(localTime * job.config.fps) + 1;
109763
- const expectedFrame = frameDir ? join15(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
109764
- log.info("[diag] hdr layer blit", {
109765
- frame: debugFrameIndex,
109766
- layerIdx,
109767
- id: layer.element.id,
109768
- pixelsAdded: after2 - before2,
109769
- totalNonZero: after2,
109770
- startTime,
109771
- localTime: localTime.toFixed(3),
109772
- hdrFrameNum: frameNum,
109773
- expectedFrame,
109774
- expectedFrameExists: expectedFrame ? existsSync15(expectedFrame) : false
109775
- });
110201
+ if (isHdrImage) {
110202
+ const buf = hdrImageBuffers.get(layer.element.id);
110203
+ log.info("[diag] hdr layer blit", {
110204
+ frame: debugFrameIndex,
110205
+ layerIdx,
110206
+ id: layer.element.id,
110207
+ kind: "image",
110208
+ pixelsAdded: after2 - before2,
110209
+ totalNonZero: after2,
110210
+ bufferDecoded: !!buf,
110211
+ bufferDims: buf ? `${buf.width}x${buf.height}` : null
110212
+ });
110213
+ } else {
110214
+ const frameDir = hdrFrameDirs.get(layer.element.id);
110215
+ const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
110216
+ const localTime = time - startTime;
110217
+ const frameNum = Math.floor(localTime * job.config.fps) + 1;
110218
+ const expectedFrame = frameDir ? join15(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
110219
+ log.info("[diag] hdr layer blit", {
110220
+ frame: debugFrameIndex,
110221
+ layerIdx,
110222
+ id: layer.element.id,
110223
+ kind: "video",
110224
+ pixelsAdded: after2 - before2,
110225
+ totalNonZero: after2,
110226
+ startTime,
110227
+ localTime: localTime.toFixed(3),
110228
+ hdrFrameNum: frameNum,
110229
+ expectedFrame,
110230
+ expectedFrameExists: expectedFrame ? existsSync15(expectedFrame) : false
110231
+ });
110232
+ }
109776
110233
  }
109777
110234
  } else {
109778
110235
  const allElementIds = fullStacking.map((e) => e.id);
@@ -109847,7 +110304,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109847
110304
  if (beforeCaptureHook) {
109848
110305
  await beforeCaptureHook(domSession.page, time);
109849
110306
  }
109850
- const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
110307
+ const stackingInfo = await queryElementStacking(domSession.page, nativeHdrIds);
109851
110308
  const activeTransition = transitionRanges.find(
109852
110309
  (t) => i >= t.startFrame && i <= t.endFrame
109853
110310
  );
@@ -109879,22 +110336,35 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109879
110336
  }
109880
110337
  for (const el of stackingInfo) {
109881
110338
  if (!el.isHdr || !sceneIds.has(el.id)) continue;
109882
- blitHdrVideoLayer(
109883
- sceneBuf,
109884
- el,
109885
- time,
109886
- job.config.fps,
109887
- hdrFrameDirs,
109888
- hdrVideoStartTimes,
109889
- width,
109890
- height,
109891
- log,
109892
- videoTransfers.get(el.id),
109893
- effectiveHdr?.transfer
109894
- );
110339
+ if (nativeHdrImageIds.has(el.id)) {
110340
+ blitHdrImageLayer(
110341
+ sceneBuf,
110342
+ el,
110343
+ hdrImageBuffers,
110344
+ width,
110345
+ height,
110346
+ log,
110347
+ imageTransfers.get(el.id),
110348
+ effectiveHdr?.transfer
110349
+ );
110350
+ } else {
110351
+ blitHdrVideoLayer(
110352
+ sceneBuf,
110353
+ el,
110354
+ time,
110355
+ job.config.fps,
110356
+ hdrFrameDirs,
110357
+ hdrVideoStartTimes,
110358
+ width,
110359
+ height,
110360
+ log,
110361
+ videoTransfers.get(el.id),
110362
+ effectiveHdr?.transfer
110363
+ );
110364
+ }
109895
110365
  }
109896
110366
  const showIds = Array.from(sceneIds);
109897
- const hideIds = stackingInfo.map((e) => e.id).filter((id) => !sceneIds.has(id) || nativeHdrVideoIds.has(id));
110367
+ const hideIds = stackingInfo.map((e) => e.id).filter((id) => !sceneIds.has(id) || nativeHdrIds.has(id));
109898
110368
  await applyDomLayerMask(domSession.page, showIds, hideIds);
109899
110369
  const domPng = await captureAlphaPng(domSession.page, width, height);
109900
110370
  await removeDomLayerMask(domSession.page, hideIds);
@@ -110015,7 +110485,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110015
110485
  fileServer.url,
110016
110486
  workDir,
110017
110487
  tasks,
110018
- captureOptions,
110488
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
110019
110489
  () => createVideoFrameInjector(frameLookup),
110020
110490
  abortSignal,
110021
110491
  (progress) => {
@@ -110045,7 +110515,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110045
110515
  const session = probeSession ?? await createCaptureSession(
110046
110516
  fileServer.url,
110047
110517
  framesDir,
110048
- captureOptions,
110518
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
110049
110519
  videoInjector,
110050
110520
  cfg
110051
110521
  );
@@ -110096,7 +110566,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110096
110566
  fileServer.url,
110097
110567
  workDir,
110098
110568
  tasks,
110099
- captureOptions,
110569
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
110100
110570
  () => createVideoFrameInjector(frameLookup),
110101
110571
  abortSignal,
110102
110572
  (progress) => {
@@ -110127,7 +110597,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110127
110597
  const session = probeSession ?? await createCaptureSession(
110128
110598
  fileServer.url,
110129
110599
  framesDir,
110130
- captureOptions,
110600
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
110131
110601
  videoInjector,
110132
110602
  cfg
110133
110603
  );
@@ -110245,6 +110715,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110245
110715
  videoCount: composition.videos.length,
110246
110716
  audioCount: composition.audios.length,
110247
110717
  stages: perfStages,
110718
+ hdrDiagnostics: hdrDiagnostics.videoExtractionFailures > 0 || hdrDiagnostics.imageDecodeFailures > 0 ? { ...hdrDiagnostics } : void 0,
110248
110719
  captureAvgMs: totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / totalFrames) : void 0
110249
110720
  };
110250
110721
  job.perfSummary = perfSummary;
@@ -110325,7 +110796,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110325
110796
  elapsedMs: elapsed,
110326
110797
  freeMemoryMB: freeMemMB,
110327
110798
  browserConsoleTail: lastBrowserConsole.length > 0 ? lastBrowserConsole.slice(-30) : void 0,
110328
- perfStages: Object.keys(perfStages).length > 0 ? { ...perfStages } : void 0
110799
+ perfStages: Object.keys(perfStages).length > 0 ? { ...perfStages } : void 0,
110800
+ hdrDiagnostics: hdrDiagnostics.videoExtractionFailures > 0 || hdrDiagnostics.imageDecodeFailures > 0 ? { ...hdrDiagnostics } : void 0
110329
110801
  };
110330
110802
  if (fileServer) {
110331
110803
  const fs8 = fileServer;
@@ -110520,7 +110992,7 @@ var streamSSE = (c, cb, onError) => {
110520
110992
  };
110521
110993
 
110522
110994
  // src/services/hyperframeLint.ts
110523
- import { existsSync as existsSync16, readFileSync as readFileSync10, statSync as statSync6 } from "node:fs";
110995
+ import { existsSync as existsSync16, readFileSync as readFileSync11, statSync as statSync6 } from "node:fs";
110524
110996
  import { resolve as resolve11, join as join16 } from "node:path";
110525
110997
  function isStringRecord(value) {
110526
110998
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -110563,7 +111035,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
110563
111035
  if (existsSync16(absoluteEntryPath) && statSync6(absoluteEntryPath).isFile()) {
110564
111036
  return {
110565
111037
  entryFile,
110566
- html: readFileSync10(absoluteEntryPath, "utf-8"),
111038
+ html: readFileSync11(absoluteEntryPath, "utf-8"),
110567
111039
  source: "projectDir"
110568
111040
  };
110569
111041
  }