@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.
@@ -78349,16 +78349,16 @@ var require_buffer_crc32 = __commonJS({
78349
78349
  }
78350
78350
  return crc ^ -1;
78351
78351
  }
78352
- function crc32() {
78352
+ function crc322() {
78353
78353
  return bufferizeInt(_crc32.apply(null, arguments));
78354
78354
  }
78355
- crc32.signed = function() {
78355
+ crc322.signed = function() {
78356
78356
  return _crc32.apply(null, arguments);
78357
78357
  };
78358
- crc32.unsigned = function() {
78358
+ crc322.unsigned = function() {
78359
78359
  return _crc32.apply(null, arguments) >>> 0;
78360
78360
  };
78361
- module.exports = crc32;
78361
+ module.exports = crc322;
78362
78362
  }
78363
78363
  });
78364
78364
 
@@ -78368,7 +78368,7 @@ var require_yauzl = __commonJS({
78368
78368
  var fs8 = __require("fs");
78369
78369
  var zlib = __require("zlib");
78370
78370
  var fd_slicer = require_fd_slicer();
78371
- var crc32 = require_buffer_crc32();
78371
+ var crc322 = require_buffer_crc32();
78372
78372
  var util = __require("util");
78373
78373
  var EventEmitter4 = __require("events").EventEmitter;
78374
78374
  var Transform = __require("stream").Transform;
@@ -78654,7 +78654,7 @@ var require_yauzl = __commonJS({
78654
78654
  continue;
78655
78655
  }
78656
78656
  var oldNameCrc32 = extraField.data.readUInt32LE(1);
78657
- if (crc32.unsigned(buffer.slice(0, entry.fileNameLength)) !== oldNameCrc32) {
78657
+ if (crc322.unsigned(buffer.slice(0, entry.fileNameLength)) !== oldNameCrc32) {
78658
78658
  continue;
78659
78659
  }
78660
78660
  entry.fileName = decodeBuffer(extraField.data, 5, extraField.data.length, true);
@@ -92209,7 +92209,7 @@ import {
92209
92209
  existsSync as existsSync15,
92210
92210
  mkdirSync as mkdirSync10,
92211
92211
  rmSync as rmSync3,
92212
- readFileSync as readFileSync9,
92212
+ readFileSync as readFileSync10,
92213
92213
  readdirSync as readdirSync6,
92214
92214
  writeFileSync as writeFileSync4,
92215
92215
  copyFileSync as copyFileSync2,
@@ -102430,9 +102430,25 @@ var mediaRules = [
102430
102430
  // video_nested_in_timed_element
102431
102431
  ({ source: source2, tags }) => {
102432
102432
  const findings = [];
102433
+ const voidElements3 = /* @__PURE__ */ new Set([
102434
+ "area",
102435
+ "base",
102436
+ "br",
102437
+ "col",
102438
+ "embed",
102439
+ "hr",
102440
+ "img",
102441
+ "input",
102442
+ "link",
102443
+ "meta",
102444
+ "source",
102445
+ "track",
102446
+ "wbr"
102447
+ ]);
102433
102448
  const timedTagPositions = [];
102434
102449
  for (const tag of tags) {
102435
102450
  if (tag.name === "video" || tag.name === "audio") continue;
102451
+ if (voidElements3.has(tag.name)) continue;
102436
102452
  if (readAttr(tag.raw, "data-composition-id")) continue;
102437
102453
  if (readAttr(tag.raw, "data-start")) {
102438
102454
  timedTagPositions.push({
@@ -102599,7 +102615,9 @@ function extractGsapWindows(script) {
102599
102615
  let index = 0;
102600
102616
  while ((match2 = methodPattern.exec(script)) !== null && index < parsed.animations.length) {
102601
102617
  const raw2 = match2[0];
102602
- const meta = parseGsapWindowMeta(match2[1] ?? "", match2[2] ?? "");
102618
+ const args = match2[2] ?? "";
102619
+ if (!/^\s*["']/.test(args)) continue;
102620
+ const meta = parseGsapWindowMeta(match2[1] ?? "", args);
102603
102621
  const animation = parsed.animations[index];
102604
102622
  index += 1;
102605
102623
  if (!animation) continue;
@@ -103893,6 +103911,12 @@ async function createCaptureSession(serverUrl, outputDir, options, onBeforeCaptu
103893
103911
  );
103894
103912
  const { browser, captureMode } = await acquireBrowser(chromeArgs, config2);
103895
103913
  const page = await browser.newPage();
103914
+ await page.evaluateOnNewDocument(() => {
103915
+ const w = window;
103916
+ if (typeof w.__name !== "function") {
103917
+ w.__name = (fn, _name) => fn;
103918
+ }
103919
+ });
103896
103920
  const browserVersion = await browser.version();
103897
103921
  const expectedMajor = config2?.expectedChromiumMajor;
103898
103922
  if (Number.isFinite(expectedMajor)) {
@@ -103997,9 +104021,10 @@ async function initializeSession(session) {
103997
104021
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
103998
104022
  );
103999
104023
  }
104024
+ const skipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
104000
104025
  const videosReady = await pollPageExpression(
104001
104026
  page,
104002
- `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`,
104027
+ `(() => { 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); })()`,
104003
104028
  pageReadyTimeout2
104004
104029
  );
104005
104030
  if (!videosReady) {
@@ -104059,10 +104084,11 @@ async function initializeSession(session) {
104059
104084
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout}ms. Page must expose window.__hf = { duration, seek }.`
104060
104085
  );
104061
104086
  }
104087
+ const beginframeSkipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
104062
104088
  const videoDeadline = Date.now() + (session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout);
104063
104089
  while (Date.now() < videoDeadline) {
104064
104090
  const videosReady = await page.evaluate(
104065
- `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`
104091
+ `(() => { 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); })()`
104066
104092
  );
104067
104093
  if (videosReady) break;
104068
104094
  await new Promise((r) => setTimeout(r, 100));
@@ -105042,6 +105068,8 @@ import { join as join8 } from "path";
105042
105068
 
105043
105069
  // ../engine/src/utils/ffprobe.ts
105044
105070
  import { spawn as spawn7 } from "child_process";
105071
+ import { readFileSync as readFileSync5 } from "fs";
105072
+ import { extname as extname2 } from "path";
105045
105073
  function runFfprobe(args) {
105046
105074
  return new Promise((resolve13, reject) => {
105047
105075
  const proc = spawn7("ffprobe", args);
@@ -105080,6 +105108,67 @@ function parseProbeJson(stdout) {
105080
105108
  }
105081
105109
  var videoMetadataCache = /* @__PURE__ */ new Map();
105082
105110
  var audioMetadataCache = /* @__PURE__ */ new Map();
105111
+ function crc32(buf) {
105112
+ let crc = 4294967295;
105113
+ for (let i = 0; i < buf.length; i++) {
105114
+ crc ^= buf[i] ?? 0;
105115
+ for (let bit = 0; bit < 8; bit++) {
105116
+ const mask = -(crc & 1);
105117
+ crc = crc >>> 1 ^ 3988292384 & mask;
105118
+ }
105119
+ }
105120
+ return (crc ^ 4294967295) >>> 0;
105121
+ }
105122
+ function extractPngMetadataFromBuffer(buf) {
105123
+ 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) {
105124
+ return null;
105125
+ }
105126
+ let width = 0;
105127
+ let height = 0;
105128
+ let seenIdat = false;
105129
+ let pos = 8;
105130
+ while (pos + 12 <= buf.length) {
105131
+ const chunkLen = buf.readUInt32BE(pos);
105132
+ const chunkType = buf.toString("ascii", pos + 4, pos + 8);
105133
+ if (pos + 12 + chunkLen > buf.length) return null;
105134
+ const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
105135
+ const chunkCrc = buf.readUInt32BE(pos + 8 + chunkLen);
105136
+ const chunkBytes = Buffer.concat([Buffer.from(chunkType, "ascii"), chunkData]);
105137
+ if (crc32(chunkBytes) !== chunkCrc) return null;
105138
+ if (chunkType === "IHDR" && chunkLen >= 8) {
105139
+ width = buf.readUInt32BE(pos + 8);
105140
+ height = buf.readUInt32BE(pos + 12);
105141
+ }
105142
+ if (chunkType === "IDAT") {
105143
+ seenIdat = true;
105144
+ }
105145
+ if (chunkType === "cICP" && chunkLen === 4 && !seenIdat) {
105146
+ const primariesCode = chunkData[0] ?? 0;
105147
+ const transferCode = chunkData[1] ?? 0;
105148
+ const matrixCode = chunkData[2] ?? 0;
105149
+ return {
105150
+ width,
105151
+ height,
105152
+ colorSpace: {
105153
+ colorPrimaries: primariesCode === 9 ? "bt2020" : primariesCode === 1 ? "bt709" : `unknown-${primariesCode}`,
105154
+ colorTransfer: transferCode === 16 ? "smpte2084" : transferCode === 18 ? "arib-std-b67" : transferCode === 1 ? "bt709" : `unknown-${transferCode}`,
105155
+ colorSpace: matrixCode === 9 ? "bt2020nc" : matrixCode === 0 ? "gbr" : `unknown-${matrixCode}`
105156
+ }
105157
+ };
105158
+ }
105159
+ if (chunkType === "IEND") break;
105160
+ pos += 12 + chunkLen;
105161
+ }
105162
+ return width > 0 && height > 0 ? { width, height, colorSpace: null } : null;
105163
+ }
105164
+ function extractStillImageMetadata(filePath) {
105165
+ if (extname2(filePath).toLowerCase() !== ".png") return null;
105166
+ try {
105167
+ return extractPngMetadataFromBuffer(readFileSync5(filePath));
105168
+ } catch {
105169
+ return null;
105170
+ }
105171
+ }
105083
105172
  function parseFrameRate(frameRateStr) {
105084
105173
  if (!frameRateStr) return 0;
105085
105174
  const parts = frameRateStr.split("/");
@@ -105094,18 +105183,38 @@ async function extractVideoMetadata(filePath) {
105094
105183
  const cached = videoMetadataCache.get(filePath);
105095
105184
  if (cached) return cached;
105096
105185
  const probePromise = (async () => {
105097
- const stdout = await runFfprobe([
105098
- "-v",
105099
- "quiet",
105100
- "-print_format",
105101
- "json",
105102
- "-show_format",
105103
- "-show_streams",
105104
- filePath
105105
- ]);
105106
- const output2 = parseProbeJson(stdout);
105107
- const videoStream = output2.streams.find((s) => s.codec_type === "video");
105108
- if (!videoStream) throw new Error("[FFmpeg] No video stream found");
105186
+ const stillImageMeta = extractStillImageMetadata(filePath);
105187
+ let output2 = null;
105188
+ try {
105189
+ const stdout = await runFfprobe([
105190
+ "-v",
105191
+ "quiet",
105192
+ "-print_format",
105193
+ "json",
105194
+ "-show_format",
105195
+ "-show_streams",
105196
+ filePath
105197
+ ]);
105198
+ output2 = parseProbeJson(stdout);
105199
+ } catch (error) {
105200
+ if (!stillImageMeta) throw error;
105201
+ }
105202
+ const videoStream = output2?.streams.find((s) => s.codec_type === "video");
105203
+ if (!videoStream) {
105204
+ if (stillImageMeta) {
105205
+ return {
105206
+ durationSeconds: 0,
105207
+ width: stillImageMeta.width,
105208
+ height: stillImageMeta.height,
105209
+ fps: 0,
105210
+ videoCodec: "png",
105211
+ hasAudio: false,
105212
+ isVFR: false,
105213
+ colorSpace: stillImageMeta.colorSpace
105214
+ };
105215
+ }
105216
+ throw new Error("[FFmpeg] No video stream found");
105217
+ }
105109
105218
  const rFps = parseFrameRate(videoStream.r_frame_rate);
105110
105219
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
105111
105220
  const fps = avgFps || rFps;
@@ -105113,16 +105222,17 @@ async function extractVideoMetadata(filePath) {
105113
105222
  const colorTransfer = videoStream.color_transfer || "";
105114
105223
  const colorPrimaries = videoStream.color_primaries || "";
105115
105224
  const colorSpaceVal = videoStream.color_space || "";
105116
- const hasColorInfo = !!(colorTransfer || colorPrimaries || colorSpaceVal);
105225
+ const ffprobeColorSpace = colorTransfer || colorPrimaries || colorSpaceVal ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null;
105226
+ const colorSpace = ffprobeColorSpace ?? stillImageMeta?.colorSpace ?? null;
105117
105227
  return {
105118
- durationSeconds: output2.format.duration ? parseFloat(output2.format.duration) : 0,
105119
- width: videoStream.width || 0,
105120
- height: videoStream.height || 0,
105228
+ durationSeconds: output2?.format.duration ? parseFloat(output2.format.duration) : 0,
105229
+ width: videoStream.width || stillImageMeta?.width || 0,
105230
+ height: videoStream.height || stillImageMeta?.height || 0,
105121
105231
  fps,
105122
105232
  videoCodec: videoStream.codec_name || "unknown",
105123
- hasAudio: output2.streams.some((s) => s.codec_type === "audio"),
105233
+ hasAudio: output2?.streams.some((s) => s.codec_type === "audio") ?? false,
105124
105234
  isVFR,
105125
- colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
105235
+ colorSpace
105126
105236
  };
105127
105237
  })();
105128
105238
  videoMetadataCache.set(filePath, probePromise);
@@ -105221,7 +105331,7 @@ async function analyzeKeyframeIntervalsUncached(filePath) {
105221
105331
  // ../engine/src/utils/urlDownloader.ts
105222
105332
  import { createWriteStream as createWriteStream2, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
105223
105333
  import { createHash } from "crypto";
105224
- import { join as join7, extname as extname2 } from "path";
105334
+ import { join as join7, extname as extname3 } from "path";
105225
105335
  import { Readable as Readable2 } from "stream";
105226
105336
  import { finished } from "stream/promises";
105227
105337
  var downloadPathCache = /* @__PURE__ */ new Map();
@@ -105229,7 +105339,7 @@ var inFlightDownloads = /* @__PURE__ */ new Map();
105229
105339
  function getFilenameFromUrl(url) {
105230
105340
  const hash2 = createHash("md5").update(url).digest("hex").slice(0, 12);
105231
105341
  const urlObj = new URL(url);
105232
- const ext = extname2(urlObj.pathname) || ".mp4";
105342
+ const ext = extname3(urlObj.pathname) || ".mp4";
105233
105343
  return `download_${hash2}${ext}`;
105234
105344
  }
105235
105345
  async function downloadToTemp(url, destDir, timeoutMs = 3e5) {
@@ -105322,6 +105432,34 @@ function parseVideoElements(html) {
105322
105432
  }
105323
105433
  return videos;
105324
105434
  }
105435
+ function parseImageElements(html) {
105436
+ const images = [];
105437
+ const { document: document2 } = parseHTML(html);
105438
+ const imgEls = document2.querySelectorAll("img[src]");
105439
+ let autoIdCounter = 0;
105440
+ for (const el of imgEls) {
105441
+ const src = el.getAttribute("src");
105442
+ if (!src) continue;
105443
+ const id = el.getAttribute("id") || `hf-img-${autoIdCounter++}`;
105444
+ if (!el.getAttribute("id")) {
105445
+ el.setAttribute("id", id);
105446
+ }
105447
+ const startAttr = el.getAttribute("data-start");
105448
+ const endAttr = el.getAttribute("data-end");
105449
+ const durationAttr = el.getAttribute("data-duration");
105450
+ const start = startAttr ? parseFloat(startAttr) : 0;
105451
+ let end = 0;
105452
+ if (endAttr) {
105453
+ end = parseFloat(endAttr);
105454
+ } else if (durationAttr) {
105455
+ end = start + parseFloat(durationAttr);
105456
+ } else {
105457
+ end = Infinity;
105458
+ }
105459
+ images.push({ id, src, start, end });
105460
+ }
105461
+ return images;
105462
+ }
105325
105463
  async function extractVideoFramesRange(videoPath, videoId, startTime, duration, options, signal, config2) {
105326
105464
  const ffmpegProcessTimeout = config2?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
105327
105465
  const { fps, outputDir, quality = 95, format: format3 = "jpg" } = options;
@@ -105731,8 +105869,8 @@ function createVideoFrameInjector(frameLookup, config2) {
105731
105869
  }
105732
105870
  };
105733
105871
  }
105734
- async function queryElementStacking(page, nativeHdrVideoIds) {
105735
- const hdrIds = Array.from(nativeHdrVideoIds);
105872
+ async function queryElementStacking(page, nativeHdrIds) {
105873
+ const hdrIds = Array.from(nativeHdrIds);
105736
105874
  return page.evaluate((hdrIdList) => {
105737
105875
  const hdrSet = new Set(hdrIdList);
105738
105876
  const elements = document.querySelectorAll("[data-start]");
@@ -105861,7 +105999,12 @@ async function queryElementStacking(page, nativeHdrVideoIds) {
105861
105999
  // affine blit can apply rotation/scale/translate properly. For DOM
105862
106000
  // elements, the element-level transform is sufficient for reference.
105863
106001
  transform: isHdrEl ? getViewportMatrix(el) : style.transform || "none",
105864
- borderRadius: isHdrEl ? getEffectiveBorderRadius(el) : [0, 0, 0, 0]
106002
+ borderRadius: isHdrEl ? getEffectiveBorderRadius(el) : [0, 0, 0, 0],
106003
+ // `getComputedStyle` returns "" when the property doesn't apply (e.g.
106004
+ // for non-replaced elements); normalize to the CSS defaults so callers
106005
+ // can rely on a populated value.
106006
+ objectFit: style.objectFit || "fill",
106007
+ objectPosition: style.objectPosition || "50% 50%"
105865
106008
  });
105866
106009
  }
105867
106010
  return results;
@@ -106702,6 +106845,125 @@ function blitRgb48leAffine(canvas, source2, matrix, srcW, srcH, canvasW, canvasH
106702
106845
  }
106703
106846
  }
106704
106847
  }
106848
+ function parseObjectPositionAxis(value, axis) {
106849
+ const lower = value.trim().toLowerCase();
106850
+ if (lower === "left" || lower === "top") return 0;
106851
+ if (lower === "right" || lower === "bottom") return 1;
106852
+ if (lower === "center" || lower === "") return 0.5;
106853
+ if (lower.endsWith("%")) {
106854
+ const pct = parseFloat(lower) / 100;
106855
+ return Number.isFinite(pct) ? Math.max(0, Math.min(1, pct)) : 0.5;
106856
+ }
106857
+ if (axis === "x" || axis === "y") return 0.5;
106858
+ return 0.5;
106859
+ }
106860
+ function parseObjectPosition(css) {
106861
+ if (!css || !css.trim()) return { x: 0.5, y: 0.5 };
106862
+ const tokens = css.trim().split(/\s+/);
106863
+ if (tokens.length === 1) {
106864
+ const single = tokens[0] ?? "";
106865
+ const v = parseObjectPositionAxis(single, "x");
106866
+ return { x: v, y: 0.5 };
106867
+ }
106868
+ return {
106869
+ x: parseObjectPositionAxis(tokens[0] ?? "", "x"),
106870
+ y: parseObjectPositionAxis(tokens[1] ?? "", "y")
106871
+ };
106872
+ }
106873
+ function computeObjectFitRect(srcW, srcH, dstW, dstH, fit, pos) {
106874
+ let renderedW = dstW;
106875
+ let renderedH = dstH;
106876
+ if (fit === "fill") {
106877
+ return { dx: 0, dy: 0, dw: dstW, dh: dstH };
106878
+ }
106879
+ if (fit === "none") {
106880
+ renderedW = srcW;
106881
+ renderedH = srcH;
106882
+ } else if (fit === "scale-down") {
106883
+ const scale = Math.min(dstW / srcW, dstH / srcH, 1);
106884
+ renderedW = srcW * scale;
106885
+ renderedH = srcH * scale;
106886
+ } else if (fit === "cover") {
106887
+ const scale = Math.max(dstW / srcW, dstH / srcH);
106888
+ renderedW = srcW * scale;
106889
+ renderedH = srcH * scale;
106890
+ } else {
106891
+ const scale = Math.min(dstW / srcW, dstH / srcH);
106892
+ renderedW = srcW * scale;
106893
+ renderedH = srcH * scale;
106894
+ }
106895
+ const dx = (dstW - renderedW) * pos.x;
106896
+ const dy = (dstH - renderedH) * pos.y;
106897
+ return { dx, dy, dw: renderedW, dh: renderedH };
106898
+ }
106899
+ function resampleRgb48leObjectFit(source2, srcW, srcH, dstW, dstH, fit = "fill", objectPosition) {
106900
+ if (srcW <= 0 || srcH <= 0 || dstW <= 0 || dstH <= 0) {
106901
+ return source2;
106902
+ }
106903
+ if (fit === "fill" && srcW === dstW && srcH === dstH) {
106904
+ return source2;
106905
+ }
106906
+ const pos = parseObjectPosition(objectPosition);
106907
+ const rect = computeObjectFitRect(srcW, srcH, dstW, dstH, fit, pos);
106908
+ const dst = Buffer.alloc(dstW * dstH * 6);
106909
+ const stride = dstW * 6;
106910
+ const xMin = Math.max(0, Math.floor(rect.dx));
106911
+ const yMin = Math.max(0, Math.floor(rect.dy));
106912
+ const xMax = Math.min(dstW, Math.ceil(rect.dx + rect.dw));
106913
+ const yMax = Math.min(dstH, Math.ceil(rect.dy + rect.dh));
106914
+ if (rect.dw <= 0 || rect.dh <= 0) {
106915
+ return dst;
106916
+ }
106917
+ const invScaleX = srcW / rect.dw;
106918
+ const invScaleY = srcH / rect.dh;
106919
+ for (let dy = yMin; dy < yMax; dy++) {
106920
+ const rowOff = dy * stride;
106921
+ const sy = (dy + 0.5 - rect.dy) * invScaleY - 0.5;
106922
+ const syc = Math.max(0, Math.min(srcH - 1, sy));
106923
+ const y0 = Math.floor(syc);
106924
+ const y1 = Math.min(y0 + 1, srcH - 1);
106925
+ const fy = syc - y0;
106926
+ const ify = 1 - fy;
106927
+ for (let dx = xMin; dx < xMax; dx++) {
106928
+ const sx = (dx + 0.5 - rect.dx) * invScaleX - 0.5;
106929
+ const sxc = Math.max(0, Math.min(srcW - 1, sx));
106930
+ const x0 = Math.floor(sxc);
106931
+ const x1 = Math.min(x0 + 1, srcW - 1);
106932
+ const fx = sxc - x0;
106933
+ const ifx = 1 - fx;
106934
+ const off00 = (y0 * srcW + x0) * 6;
106935
+ const off10 = (y0 * srcW + x1) * 6;
106936
+ const off01 = (y1 * srcW + x0) * 6;
106937
+ const off11 = (y1 * srcW + x1) * 6;
106938
+ const w00 = ifx * ify;
106939
+ const w10 = fx * ify;
106940
+ const w01 = ifx * fy;
106941
+ const w11 = fx * fy;
106942
+ const r = source2.readUInt16LE(off00) * w00 + source2.readUInt16LE(off10) * w10 + source2.readUInt16LE(off01) * w01 + source2.readUInt16LE(off11) * w11;
106943
+ const g = source2.readUInt16LE(off00 + 2) * w00 + source2.readUInt16LE(off10 + 2) * w10 + source2.readUInt16LE(off01 + 2) * w01 + source2.readUInt16LE(off11 + 2) * w11;
106944
+ const b = source2.readUInt16LE(off00 + 4) * w00 + source2.readUInt16LE(off10 + 4) * w10 + source2.readUInt16LE(off01 + 4) * w01 + source2.readUInt16LE(off11 + 4) * w11;
106945
+ const dstOff = rowOff + dx * 6;
106946
+ dst.writeUInt16LE(Math.round(r), dstOff);
106947
+ dst.writeUInt16LE(Math.round(g), dstOff + 2);
106948
+ dst.writeUInt16LE(Math.round(b), dstOff + 4);
106949
+ }
106950
+ }
106951
+ return dst;
106952
+ }
106953
+ function normalizeObjectFit(value) {
106954
+ switch ((value ?? "").trim().toLowerCase()) {
106955
+ case "cover":
106956
+ return "cover";
106957
+ case "contain":
106958
+ return "contain";
106959
+ case "none":
106960
+ return "none";
106961
+ case "scale-down":
106962
+ return "scale-down";
106963
+ default:
106964
+ return "fill";
106965
+ }
106966
+ }
106705
106967
  function parseTransformMatrix(css) {
106706
106968
  if (!css || css === "none") return null;
106707
106969
  const match2 = css.match(
@@ -107370,12 +107632,12 @@ import { freemem as freemem2 } from "os";
107370
107632
  import { fileURLToPath as fileURLToPath3 } from "url";
107371
107633
 
107372
107634
  // src/services/fileServer.ts
107373
- import { readFileSync as readFileSync6, existsSync as existsSync12, statSync as statSync5 } from "node:fs";
107374
- import { join as join11, extname as extname3 } from "node:path";
107635
+ import { readFileSync as readFileSync7, existsSync as existsSync12, statSync as statSync5 } from "node:fs";
107636
+ import { join as join11, extname as extname4 } from "node:path";
107375
107637
 
107376
107638
  // src/services/hyperframeRuntimeLoader.ts
107377
107639
  import { createHash as createHash2 } from "node:crypto";
107378
- import { existsSync as existsSync11, readFileSync as readFileSync5 } from "node:fs";
107640
+ import { existsSync as existsSync11, readFileSync as readFileSync6 } from "node:fs";
107379
107641
  import { dirname as dirname8, resolve as resolve7 } from "node:path";
107380
107642
  import { fileURLToPath as fileURLToPath2 } from "node:url";
107381
107643
  var PRODUCER_DIR = dirname8(fileURLToPath2(import.meta.url));
@@ -107418,7 +107680,7 @@ function resolveVerifiedHyperframeRuntime() {
107418
107680
  `[HyperframeRuntimeLoader] Missing manifest at ${manifestPath}. Build core runtime artifacts before rendering.`
107419
107681
  );
107420
107682
  }
107421
- const manifestRaw = readFileSync5(manifestPath, "utf8");
107683
+ const manifestRaw = readFileSync6(manifestPath, "utf8");
107422
107684
  const manifest = JSON.parse(manifestRaw);
107423
107685
  const runtimeFileName = manifest.artifacts?.iife;
107424
107686
  if (!runtimeFileName || !manifest.sha256) {
@@ -107430,7 +107692,7 @@ function resolveVerifiedHyperframeRuntime() {
107430
107692
  if (!existsSync11(runtimePath)) {
107431
107693
  throw new Error(`[HyperframeRuntimeLoader] Missing runtime artifact at ${runtimePath}.`);
107432
107694
  }
107433
- const runtimeSource = readFileSync5(runtimePath, "utf8");
107695
+ const runtimeSource = readFileSync6(runtimePath, "utf8");
107434
107696
  const runtimeSha = createHash2("sha256").update(runtimeSource, "utf8").digest("hex");
107435
107697
  if (runtimeSha !== manifest.sha256) {
107436
107698
  throw new Error(
@@ -107850,10 +108112,10 @@ function createFileServer2(options) {
107850
108112
  }
107851
108113
  return c.text("Not found", 404);
107852
108114
  }
107853
- const ext = extname3(filePath).toLowerCase();
108115
+ const ext = extname4(filePath).toLowerCase();
107854
108116
  const contentType = MIME_TYPES[ext] || "application/octet-stream";
107855
108117
  if (ext === ".html") {
107856
- const rawHtml = readFileSync6(filePath, "utf-8");
108118
+ const rawHtml = readFileSync7(filePath, "utf-8");
107857
108119
  const isIndex = relativePath === "index.html";
107858
108120
  let html = rawHtml;
107859
108121
  if (preHeadScripts.length > 0) {
@@ -107862,7 +108124,7 @@ function createFileServer2(options) {
107862
108124
  html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
107863
108125
  return c.text(html, 200, { "Content-Type": contentType });
107864
108126
  }
107865
- const content = readFileSync6(filePath);
108127
+ const content = readFileSync7(filePath);
107866
108128
  return new Response(content, {
107867
108129
  status: 200,
107868
108130
  headers: { "Content-Type": contentType }
@@ -107889,7 +108151,7 @@ function createFileServer2(options) {
107889
108151
  }
107890
108152
 
107891
108153
  // src/services/htmlCompiler.ts
107892
- import { readFileSync as readFileSync8, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
108154
+ import { readFileSync as readFileSync9, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
107893
108155
  import { join as join14, dirname as dirname9, resolve as resolve9 } from "path";
107894
108156
  import postcss from "postcss";
107895
108157
 
@@ -107922,7 +108184,7 @@ function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS
107922
108184
  }
107923
108185
 
107924
108186
  // src/services/deterministicFonts.ts
107925
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
108187
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
107926
108188
  import { homedir as homedir2 } from "node:os";
107927
108189
  import { join as join13 } from "node:path";
107928
108190
 
@@ -108251,7 +108513,7 @@ async function fetchGoogleFont(familyName) {
108251
108513
  continue;
108252
108514
  }
108253
108515
  }
108254
- const fontBytes = readFileSync7(cachePath);
108516
+ const fontBytes = readFileSync8(cachePath);
108255
108517
  const dataUri = `data:font/woff2;base64,${fontBytes.toString("base64")}`;
108256
108518
  faces.push({ weight, style, dataUri });
108257
108519
  }
@@ -108402,6 +108664,7 @@ async function compileHtmlFile(html, baseDir, downloadDir) {
108402
108664
  async function parseSubCompositions(html, projectDir, downloadDir, parentOffset = 0, parentEnd = Infinity, visited = /* @__PURE__ */ new Set()) {
108403
108665
  const videos = [];
108404
108666
  const audios = [];
108667
+ const images = [];
108405
108668
  const subCompositions = /* @__PURE__ */ new Map();
108406
108669
  const { document: document2 } = parseHTML(html);
108407
108670
  const compEls = document2.querySelectorAll("[data-composition-src]");
@@ -108421,7 +108684,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
108421
108684
  if (!existsSync14(filePath)) {
108422
108685
  continue;
108423
108686
  }
108424
- const rawSubHtml = readFileSync8(filePath, "utf-8");
108687
+ const rawSubHtml = readFileSync9(filePath, "utf-8");
108425
108688
  const nestedVisited = new Set(visited);
108426
108689
  nestedVisited.add(filePath);
108427
108690
  workItems.push({ srcPath, absoluteStart, absoluteEnd, filePath, rawSubHtml, nestedVisited });
@@ -108443,12 +108706,14 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
108443
108706
  );
108444
108707
  const subVideos = parseVideoElements(compiledSub);
108445
108708
  const subAudios = parseAudioElements(compiledSub);
108709
+ const subImages = parseImageElements(compiledSub);
108446
108710
  return {
108447
108711
  srcPath: item.srcPath,
108448
108712
  compiledSub,
108449
108713
  nested,
108450
108714
  subVideos,
108451
108715
  subAudios,
108716
+ subImages,
108452
108717
  absoluteStart: item.absoluteStart,
108453
108718
  absoluteEnd: item.absoluteEnd
108454
108719
  };
@@ -108461,6 +108726,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
108461
108726
  }
108462
108727
  videos.push(...r.nested.videos);
108463
108728
  audios.push(...r.nested.audios);
108729
+ images.push(...r.nested.images);
108464
108730
  for (const v of r.subVideos) {
108465
108731
  v.start += r.absoluteStart;
108466
108732
  v.end += r.absoluteStart;
@@ -108481,10 +108747,20 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
108481
108747
  audios.push(a);
108482
108748
  }
108483
108749
  }
108484
- if (r.subVideos.length > 0 || r.subAudios.length > 0 || r.nested.videos.length > 0 || r.nested.audios.length > 0) {
108750
+ for (const img of r.subImages) {
108751
+ img.start += r.absoluteStart;
108752
+ img.end += r.absoluteStart;
108753
+ if (img.end > r.absoluteEnd) {
108754
+ img.end = r.absoluteEnd;
108755
+ }
108756
+ if (img.start < r.absoluteEnd) {
108757
+ images.push(img);
108758
+ }
108759
+ }
108760
+ 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) {
108485
108761
  }
108486
108762
  }
108487
- return { videos, audios, subCompositions };
108763
+ return { videos, audios, images, subCompositions };
108488
108764
  }
108489
108765
  function promoteCssImportsToLinkTags(html) {
108490
108766
  const { document: document2 } = parseHTML(html);
@@ -108616,7 +108892,7 @@ function inlineSubCompositions(html, subCompositions, projectDir) {
108616
108892
  if (!compHtml) {
108617
108893
  const filePath = resolve9(projectDir, srcPath);
108618
108894
  if (existsSync14(filePath)) {
108619
- compHtml = readFileSync8(filePath, "utf-8");
108895
+ compHtml = readFileSync9(filePath, "utf-8");
108620
108896
  }
108621
108897
  }
108622
108898
  if (!compHtml) {
@@ -108784,7 +109060,9 @@ ${html}
108784
109060
  </html>`;
108785
109061
  }
108786
109062
  async function inlineExternalScripts(html) {
108787
- const { document: document2 } = parseHTML(html);
109063
+ const fullHtml = ensureFullDocument(html);
109064
+ const wrappedFragment = fullHtml !== html;
109065
+ const { document: document2 } = parseHTML(fullHtml);
108788
109066
  const scripts = document2.querySelectorAll("script[src]");
108789
109067
  const externalScripts = [];
108790
109068
  for (const el of scripts) {
@@ -108803,20 +109081,20 @@ async function inlineExternalScripts(html) {
108803
109081
  return { src, text: await response.text() };
108804
109082
  })
108805
109083
  );
108806
- let result = html;
108807
109084
  for (let i = 0; i < downloads.length; i++) {
108808
109085
  const download = downloads[i];
108809
- const { src } = externalScripts[i];
109086
+ const { el, src } = externalScripts[i];
108810
109087
  if (download.status === "fulfilled") {
108811
- const escapedSrc = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
108812
- const scriptTagRe = new RegExp(
108813
- `<script\\b[^>]*\\bsrc=["']${escapedSrc}["'][^>]*>\\s*</script>`,
108814
- "is"
108815
- );
108816
109088
  const safeText = download.value.text.replace(/<\/script/gi, "<\\/script");
108817
- result = result.replace(scriptTagRe, `<script>/* inlined: ${src} */
109089
+ const inlineScript = document2.createElement("script");
109090
+ for (const attr of Array.from(el.attributes)) {
109091
+ if (attr.name.toLowerCase() === "src") continue;
109092
+ inlineScript.setAttribute(attr.name, attr.value);
109093
+ }
109094
+ inlineScript.textContent = `/* inlined: ${src} */
108818
109095
  ${safeText}
108819
- </script>`);
109096
+ `;
109097
+ el.replaceWith(inlineScript);
108820
109098
  console.log(`[Compiler] Inlined CDN script: ${src}`);
108821
109099
  } else {
108822
109100
  console.warn(
@@ -108824,7 +109102,7 @@ ${safeText}
108824
109102
  );
108825
109103
  }
108826
109104
  }
108827
- return result;
109105
+ return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
108828
109106
  }
108829
109107
  function collectExternalAssets(html, projectDir) {
108830
109108
  const absProjectDir = resolve9(projectDir);
@@ -108884,7 +109162,7 @@ function collectExternalAssets(html, projectDir) {
108884
109162
  };
108885
109163
  }
108886
109164
  async function compileForRender(projectDir, htmlPath, downloadDir) {
108887
- const rawHtml = readFileSync8(htmlPath, "utf-8");
109165
+ const rawHtml = readFileSync9(htmlPath, "utf-8");
108888
109166
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
108889
109167
  rawHtml,
108890
109168
  projectDir,
@@ -108893,6 +109171,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
108893
109171
  const {
108894
109172
  videos: subVideos,
108895
109173
  audios: subAudios,
109174
+ images: subImages,
108896
109175
  subCompositions
108897
109176
  } = await parseSubCompositions(compiledHtml, projectDir, downloadDir);
108898
109177
  const fullHtml = ensureFullDocument(compiledHtml);
@@ -108909,8 +109188,10 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
108909
109188
  const { html, externalAssets } = collectExternalAssets(assembledHtml, projectDir);
108910
109189
  const mainVideos = parseVideoElements(html);
108911
109190
  const mainAudios = parseAudioElements(html);
109191
+ const mainImages = parseImageElements(html);
108912
109192
  const videos = dedupeElementsById([...mainVideos, ...subVideos]);
108913
109193
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
109194
+ const images = dedupeElementsById([...mainImages, ...subImages]);
108914
109195
  for (const video of videos) {
108915
109196
  if (isHttpUrl(video.src)) continue;
108916
109197
  const videoPath = resolve9(projectDir, video.src);
@@ -108941,6 +109222,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
108941
109222
  subCompositions,
108942
109223
  videos,
108943
109224
  audios,
109225
+ images,
108944
109226
  unresolvedCompositions,
108945
109227
  externalAssets,
108946
109228
  width,
@@ -109025,12 +109307,15 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
109025
109307
  const {
109026
109308
  videos: subVideos,
109027
109309
  audios: subAudios,
109310
+ images: subImages,
109028
109311
  subCompositions
109029
109312
  } = await parseSubCompositions(html, projectDir, downloadDir);
109030
109313
  const mainVideos = parseVideoElements(html);
109031
109314
  const mainAudios = parseAudioElements(html);
109315
+ const mainImages = parseImageElements(html);
109032
109316
  const videos = dedupeElementsById([...mainVideos, ...subVideos]);
109033
109317
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
109318
+ const images = dedupeElementsById([...mainImages, ...subImages]);
109034
109319
  const remaining = compiled.unresolvedCompositions.filter(
109035
109320
  (c) => !resolutions.some((r) => r.id === c.id)
109036
109321
  );
@@ -109040,6 +109325,7 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
109040
109325
  subCompositions,
109041
109326
  videos,
109042
109327
  audios,
109328
+ images,
109043
109329
  unresolvedCompositions: remaining,
109044
109330
  renderModeHints: compiled.renderModeHints
109045
109331
  };
@@ -109225,7 +109511,7 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, w
109225
109511
  return;
109226
109512
  }
109227
109513
  try {
109228
- const { data: hdrRgb, width: srcW, height: srcH } = decodePngToRgb48le(readFileSync9(framePath));
109514
+ const { data: hdrRgb, width: srcW, height: srcH } = decodePngToRgb48le(readFileSync10(framePath));
109229
109515
  if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
109230
109516
  convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
109231
109517
  }
@@ -109267,6 +109553,55 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, w
109267
109553
  }
109268
109554
  }
109269
109555
  }
109556
+ function blitHdrImageLayer(canvas, el, hdrImageBuffers, width, height, log, sourceTransfer, targetTransfer) {
109557
+ const buf = hdrImageBuffers.get(el.id);
109558
+ if (!buf) {
109559
+ return;
109560
+ }
109561
+ try {
109562
+ let hdrRgb = buf.data;
109563
+ if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
109564
+ hdrRgb = Buffer.from(buf.data);
109565
+ convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
109566
+ }
109567
+ const viewportMatrix = parseTransformMatrix(el.transform);
109568
+ const br = el.borderRadius;
109569
+ const hasBorderRadius = br[0] > 0 || br[1] > 0 || br[2] > 0 || br[3] > 0;
109570
+ const borderRadiusParam = hasBorderRadius ? br : void 0;
109571
+ if (viewportMatrix) {
109572
+ blitRgb48leAffine(
109573
+ canvas,
109574
+ hdrRgb,
109575
+ viewportMatrix,
109576
+ buf.width,
109577
+ buf.height,
109578
+ width,
109579
+ height,
109580
+ el.opacity < 0.999 ? el.opacity : void 0,
109581
+ borderRadiusParam
109582
+ );
109583
+ } else {
109584
+ blitRgb48leRegion(
109585
+ canvas,
109586
+ hdrRgb,
109587
+ el.x,
109588
+ el.y,
109589
+ buf.width,
109590
+ buf.height,
109591
+ width,
109592
+ height,
109593
+ el.opacity < 0.999 ? el.opacity : void 0,
109594
+ borderRadiusParam
109595
+ );
109596
+ }
109597
+ } catch (err) {
109598
+ if (log) {
109599
+ log.debug(`HDR image blit failed for ${el.id}`, {
109600
+ error: err instanceof Error ? err.message : String(err)
109601
+ });
109602
+ }
109603
+ }
109604
+ }
109270
109605
  function createRenderJob(config2) {
109271
109606
  return {
109272
109607
  id: randomUUID(),
@@ -109318,6 +109653,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109318
109653
  let lastBrowserConsole = [];
109319
109654
  let restoreLogger = null;
109320
109655
  const perfStages = {};
109656
+ const hdrDiagnostics = {
109657
+ videoExtractionFailures: 0,
109658
+ imageDecodeFailures: 0
109659
+ };
109321
109660
  const perfOutputPath = join15(workDir, "perf-summary.json");
109322
109661
  const cfg = { ...job.config.producerConfig ?? resolveConfig() };
109323
109662
  const outputFormat = job.config.format ?? "mp4";
@@ -109349,7 +109688,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109349
109688
  throw new Error(`Entry file not found: ${htmlPath}`);
109350
109689
  }
109351
109690
  assertNotAborted();
109352
- const rawEntry = readFileSync9(htmlPath, "utf-8");
109691
+ const rawEntry = readFileSync10(htmlPath, "utf-8");
109353
109692
  if (entryFile !== "index.html" && rawEntry.trimStart().startsWith("<template")) {
109354
109693
  const wrapperPath = join15(workDir, "standalone-entry.html");
109355
109694
  const projectIndexPath = join15(projectDir, "index.html");
@@ -109359,7 +109698,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109359
109698
  );
109360
109699
  }
109361
109700
  const standaloneHtml = extractStandaloneEntryFromIndex(
109362
- readFileSync9(projectIndexPath, "utf-8"),
109701
+ readFileSync10(projectIndexPath, "utf-8"),
109363
109702
  entryFile
109364
109703
  );
109365
109704
  if (!standaloneHtml) {
@@ -109394,6 +109733,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109394
109733
  duration: compiled.staticDuration,
109395
109734
  videos: compiled.videos,
109396
109735
  audios: compiled.audios,
109736
+ images: compiled.images,
109397
109737
  width: compiled.width,
109398
109738
  height: compiled.height
109399
109739
  };
@@ -109458,6 +109798,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109458
109798
  assertNotAborted();
109459
109799
  composition.videos = compiled.videos;
109460
109800
  composition.audios = compiled.audios;
109801
+ composition.images = compiled.images;
109461
109802
  writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
109462
109803
  }
109463
109804
  }
@@ -109613,6 +109954,30 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109613
109954
  })
109614
109955
  );
109615
109956
  }
109957
+ const nativeHdrImageIds = /* @__PURE__ */ new Set();
109958
+ const imageTransfers = /* @__PURE__ */ new Map();
109959
+ const hdrImageSrcPaths = /* @__PURE__ */ new Map();
109960
+ const imageColorSpaces = [];
109961
+ if (job.config.hdr && composition.images.length > 0) {
109962
+ const probed = await Promise.all(
109963
+ composition.images.map(async (img) => {
109964
+ let imgPath = img.src;
109965
+ if (!imgPath.startsWith("/")) {
109966
+ const fromCompiled = existsSync15(join15(compiledDir, imgPath)) ? join15(compiledDir, imgPath) : join15(projectDir, imgPath);
109967
+ imgPath = fromCompiled;
109968
+ }
109969
+ if (!existsSync15(imgPath)) return null;
109970
+ const meta = await extractVideoMetadata(imgPath);
109971
+ if (isHdrColorSpace(meta.colorSpace)) {
109972
+ nativeHdrImageIds.add(img.id);
109973
+ imageTransfers.set(img.id, detectTransfer(meta.colorSpace));
109974
+ hdrImageSrcPaths.set(img.id, imgPath);
109975
+ }
109976
+ return meta.colorSpace;
109977
+ })
109978
+ );
109979
+ imageColorSpaces.push(...probed);
109980
+ }
109616
109981
  if (composition.videos.length > 0) {
109617
109982
  extractionResult = await extractAllVideoFrames(
109618
109983
  composition.videos,
@@ -109650,21 +110015,22 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109650
110015
  perfStages.videoExtractMs = Date.now() - stage2Start;
109651
110016
  }
109652
110017
  let effectiveHdr;
109653
- if (job.config.hdr && frameLookup) {
109654
- const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
109655
- const info = analyzeCompositionHdr(colorSpaces);
109656
- if (info.hasHdr && info.dominantTransfer) {
109657
- effectiveHdr = { transfer: info.dominantTransfer };
109658
- }
109659
- }
109660
- if (job.config.hdr && !effectiveHdr && nativeHdrVideoIds.size > 0) {
109661
- const firstTransfer = videoTransfers.values().next().value;
109662
- if (firstTransfer) {
109663
- effectiveHdr = { transfer: firstTransfer };
110018
+ if (job.config.hdr) {
110019
+ const videoColorSpaces = (extractionResult?.extracted ?? []).map(
110020
+ (ext) => ext.metadata.colorSpace
110021
+ );
110022
+ const allColorSpaces = [...videoColorSpaces, ...imageColorSpaces];
110023
+ if (allColorSpaces.length > 0) {
110024
+ const info = analyzeCompositionHdr(allColorSpaces);
110025
+ if (info.hasHdr && info.dominantTransfer) {
110026
+ effectiveHdr = { transfer: info.dominantTransfer };
110027
+ }
109664
110028
  }
109665
110029
  }
109666
110030
  if (effectiveHdr && outputFormat !== "mp4") {
109667
- log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
110031
+ log.warn(
110032
+ `[Render] HDR source detected but format is ${outputFormat} \u2014 falling back to SDR. Use --format mp4 for HDR10 output.`
110033
+ );
109668
110034
  effectiveHdr = void 0;
109669
110035
  }
109670
110036
  if (effectiveHdr) {
@@ -109717,12 +110083,14 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109717
110083
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
109718
110084
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
109719
110085
  const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
109720
- const hasHdrContent = effectiveHdr && nativeHdrVideoIds.size > 0;
110086
+ const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
110087
+ const hasHdrContent = effectiveHdr && nativeHdrIds.size > 0;
109721
110088
  const encoderHdr = hasHdrContent ? effectiveHdr : void 0;
109722
110089
  const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
109723
110090
  job.framesRendered = 0;
109724
110091
  if (hasHdrContent) {
109725
110092
  log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
110093
+ cfg.forceScreenshot = true;
109726
110094
  const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
109727
110095
  const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
109728
110096
  for (const v of composition.videos) {
@@ -109738,7 +110106,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109738
110106
  const domSession = await createCaptureSession(
109739
110107
  fileServer.url,
109740
110108
  framesDir,
109741
- captureOptions,
110109
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
109742
110110
  createVideoFrameInjector(frameLookup),
109743
110111
  cfg
109744
110112
  );
@@ -109792,13 +110160,22 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109792
110160
  );
109793
110161
  assertNotAborted();
109794
110162
  const hdrExtractionDims = /* @__PURE__ */ new Map();
110163
+ const hdrImageFitInfo = /* @__PURE__ */ new Map();
109795
110164
  const hdrVideoStartTimes = /* @__PURE__ */ new Map();
109796
110165
  for (const v of composition.videos) {
109797
110166
  if (hdrVideoIds.includes(v.id)) {
109798
110167
  hdrVideoStartTimes.set(v.id, v.start);
109799
110168
  }
109800
110169
  }
109801
- const uniqueStartTimes = [...new Set(hdrVideoStartTimes.values())].sort((a, b) => a - b);
110170
+ const hdrImageStartTimes = /* @__PURE__ */ new Map();
110171
+ for (const img of composition.images) {
110172
+ if (nativeHdrImageIds.has(img.id)) {
110173
+ hdrImageStartTimes.set(img.id, img.start);
110174
+ }
110175
+ }
110176
+ const uniqueStartTimes = [
110177
+ .../* @__PURE__ */ new Set([...hdrVideoStartTimes.values(), ...hdrImageStartTimes.values()])
110178
+ ].sort((a, b) => a - b);
109802
110179
  for (const seekTime of uniqueStartTimes) {
109803
110180
  await domSession.page.evaluate((t) => {
109804
110181
  if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t);
@@ -109806,11 +110183,17 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109806
110183
  if (domSession.onBeforeCapture) {
109807
110184
  await domSession.onBeforeCapture(domSession.page, seekTime);
109808
110185
  }
109809
- const stacking = await queryElementStacking(domSession.page, nativeHdrVideoIds);
110186
+ const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
109810
110187
  for (const el of stacking) {
109811
110188
  if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !hdrExtractionDims.has(el.id)) {
109812
110189
  hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
109813
110190
  }
110191
+ if (el.isHdr && nativeHdrImageIds.has(el.id) && !hdrImageFitInfo.has(el.id)) {
110192
+ hdrImageFitInfo.set(el.id, {
110193
+ fit: el.objectFit,
110194
+ position: el.objectPosition
110195
+ });
110196
+ }
109814
110197
  }
109815
110198
  }
109816
110199
  const hdrFrameDirs = /* @__PURE__ */ new Map();
@@ -109841,14 +110224,59 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109841
110224
  ];
109842
110225
  const result = await runFfmpeg(ffmpegArgs, { signal: abortSignal });
109843
110226
  if (!result.success) {
109844
- log.warn("HDR frame pre-extraction failed; loop will fill with black", {
110227
+ hdrDiagnostics.videoExtractionFailures += 1;
110228
+ log.error("HDR frame pre-extraction failed; aborting render", {
109845
110229
  videoId,
109846
110230
  srcPath,
109847
110231
  stderr: result.stderr.slice(-400)
109848
110232
  });
110233
+ throw new Error(
110234
+ `HDR frame extraction failed for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
110235
+ );
109849
110236
  }
109850
110237
  hdrFrameDirs.set(videoId, frameDir);
109851
110238
  }
110239
+ const hdrImageBuffers = /* @__PURE__ */ new Map();
110240
+ for (const [imageId, srcPath] of hdrImageSrcPaths) {
110241
+ try {
110242
+ const decoded = decodePngToRgb48le(readFileSync10(srcPath));
110243
+ const layout2 = hdrExtractionDims.get(imageId);
110244
+ const fitInfo = hdrImageFitInfo.get(imageId);
110245
+ if (layout2 && (layout2.width !== decoded.width || layout2.height !== decoded.height)) {
110246
+ const fit = normalizeObjectFit(fitInfo?.fit);
110247
+ const resampled = resampleRgb48leObjectFit(
110248
+ decoded.data,
110249
+ decoded.width,
110250
+ decoded.height,
110251
+ layout2.width,
110252
+ layout2.height,
110253
+ fit,
110254
+ fitInfo?.position
110255
+ );
110256
+ hdrImageBuffers.set(imageId, {
110257
+ data: resampled,
110258
+ width: layout2.width,
110259
+ height: layout2.height
110260
+ });
110261
+ } else {
110262
+ hdrImageBuffers.set(imageId, {
110263
+ data: Buffer.from(decoded.data),
110264
+ width: decoded.width,
110265
+ height: decoded.height
110266
+ });
110267
+ }
110268
+ } catch (err) {
110269
+ hdrDiagnostics.imageDecodeFailures += 1;
110270
+ log.error("HDR image decode failed; aborting render", {
110271
+ imageId,
110272
+ srcPath,
110273
+ error: err instanceof Error ? err.message : String(err)
110274
+ });
110275
+ throw new Error(
110276
+ `HDR image decode failed for image "${imageId}". Aborting render to avoid shipping missing HDR image layers.`
110277
+ );
110278
+ }
110279
+ }
109852
110280
  assertNotAborted();
109853
110281
  try {
109854
110282
  let countNonZeroAlpha2 = function(rgba) {
@@ -109906,38 +110334,67 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
109906
110334
  const layer = layers[layerIdx];
109907
110335
  if (layer.type === "hdr") {
109908
110336
  const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
109909
- blitHdrVideoLayer(
109910
- canvas,
109911
- layer.element,
109912
- time,
109913
- job.config.fps,
109914
- hdrFrameDirs,
109915
- hdrVideoStartTimes,
109916
- width,
109917
- height,
109918
- log,
109919
- videoTransfers.get(layer.element.id),
109920
- effectiveHdr?.transfer
109921
- );
110337
+ const isHdrImage = nativeHdrImageIds.has(layer.element.id);
110338
+ if (isHdrImage) {
110339
+ blitHdrImageLayer(
110340
+ canvas,
110341
+ layer.element,
110342
+ hdrImageBuffers,
110343
+ width,
110344
+ height,
110345
+ log,
110346
+ imageTransfers.get(layer.element.id),
110347
+ effectiveHdr?.transfer
110348
+ );
110349
+ } else {
110350
+ blitHdrVideoLayer(
110351
+ canvas,
110352
+ layer.element,
110353
+ time,
110354
+ job.config.fps,
110355
+ hdrFrameDirs,
110356
+ hdrVideoStartTimes,
110357
+ width,
110358
+ height,
110359
+ log,
110360
+ videoTransfers.get(layer.element.id),
110361
+ effectiveHdr?.transfer
110362
+ );
110363
+ }
109922
110364
  if (shouldLog) {
109923
110365
  const after2 = countNonZeroRgb482(canvas);
109924
- const frameDir = hdrFrameDirs.get(layer.element.id);
109925
- const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
109926
- const localTime = time - startTime;
109927
- const frameNum = Math.floor(localTime * job.config.fps) + 1;
109928
- const expectedFrame = frameDir ? join15(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
109929
- log.info("[diag] hdr layer blit", {
109930
- frame: debugFrameIndex,
109931
- layerIdx,
109932
- id: layer.element.id,
109933
- pixelsAdded: after2 - before2,
109934
- totalNonZero: after2,
109935
- startTime,
109936
- localTime: localTime.toFixed(3),
109937
- hdrFrameNum: frameNum,
109938
- expectedFrame,
109939
- expectedFrameExists: expectedFrame ? existsSync15(expectedFrame) : false
109940
- });
110366
+ if (isHdrImage) {
110367
+ const buf = hdrImageBuffers.get(layer.element.id);
110368
+ log.info("[diag] hdr layer blit", {
110369
+ frame: debugFrameIndex,
110370
+ layerIdx,
110371
+ id: layer.element.id,
110372
+ kind: "image",
110373
+ pixelsAdded: after2 - before2,
110374
+ totalNonZero: after2,
110375
+ bufferDecoded: !!buf,
110376
+ bufferDims: buf ? `${buf.width}x${buf.height}` : null
110377
+ });
110378
+ } else {
110379
+ const frameDir = hdrFrameDirs.get(layer.element.id);
110380
+ const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
110381
+ const localTime = time - startTime;
110382
+ const frameNum = Math.floor(localTime * job.config.fps) + 1;
110383
+ const expectedFrame = frameDir ? join15(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
110384
+ log.info("[diag] hdr layer blit", {
110385
+ frame: debugFrameIndex,
110386
+ layerIdx,
110387
+ id: layer.element.id,
110388
+ kind: "video",
110389
+ pixelsAdded: after2 - before2,
110390
+ totalNonZero: after2,
110391
+ startTime,
110392
+ localTime: localTime.toFixed(3),
110393
+ hdrFrameNum: frameNum,
110394
+ expectedFrame,
110395
+ expectedFrameExists: expectedFrame ? existsSync15(expectedFrame) : false
110396
+ });
110397
+ }
109941
110398
  }
109942
110399
  } else {
109943
110400
  const allElementIds = fullStacking.map((e) => e.id);
@@ -110012,7 +110469,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110012
110469
  if (beforeCaptureHook) {
110013
110470
  await beforeCaptureHook(domSession.page, time);
110014
110471
  }
110015
- const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
110472
+ const stackingInfo = await queryElementStacking(domSession.page, nativeHdrIds);
110016
110473
  const activeTransition = transitionRanges.find(
110017
110474
  (t) => i >= t.startFrame && i <= t.endFrame
110018
110475
  );
@@ -110044,22 +110501,35 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110044
110501
  }
110045
110502
  for (const el of stackingInfo) {
110046
110503
  if (!el.isHdr || !sceneIds.has(el.id)) continue;
110047
- blitHdrVideoLayer(
110048
- sceneBuf,
110049
- el,
110050
- time,
110051
- job.config.fps,
110052
- hdrFrameDirs,
110053
- hdrVideoStartTimes,
110054
- width,
110055
- height,
110056
- log,
110057
- videoTransfers.get(el.id),
110058
- effectiveHdr?.transfer
110059
- );
110504
+ if (nativeHdrImageIds.has(el.id)) {
110505
+ blitHdrImageLayer(
110506
+ sceneBuf,
110507
+ el,
110508
+ hdrImageBuffers,
110509
+ width,
110510
+ height,
110511
+ log,
110512
+ imageTransfers.get(el.id),
110513
+ effectiveHdr?.transfer
110514
+ );
110515
+ } else {
110516
+ blitHdrVideoLayer(
110517
+ sceneBuf,
110518
+ el,
110519
+ time,
110520
+ job.config.fps,
110521
+ hdrFrameDirs,
110522
+ hdrVideoStartTimes,
110523
+ width,
110524
+ height,
110525
+ log,
110526
+ videoTransfers.get(el.id),
110527
+ effectiveHdr?.transfer
110528
+ );
110529
+ }
110060
110530
  }
110061
110531
  const showIds = Array.from(sceneIds);
110062
- const hideIds = stackingInfo.map((e) => e.id).filter((id) => !sceneIds.has(id) || nativeHdrVideoIds.has(id));
110532
+ const hideIds = stackingInfo.map((e) => e.id).filter((id) => !sceneIds.has(id) || nativeHdrIds.has(id));
110063
110533
  await applyDomLayerMask(domSession.page, showIds, hideIds);
110064
110534
  const domPng = await captureAlphaPng(domSession.page, width, height);
110065
110535
  await removeDomLayerMask(domSession.page, hideIds);
@@ -110180,7 +110650,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110180
110650
  fileServer.url,
110181
110651
  workDir,
110182
110652
  tasks,
110183
- captureOptions,
110653
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
110184
110654
  () => createVideoFrameInjector(frameLookup),
110185
110655
  abortSignal,
110186
110656
  (progress) => {
@@ -110210,7 +110680,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110210
110680
  const session = probeSession ?? await createCaptureSession(
110211
110681
  fileServer.url,
110212
110682
  framesDir,
110213
- captureOptions,
110683
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
110214
110684
  videoInjector,
110215
110685
  cfg
110216
110686
  );
@@ -110261,7 +110731,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110261
110731
  fileServer.url,
110262
110732
  workDir,
110263
110733
  tasks,
110264
- captureOptions,
110734
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
110265
110735
  () => createVideoFrameInjector(frameLookup),
110266
110736
  abortSignal,
110267
110737
  (progress) => {
@@ -110292,7 +110762,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110292
110762
  const session = probeSession ?? await createCaptureSession(
110293
110763
  fileServer.url,
110294
110764
  framesDir,
110295
- captureOptions,
110765
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
110296
110766
  videoInjector,
110297
110767
  cfg
110298
110768
  );
@@ -110410,6 +110880,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110410
110880
  videoCount: composition.videos.length,
110411
110881
  audioCount: composition.audios.length,
110412
110882
  stages: perfStages,
110883
+ hdrDiagnostics: hdrDiagnostics.videoExtractionFailures > 0 || hdrDiagnostics.imageDecodeFailures > 0 ? { ...hdrDiagnostics } : void 0,
110413
110884
  captureAvgMs: totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / totalFrames) : void 0
110414
110885
  };
110415
110886
  job.perfSummary = perfSummary;
@@ -110490,7 +110961,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110490
110961
  elapsedMs: elapsed,
110491
110962
  freeMemoryMB: freeMemMB,
110492
110963
  browserConsoleTail: lastBrowserConsole.length > 0 ? lastBrowserConsole.slice(-30) : void 0,
110493
- perfStages: Object.keys(perfStages).length > 0 ? { ...perfStages } : void 0
110964
+ perfStages: Object.keys(perfStages).length > 0 ? { ...perfStages } : void 0,
110965
+ hdrDiagnostics: hdrDiagnostics.videoExtractionFailures > 0 || hdrDiagnostics.imageDecodeFailures > 0 ? { ...hdrDiagnostics } : void 0
110494
110966
  };
110495
110967
  if (fileServer) {
110496
110968
  const fs8 = fileServer;
@@ -110521,7 +110993,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
110521
110993
  }
110522
110994
 
110523
110995
  // src/services/hyperframeLint.ts
110524
- import { existsSync as existsSync16, readFileSync as readFileSync10, statSync as statSync6 } from "node:fs";
110996
+ import { existsSync as existsSync16, readFileSync as readFileSync11, statSync as statSync6 } from "node:fs";
110525
110997
  import { resolve as resolve11, join as join16 } from "node:path";
110526
110998
  function isStringRecord(value) {
110527
110999
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -110564,7 +111036,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
110564
111036
  if (existsSync16(absoluteEntryPath) && statSync6(absoluteEntryPath).isFile()) {
110565
111037
  return {
110566
111038
  entryFile,
110567
- html: readFileSync10(absoluteEntryPath, "utf-8"),
111039
+ html: readFileSync11(absoluteEntryPath, "utf-8"),
110568
111040
  source: "projectDir"
110569
111041
  };
110570
111042
  }