@hyperframes/producer 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57866,7 +57866,7 @@ var require_util2 = __commonJS({
57866
57866
  }
57867
57867
  path12 = url.path;
57868
57868
  }
57869
- var isAbsolute2 = exports.isAbsolute(path12);
57869
+ var isAbsolute3 = exports.isAbsolute(path12);
57870
57870
  var parts = path12.split(/\/+/);
57871
57871
  for (var part, up = 0, i = parts.length - 1; i >= 0; i--) {
57872
57872
  part = parts[i];
@@ -57886,7 +57886,7 @@ var require_util2 = __commonJS({
57886
57886
  }
57887
57887
  path12 = parts.join("/");
57888
57888
  if (path12 === "") {
57889
- path12 = isAbsolute2 ? "/" : ".";
57889
+ path12 = isAbsolute3 ? "/" : ".";
57890
57890
  }
57891
57891
  if (url) {
57892
57892
  url.path = path12;
@@ -57931,7 +57931,7 @@ var require_util2 = __commonJS({
57931
57931
  exports.isAbsolute = function(aPath) {
57932
57932
  return aPath.charAt(0) === "/" || urlRegexp.test(aPath);
57933
57933
  };
57934
- function relative2(aRoot, aPath) {
57934
+ function relative3(aRoot, aPath) {
57935
57935
  if (aRoot === "") {
57936
57936
  aRoot = ".";
57937
57937
  }
@@ -57950,7 +57950,7 @@ var require_util2 = __commonJS({
57950
57950
  }
57951
57951
  return Array(level + 1).join("../") + aPath.substr(aRoot.length + 1);
57952
57952
  }
57953
- exports.relative = relative2;
57953
+ exports.relative = relative3;
57954
57954
  var supportsNullProto = (function() {
57955
57955
  var obj = /* @__PURE__ */ Object.create(null);
57956
57956
  return !("__proto__" in obj);
@@ -95503,10 +95503,10 @@ function compareDocumentPosition(nodeA, nodeB) {
95503
95503
  function uniqueSort(nodes) {
95504
95504
  nodes = nodes.filter((node, i, arr) => !arr.includes(node, i + 1));
95505
95505
  nodes.sort((a, b) => {
95506
- const relative2 = compareDocumentPosition(a, b);
95507
- if (relative2 & DocumentPosition.PRECEDING) {
95506
+ const relative3 = compareDocumentPosition(a, b);
95507
+ if (relative3 & DocumentPosition.PRECEDING) {
95508
95508
  return -1;
95509
- } else if (relative2 & DocumentPosition.FOLLOWING) {
95509
+ } else if (relative3 & DocumentPosition.FOLLOWING) {
95510
95510
  return 1;
95511
95511
  }
95512
95512
  return 0;
@@ -103860,6 +103860,15 @@ function isFontResourceError(type, text, locationUrl) {
103860
103860
  `${locationUrl} ${text}`
103861
103861
  );
103862
103862
  }
103863
+ async function pollPageExpression(page, expression, timeoutMs, intervalMs = 100) {
103864
+ const deadline = Date.now() + timeoutMs;
103865
+ while (Date.now() < deadline) {
103866
+ const ready = Boolean(await page.evaluate(expression));
103867
+ if (ready) return true;
103868
+ await new Promise((resolve13) => setTimeout(resolve13, intervalMs));
103869
+ }
103870
+ return Boolean(await page.evaluate(expression));
103871
+ }
103863
103872
  async function initializeSession(session) {
103864
103873
  const { page, serverUrl } = session;
103865
103874
  page.on("console", (msg) => {
@@ -103889,14 +103898,26 @@ async function initializeSession(session) {
103889
103898
  if (session.captureMode === "screenshot") {
103890
103899
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
103891
103900
  const pageReadyTimeout2 = session.config?.playerReadyTimeout ?? DEFAULT_CONFIG.playerReadyTimeout;
103892
- await page.waitForFunction(
103901
+ const pageReady2 = await pollPageExpression(
103902
+ page,
103893
103903
  `!!(window.__hf && typeof window.__hf.seek === "function" && window.__hf.duration > 0)`,
103894
- { timeout: pageReadyTimeout2 }
103904
+ pageReadyTimeout2
103895
103905
  );
103896
- await page.waitForFunction(
103906
+ if (!pageReady2) {
103907
+ throw new Error(
103908
+ `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
103909
+ );
103910
+ }
103911
+ const videosReady = await pollPageExpression(
103912
+ page,
103897
103913
  `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`,
103898
- { timeout: pageReadyTimeout2 }
103914
+ pageReadyTimeout2
103899
103915
  );
103916
+ if (!videosReady) {
103917
+ throw new Error(
103918
+ `[FrameCapture] video metadata not ready after ${pageReadyTimeout2}ms. Video elements must load metadata before capture starts.`
103919
+ );
103920
+ }
103900
103921
  await page.evaluate(`document.fonts?.ready`);
103901
103922
  session.isInitialized = true;
103902
103923
  return;
@@ -105931,7 +105952,7 @@ async function mergeWorkerFrames(workDir, tasks, outputDir) {
105931
105952
  }
105932
105953
 
105933
105954
  // src/services/renderOrchestrator.ts
105934
- import { join as join14, dirname as dirname10, resolve as resolve9 } from "path";
105955
+ import { join as join15, dirname as dirname10, resolve as resolve10 } from "path";
105935
105956
  import { randomUUID } from "crypto";
105936
105957
  import { freemem as freemem2 } from "os";
105937
105958
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -106036,6 +106057,102 @@ var MIME_TYPES = {
106036
106057
  ".ttf": "font/ttf",
106037
106058
  ".otf": "font/otf"
106038
106059
  };
106060
+ var VIRTUAL_TIME_SHIM = String.raw`(function() {
106061
+ if (window.__HF_VIRTUAL_TIME__) return;
106062
+
106063
+ var virtualNowMs = 0;
106064
+ var rafId = 1;
106065
+ var rafQueue = [];
106066
+ var OriginalDate = Date;
106067
+ var originalSetTimeout = window.setTimeout.bind(window);
106068
+ var originalClearTimeout = window.clearTimeout.bind(window);
106069
+ var originalSetInterval = window.setInterval.bind(window);
106070
+ var originalClearInterval = window.clearInterval.bind(window);
106071
+ var originalRequestAnimationFrame = window.requestAnimationFrame
106072
+ ? window.requestAnimationFrame.bind(window)
106073
+ : null;
106074
+ var originalCancelAnimationFrame = window.cancelAnimationFrame
106075
+ ? window.cancelAnimationFrame.bind(window)
106076
+ : null;
106077
+
106078
+ function flushAnimationFrame() {
106079
+ if (!rafQueue.length) return;
106080
+ var current = rafQueue.slice();
106081
+ rafQueue.length = 0;
106082
+ for (var i = 0; i < current.length; i++) {
106083
+ var entry = current[i];
106084
+ if (entry.cancelled) continue;
106085
+ try {
106086
+ entry.callback(virtualNowMs);
106087
+ } catch {}
106088
+ }
106089
+ }
106090
+
106091
+ function VirtualDate() {
106092
+ var args = Array.prototype.slice.call(arguments);
106093
+ if (!(this instanceof VirtualDate)) {
106094
+ return OriginalDate.apply(null, args.length ? args : [virtualNowMs]);
106095
+ }
106096
+ var instance = args.length ? new (Function.prototype.bind.apply(OriginalDate, [null].concat(args)))() : new OriginalDate(virtualNowMs);
106097
+ Object.setPrototypeOf(instance, VirtualDate.prototype);
106098
+ return instance;
106099
+ }
106100
+
106101
+ VirtualDate.prototype = OriginalDate.prototype;
106102
+ Object.setPrototypeOf(VirtualDate, OriginalDate);
106103
+ VirtualDate.now = function() { return virtualNowMs; };
106104
+ VirtualDate.parse = OriginalDate.parse.bind(OriginalDate);
106105
+ VirtualDate.UTC = OriginalDate.UTC.bind(OriginalDate);
106106
+
106107
+ try {
106108
+ Object.defineProperty(window, "Date", {
106109
+ configurable: true,
106110
+ writable: true,
106111
+ value: VirtualDate,
106112
+ });
106113
+ } catch {}
106114
+
106115
+ if (window.performance && typeof window.performance.now === "function") {
106116
+ try {
106117
+ Object.defineProperty(window.performance, "now", {
106118
+ configurable: true,
106119
+ value: function() { return virtualNowMs; },
106120
+ });
106121
+ } catch {}
106122
+ }
106123
+
106124
+ window.requestAnimationFrame = function(callback) {
106125
+ if (typeof callback !== "function") return 0;
106126
+ var entry = { id: rafId++, callback: callback, cancelled: false };
106127
+ rafQueue.push(entry);
106128
+ return entry.id;
106129
+ };
106130
+ window.cancelAnimationFrame = function(id) {
106131
+ for (var i = 0; i < rafQueue.length; i++) {
106132
+ if (rafQueue[i].id === id) {
106133
+ rafQueue[i].cancelled = true;
106134
+ }
106135
+ }
106136
+ };
106137
+
106138
+ window.__HF_VIRTUAL_TIME__ = {
106139
+ originalSetTimeout: originalSetTimeout,
106140
+ originalClearTimeout: originalClearTimeout,
106141
+ originalSetInterval: originalSetInterval,
106142
+ originalClearInterval: originalClearInterval,
106143
+ originalRequestAnimationFrame: originalRequestAnimationFrame,
106144
+ originalCancelAnimationFrame: originalCancelAnimationFrame,
106145
+ seekToTime: function(nextTimeMs) {
106146
+ var safeTimeMs = Math.max(0, Number(nextTimeMs) || 0);
106147
+ virtualNowMs = safeTimeMs;
106148
+ flushAnimationFrame();
106149
+ return virtualNowMs;
106150
+ },
106151
+ getTime: function() {
106152
+ return virtualNowMs;
106153
+ },
106154
+ };
106155
+ })();`;
106039
106156
  var RENDER_SEEK_MODE = process.env.PRODUCER_RUNTIME_RENDER_SEEK_MODE === "strict-boundary" ? "strict-boundary" : "preview-phase";
106040
106157
  var RENDER_SEEK_DIAGNOSTICS = process.env.PRODUCER_DEBUG_SEEK_DIAGNOSTICS === "true";
106041
106158
  var RENDER_SEEK_STEP = Math.max(
@@ -106047,6 +106164,10 @@ var RENDER_SEEK_OFFSET_FRACTION = Math.max(
106047
106164
  Math.min(0.95, Number(process.env.PRODUCER_RUNTIME_RENDER_SEEK_OFFSET_FRACTION || 0.5))
106048
106165
  );
106049
106166
  var RENDER_MODE_SCRIPT = `(function() {
106167
+ var __realSetTimeout =
106168
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetTimeout === "function"
106169
+ ? window.__HF_VIRTUAL_TIME__.originalSetTimeout
106170
+ : window.setTimeout.bind(window);
106050
106171
  var __seekMode = ${JSON.stringify(RENDER_SEEK_MODE)};
106051
106172
  var __seekDiagnostics = ${RENDER_SEEK_DIAGNOSTICS ? "true" : "false"};
106052
106173
  var __seekStep = ${RENDER_SEEK_STEP};
@@ -106140,23 +106261,56 @@ var RENDER_MODE_SCRIPT = `(function() {
106140
106261
  window.__renderReady = true;
106141
106262
  return;
106142
106263
  }
106143
- setTimeout(waitForPlayer, 50);
106264
+ __realSetTimeout(waitForPlayer, 50);
106144
106265
  return;
106145
106266
  }
106146
106267
  if (installMediaFallbackPlayer()) {
106147
106268
  return;
106148
106269
  }
106149
- setTimeout(waitForPlayer, 50);
106270
+ __realSetTimeout(waitForPlayer, 50);
106150
106271
  }
106151
106272
  waitForPlayer();
106152
106273
  })();`;
106153
106274
  var HF_BRIDGE_SCRIPT = `(function() {
106275
+ var __realSetInterval =
106276
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetInterval === "function"
106277
+ ? window.__HF_VIRTUAL_TIME__.originalSetInterval
106278
+ : window.setInterval.bind(window);
106279
+ var __realClearInterval =
106280
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalClearInterval === "function"
106281
+ ? window.__HF_VIRTUAL_TIME__.originalClearInterval
106282
+ : window.clearInterval.bind(window);
106154
106283
  function getDeclaredDuration() {
106155
106284
  var root = document.querySelector('[data-composition-id]');
106156
106285
  if (!root) return 0;
106157
106286
  var d = Number(root.getAttribute('data-duration'));
106158
106287
  return Number.isFinite(d) && d > 0 ? d : 0;
106159
106288
  }
106289
+ function seekSameOriginChildFrames(frameWindow, nextTimeMs) {
106290
+ var frames;
106291
+ try {
106292
+ frames = frameWindow.frames;
106293
+ } catch (_error) {
106294
+ return;
106295
+ }
106296
+ if (!frames || typeof frames.length !== "number") return;
106297
+ for (var i = 0; i < frames.length; i++) {
106298
+ var childWindow = null;
106299
+ try {
106300
+ childWindow = frames[i];
106301
+ if (!childWindow || childWindow === frameWindow) continue;
106302
+ if (
106303
+ childWindow.__HF_VIRTUAL_TIME__ &&
106304
+ typeof childWindow.__HF_VIRTUAL_TIME__.seekToTime === "function"
106305
+ ) {
106306
+ childWindow.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
106307
+ }
106308
+ } catch (_error) {
106309
+ continue;
106310
+ }
106311
+ seekSameOriginChildFrames(childWindow, nextTimeMs);
106312
+ }
106313
+ }
106160
106314
  function bridge() {
106161
106315
  var p = window.__player;
106162
106316
  if (!p || typeof p.renderSeek !== "function" || typeof p.getDuration !== "function") {
@@ -106167,13 +106321,20 @@ var HF_BRIDGE_SCRIPT = `(function() {
106167
106321
  var d = p.getDuration();
106168
106322
  return d > 0 ? d : getDeclaredDuration();
106169
106323
  },
106170
- seek: function(t) { p.renderSeek(t); },
106324
+ seek: function(t) {
106325
+ p.renderSeek(t);
106326
+ var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
106327
+ if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
106328
+ window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
106329
+ }
106330
+ seekSameOriginChildFrames(window, nextTimeMs);
106331
+ },
106171
106332
  };
106172
106333
  return true;
106173
106334
  }
106174
106335
  if (bridge()) return;
106175
- var iv = setInterval(function() {
106176
- if (bridge()) clearInterval(iv);
106336
+ var iv = __realSetInterval(function() {
106337
+ if (bridge()) __realClearInterval(iv);
106177
106338
  }, 50);
106178
106339
  })();`;
106179
106340
  function stripEmbeddedRuntimeScripts(html) {
@@ -106235,8 +106396,22 @@ function injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbedded) {
106235
106396
  }
106236
106397
  return html;
106237
106398
  }
106399
+ function injectScriptsAtHeadStart(html, scripts) {
106400
+ if (scripts.length === 0) return html;
106401
+ const headTags = scripts.map((src) => `<script>${src}</script>`).join("\n");
106402
+ if (html.includes("<head")) {
106403
+ return html.replace(/<head\b[^>]*>/i, (match2) => `${match2}
106404
+ ${headTags}`);
106405
+ }
106406
+ if (html.includes("<body")) {
106407
+ return html.replace("<body", () => `${headTags}
106408
+ <body`);
106409
+ }
106410
+ return headTags + "\n" + html;
106411
+ }
106238
106412
  function createFileServer2(options) {
106239
106413
  const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
106414
+ const preHeadScripts = options.preHeadScripts ?? [];
106240
106415
  const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
106241
106416
  const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
106242
106417
  const app = new Hono2();
@@ -106260,7 +106435,11 @@ function createFileServer2(options) {
106260
106435
  if (ext === ".html") {
106261
106436
  const rawHtml = readFileSync6(filePath, "utf-8");
106262
106437
  const isIndex = relativePath === "index.html";
106263
- const html = isIndex ? injectScriptsIntoHtml(rawHtml, headScripts, bodyScripts, stripEmbeddedRuntime) : rawHtml;
106438
+ let html = rawHtml;
106439
+ if (preHeadScripts.length > 0) {
106440
+ html = injectScriptsAtHeadStart(html, preHeadScripts);
106441
+ }
106442
+ html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
106264
106443
  return c.text(html, 200, { "Content-Type": contentType });
106265
106444
  }
106266
106445
  const content = readFileSync6(filePath);
@@ -106291,13 +106470,41 @@ function createFileServer2(options) {
106291
106470
 
106292
106471
  // src/services/htmlCompiler.ts
106293
106472
  import { readFileSync as readFileSync8, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
106294
- import { join as join13, dirname as dirname9, resolve as resolve8 } from "path";
106473
+ import { join as join14, dirname as dirname9, resolve as resolve9 } from "path";
106295
106474
  import postcss from "postcss";
106296
106475
 
106476
+ // src/utils/paths.ts
106477
+ import { resolve as resolve8, basename as basename2, join as join12, relative as relative2, isAbsolute as isAbsolute2 } from "node:path";
106478
+ var DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? resolve8(new URL(import.meta.url).pathname, "../../..", "renders");
106479
+ function isPathInside(childPath, parentPath) {
106480
+ const absChild = resolve8(childPath);
106481
+ const absParent = resolve8(parentPath);
106482
+ if (absChild === absParent) return true;
106483
+ const rel = relative2(absParent, absChild);
106484
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute2(rel);
106485
+ }
106486
+ function toExternalAssetKey(absPath) {
106487
+ if (absPath.startsWith("hf-ext/")) return absPath;
106488
+ let normalised = absPath.replace(/\\/g, "/");
106489
+ normalised = normalised.replace(/^\/\/\?\/UNC\//i, "//");
106490
+ normalised = normalised.replace(/^\/\/\?\//, "");
106491
+ normalised = normalised.replace(/^\/\/([^/]+)\//, "unc/$1/");
106492
+ normalised = normalised.replace(/^\/+/, "");
106493
+ normalised = normalised.replace(/^([A-Za-z]):\/?/, "$1/");
106494
+ return "hf-ext/" + normalised;
106495
+ }
106496
+ function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS_DIR) {
106497
+ const absoluteProjectDir = resolve8(projectDir);
106498
+ const projectName = basename2(absoluteProjectDir);
106499
+ const resolvedOutputPath = outputPath ?? join12(rendersDir, `${projectName}.mp4`);
106500
+ const absoluteOutputPath = resolve8(resolvedOutputPath);
106501
+ return { absoluteProjectDir, absoluteOutputPath };
106502
+ }
106503
+
106297
106504
  // src/services/deterministicFonts.ts
106298
106505
  import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
106299
106506
  import { homedir as homedir2 } from "node:os";
106300
- import { join as join12 } from "node:path";
106507
+ import { join as join13 } from "node:path";
106301
106508
 
106302
106509
  // src/services/fontData.generated.ts
106303
106510
  var EMBEDDED_FONT_DATA = /* @__PURE__ */ new Map([
@@ -106575,20 +106782,20 @@ function warnUnresolvedFonts(unresolved) {
106575
106782
  Docs: https://hyperframes.heygen.com/docs/fonts`
106576
106783
  );
106577
106784
  }
106578
- var GOOGLE_FONTS_CACHE_DIR = join12(homedir2(), ".cache", "hyperframes", "fonts");
106785
+ var GOOGLE_FONTS_CACHE_DIR = join13(homedir2(), ".cache", "hyperframes", "fonts");
106579
106786
  var WOFF2_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
106580
106787
  function fontSlug(familyName) {
106581
106788
  return familyName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
106582
106789
  }
106583
106790
  function fontCacheDir(slug) {
106584
- const dir = join12(GOOGLE_FONTS_CACHE_DIR, slug);
106791
+ const dir = join13(GOOGLE_FONTS_CACHE_DIR, slug);
106585
106792
  if (!existsSync13(dir)) {
106586
106793
  mkdirSync8(dir, { recursive: true });
106587
106794
  }
106588
106795
  return dir;
106589
106796
  }
106590
106797
  function cachedWoff2Path(slug, weight, style) {
106591
- return join12(fontCacheDir(slug), `${weight}-${style}.woff2`);
106798
+ return join13(fontCacheDir(slug), `${weight}-${style}.woff2`);
106592
106799
  }
106593
106800
  async function fetchGoogleFont(familyName) {
106594
106801
  const slug = fontSlug(familyName);
@@ -106680,6 +106887,37 @@ function dedupeElementsById(elements) {
106680
106887
  }
106681
106888
  return Array.from(deduped.values());
106682
106889
  }
106890
+ var INLINE_SCRIPT_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
106891
+ function stripJsComments(source2) {
106892
+ return source2.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
106893
+ }
106894
+ function detectRenderModeHints(html) {
106895
+ const reasons = [];
106896
+ const { document: document2 } = parseHTML(html);
106897
+ if (document2.querySelector("iframe")) {
106898
+ reasons.push({
106899
+ code: "iframe",
106900
+ message: "Detected <iframe> in the composition DOM. Nested iframe animation is routed through screenshot capture mode for compatibility."
106901
+ });
106902
+ }
106903
+ let scriptMatch;
106904
+ const scriptPattern = new RegExp(INLINE_SCRIPT_PATTERN.source, INLINE_SCRIPT_PATTERN.flags);
106905
+ while ((scriptMatch = scriptPattern.exec(html)) !== null) {
106906
+ const attrs = scriptMatch[1] || "";
106907
+ if (/\bsrc\s*=/i.test(attrs)) continue;
106908
+ const content = stripJsComments(scriptMatch[2] || "");
106909
+ if (!/requestAnimationFrame\s*\(/.test(content)) continue;
106910
+ reasons.push({
106911
+ code: "requestAnimationFrame",
106912
+ message: "Detected raw requestAnimationFrame() in an inline script. This render is routed through screenshot capture mode with virtual time enabled."
106913
+ });
106914
+ break;
106915
+ }
106916
+ return {
106917
+ recommendScreenshot: reasons.length > 0,
106918
+ reasons
106919
+ };
106920
+ }
106683
106921
  async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagName19) {
106684
106922
  let filePath = src;
106685
106923
  if (isHttpUrl(src)) {
@@ -106690,7 +106928,7 @@ async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagNa
106690
106928
  return { duration: 0, resolvedPath: src };
106691
106929
  }
106692
106930
  } else if (!filePath.startsWith("/")) {
106693
- filePath = join13(baseDir, filePath);
106931
+ filePath = join14(baseDir, filePath);
106694
106932
  }
106695
106933
  if (!existsSync14(filePath)) {
106696
106934
  return { duration: 0, resolvedPath: filePath };
@@ -106756,7 +106994,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
106756
106994
  const elEnd = elEndRaw ? parseFloat(elEndRaw) : Infinity;
106757
106995
  const absoluteStart = parentOffset + elStart;
106758
106996
  const absoluteEnd = Math.min(parentEnd, isFinite(elEnd) ? parentOffset + elEnd : Infinity);
106759
- const filePath = resolve8(projectDir, srcPath);
106997
+ const filePath = resolve9(projectDir, srcPath);
106760
106998
  if (visited.has(filePath)) {
106761
106999
  continue;
106762
107000
  }
@@ -106956,7 +107194,7 @@ function inlineSubCompositions(html, subCompositions, projectDir) {
106956
107194
  if (!srcPath) continue;
106957
107195
  let compHtml = subCompositions.get(srcPath) || null;
106958
107196
  if (!compHtml) {
106959
- const filePath = resolve8(projectDir, srcPath);
107197
+ const filePath = resolve9(projectDir, srcPath);
106960
107198
  if (existsSync14(filePath)) {
106961
107199
  compHtml = readFileSync8(filePath, "utf-8");
106962
107200
  }
@@ -107169,7 +107407,7 @@ ${safeText}
107169
107407
  return result;
107170
107408
  }
107171
107409
  function collectExternalAssets(html, projectDir) {
107172
- const absProjectDir = resolve8(projectDir);
107410
+ const absProjectDir = resolve9(projectDir);
107173
107411
  const externalAssets = /* @__PURE__ */ new Map();
107174
107412
  const CSS_URL_RE2 = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
107175
107413
  function processPath(rawPath) {
@@ -107177,12 +107415,12 @@ function collectExternalAssets(html, projectDir) {
107177
107415
  if (!trimmed || trimmed.startsWith("/") || trimmed.startsWith("http://") || trimmed.startsWith("https://") || trimmed.startsWith("//") || trimmed.startsWith("data:") || trimmed.startsWith("#")) {
107178
107416
  return null;
107179
107417
  }
107180
- const absPath = resolve8(absProjectDir, trimmed);
107181
- if (absPath.startsWith(absProjectDir + "/") || absPath === absProjectDir) {
107418
+ const absPath = resolve9(absProjectDir, trimmed);
107419
+ if (isPathInside(absPath, absProjectDir)) {
107182
107420
  return null;
107183
107421
  }
107184
107422
  if (!existsSync14(absPath)) return null;
107185
- const safeKey = "hf-ext/" + absPath.replace(/^\//, "");
107423
+ const safeKey = toExternalAssetKey(absPath);
107186
107424
  externalAssets.set(safeKey, absPath);
107187
107425
  return safeKey;
107188
107426
  }
@@ -107243,6 +107481,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
107243
107481
  /(<(?:video|audio)\b[^>]*?)\s+preload\s*=\s*["']none["']/gi,
107244
107482
  "$1"
107245
107483
  );
107484
+ const renderModeHints = detectRenderModeHints(sanitizedHtml);
107246
107485
  const coalescedHtml = await injectDeterministicFontFaces(
107247
107486
  coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
107248
107487
  );
@@ -107254,7 +107493,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
107254
107493
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
107255
107494
  for (const video of videos) {
107256
107495
  if (isHttpUrl(video.src)) continue;
107257
- const videoPath = resolve8(projectDir, video.src);
107496
+ const videoPath = resolve9(projectDir, video.src);
107258
107497
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
107259
107498
  Promise.all([analyzeKeyframeIntervals(videoPath), extractVideoMetadata(videoPath)]).then(([analysis, metadata]) => {
107260
107499
  if (analysis.isProblematic) {
@@ -107286,7 +107525,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
107286
107525
  externalAssets,
107287
107526
  width,
107288
107527
  height,
107289
- staticDuration
107528
+ staticDuration,
107529
+ renderModeHints
107290
107530
  };
107291
107531
  }
107292
107532
  async function discoverMediaFromBrowser(page) {
@@ -107380,7 +107620,8 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
107380
107620
  subCompositions,
107381
107621
  videos,
107382
107622
  audios,
107383
- unresolvedCompositions: remaining
107623
+ unresolvedCompositions: remaining,
107624
+ renderModeHints: compiled.renderModeHints
107384
107625
  };
107385
107626
  }
107386
107627
 
@@ -107481,17 +107722,17 @@ function installDebugLogger(logPath, log = defaultLogger) {
107481
107722
  };
107482
107723
  }
107483
107724
  function writeCompiledArtifacts(compiled, workDir, includeSummary) {
107484
- const compileDir = join14(workDir, "compiled");
107725
+ const compileDir = join15(workDir, "compiled");
107485
107726
  mkdirSync10(compileDir, { recursive: true });
107486
- writeFileSync4(join14(compileDir, "index.html"), compiled.html, "utf-8");
107727
+ writeFileSync4(join15(compileDir, "index.html"), compiled.html, "utf-8");
107487
107728
  for (const [srcPath, html] of compiled.subCompositions) {
107488
- const outPath = join14(compileDir, srcPath);
107729
+ const outPath = join15(compileDir, srcPath);
107489
107730
  mkdirSync10(dirname10(outPath), { recursive: true });
107490
107731
  writeFileSync4(outPath, html, "utf-8");
107491
107732
  }
107492
107733
  for (const [relativePath, absolutePath] of compiled.externalAssets) {
107493
- const outPath = resolve9(join14(compileDir, relativePath));
107494
- if (!outPath.startsWith(compileDir + "/")) {
107734
+ const outPath = resolve10(join15(compileDir, relativePath));
107735
+ if (!isPathInside(outPath, compileDir)) {
107495
107736
  console.warn(`[Render] Skipping external asset with unsafe path: ${relativePath}`);
107496
107737
  continue;
107497
107738
  }
@@ -107517,11 +107758,20 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
107517
107758
  end: a.end,
107518
107759
  mediaStart: a.mediaStart
107519
107760
  })),
107520
- subCompositions: Array.from(compiled.subCompositions.keys())
107761
+ subCompositions: Array.from(compiled.subCompositions.keys()),
107762
+ renderModeHints: compiled.renderModeHints
107521
107763
  };
107522
- writeFileSync4(join14(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
107764
+ writeFileSync4(join15(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
107523
107765
  }
107524
107766
  }
107767
+ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
107768
+ if (cfg.forceScreenshot || !compiled.renderModeHints.recommendScreenshot) return;
107769
+ cfg.forceScreenshot = true;
107770
+ log.warn("Auto-selected screenshot capture mode for render compatibility", {
107771
+ reasonCodes: compiled.renderModeHints.reasons.map((reason) => reason.code),
107772
+ reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
107773
+ });
107774
+ }
107525
107775
  function createRenderJob(config2) {
107526
107776
  return {
107527
107777
  id: randomUUID(),
@@ -107563,9 +107813,9 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
107563
107813
  }
107564
107814
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
107565
107815
  const moduleDir = dirname10(fileURLToPath3(import.meta.url));
107566
- const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve9(process.env.PRODUCER_RENDERS_DIR, "..") : resolve9(moduleDir, "../..");
107567
- const debugDir = join14(producerRoot, ".debug");
107568
- const workDir = job.config.debug ? join14(debugDir, job.id) : join14(dirname10(outputPath), `work-${job.id}`);
107816
+ const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve10(process.env.PRODUCER_RENDERS_DIR, "..") : resolve10(moduleDir, "../..");
107817
+ const debugDir = join15(producerRoot, ".debug");
107818
+ const workDir = job.config.debug ? join15(debugDir, job.id) : join15(dirname10(outputPath), `work-${job.id}`);
107569
107819
  const pipelineStart = Date.now();
107570
107820
  const log = job.config.logger ?? defaultLogger;
107571
107821
  let fileServer = null;
@@ -107573,7 +107823,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107573
107823
  let lastBrowserConsole = [];
107574
107824
  let restoreLogger = null;
107575
107825
  const perfStages = {};
107576
- const perfOutputPath = join14(workDir, "perf-summary.json");
107826
+ const perfOutputPath = join15(workDir, "perf-summary.json");
107577
107827
  const cfg = { ...job.config.producerConfig ?? resolveConfig() };
107578
107828
  const outputFormat = job.config.format ?? "mp4";
107579
107829
  const isWebm = outputFormat === "webm";
@@ -107595,19 +107845,19 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107595
107845
  assertNotAborted();
107596
107846
  if (!existsSync15(workDir)) mkdirSync10(workDir, { recursive: true });
107597
107847
  if (job.config.debug) {
107598
- const logPath = join14(workDir, "render.log");
107848
+ const logPath = join15(workDir, "render.log");
107599
107849
  restoreLogger = installDebugLogger(logPath, log);
107600
107850
  }
107601
107851
  const entryFile = job.config.entryFile || "index.html";
107602
- let htmlPath = join14(projectDir, entryFile);
107852
+ let htmlPath = join15(projectDir, entryFile);
107603
107853
  if (!existsSync15(htmlPath)) {
107604
107854
  throw new Error(`Entry file not found: ${htmlPath}`);
107605
107855
  }
107606
107856
  assertNotAborted();
107607
107857
  const rawEntry = readFileSync9(htmlPath, "utf-8");
107608
107858
  if (entryFile !== "index.html" && rawEntry.trimStart().startsWith("<template")) {
107609
- const wrapperPath = join14(workDir, "standalone-entry.html");
107610
- const projectIndexPath = join14(projectDir, "index.html");
107859
+ const wrapperPath = join15(workDir, "standalone-entry.html");
107860
+ const projectIndexPath = join15(projectDir, "index.html");
107611
107861
  if (!existsSync15(projectIndexPath)) {
107612
107862
  throw new Error(
107613
107863
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
@@ -107631,9 +107881,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107631
107881
  const stage1Start = Date.now();
107632
107882
  updateJobStatus(job, "preprocessing", "Compiling composition", 5, onProgress);
107633
107883
  const compileStart = Date.now();
107634
- let compiled = await compileForRender(projectDir, htmlPath, join14(workDir, "downloads"));
107884
+ let compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"));
107635
107885
  assertNotAborted();
107636
107886
  perfStages.compileOnlyMs = Date.now() - compileStart;
107887
+ applyRenderModeHints(cfg, compiled, log);
107637
107888
  writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
107638
107889
  log.info("Compiled composition metadata", {
107639
107890
  entryFile,
@@ -107641,7 +107892,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107641
107892
  width: compiled.width,
107642
107893
  height: compiled.height,
107643
107894
  videoCount: compiled.videos.length,
107644
- audioCount: compiled.audios.length
107895
+ audioCount: compiled.audios.length,
107896
+ renderModeHints: compiled.renderModeHints
107645
107897
  });
107646
107898
  const composition = {
107647
107899
  duration: compiled.staticDuration,
@@ -107660,8 +107912,9 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107660
107912
  reasons.push(`${compiled.unresolvedCompositions.length} unresolved composition(s)`);
107661
107913
  fileServer = await createFileServer2({
107662
107914
  projectDir,
107663
- compiledDir: join14(workDir, "compiled"),
107664
- port: 0
107915
+ compiledDir: join15(workDir, "compiled"),
107916
+ port: 0,
107917
+ preHeadScripts: [VIRTUAL_TIME_SHIM]
107665
107918
  });
107666
107919
  assertNotAborted();
107667
107920
  const captureOpts = {
@@ -107673,7 +107926,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107673
107926
  };
107674
107927
  probeSession = await createCaptureSession(
107675
107928
  fileServer.url,
107676
- join14(workDir, "probe"),
107929
+ join15(workDir, "probe"),
107677
107930
  captureOpts,
107678
107931
  null,
107679
107932
  cfg
@@ -107705,7 +107958,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107705
107958
  compiled,
107706
107959
  resolutions,
107707
107960
  projectDir,
107708
- join14(workDir, "downloads")
107961
+ join15(workDir, "downloads")
107709
107962
  );
107710
107963
  assertNotAborted();
107711
107964
  composition.videos = compiled.videos;
@@ -107840,12 +108093,12 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107840
108093
  const stage2Start = Date.now();
107841
108094
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
107842
108095
  let frameLookup = null;
107843
- const compiledDir = join14(workDir, "compiled");
108096
+ const compiledDir = join15(workDir, "compiled");
107844
108097
  if (composition.videos.length > 0) {
107845
108098
  const extractionResult = await extractAllVideoFrames(
107846
108099
  composition.videos,
107847
108100
  projectDir,
107848
- { fps: job.config.fps, outputDir: join14(workDir, "video-frames") },
108101
+ { fps: job.config.fps, outputDir: join15(workDir, "video-frames") },
107849
108102
  abortSignal,
107850
108103
  void 0,
107851
108104
  compiledDir
@@ -107879,13 +108132,13 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107879
108132
  }
107880
108133
  const stage3Start = Date.now();
107881
108134
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
107882
- const audioOutputPath = join14(workDir, "audio.aac");
108135
+ const audioOutputPath = join15(workDir, "audio.aac");
107883
108136
  let hasAudio = false;
107884
108137
  if (composition.audios.length > 0) {
107885
108138
  const audioResult = await processCompositionAudio(
107886
108139
  composition.audios,
107887
108140
  projectDir,
107888
- join14(workDir, "audio-work"),
108141
+ join15(workDir, "audio-work"),
107889
108142
  audioOutputPath,
107890
108143
  job.duration,
107891
108144
  abortSignal,
@@ -107903,12 +108156,13 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107903
108156
  if (!fileServer) {
107904
108157
  fileServer = await createFileServer2({
107905
108158
  projectDir,
107906
- compiledDir: join14(workDir, "compiled"),
107907
- port: 0
108159
+ compiledDir: join15(workDir, "compiled"),
108160
+ port: 0,
108161
+ preHeadScripts: [VIRTUAL_TIME_SHIM]
107908
108162
  });
107909
108163
  assertNotAborted();
107910
108164
  }
107911
- const framesDir = join14(workDir, "captured-frames");
108165
+ const framesDir = join15(workDir, "captured-frames");
107912
108166
  if (!existsSync15(framesDir)) mkdirSync10(framesDir, { recursive: true });
107913
108167
  const captureOptions = {
107914
108168
  width,
@@ -107920,7 +108174,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
107920
108174
  const workerCount = calculateOptimalWorkers(job.totalFrames, job.config.workers, cfg);
107921
108175
  const FORMAT_EXT = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
107922
108176
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
107923
- const videoOnlyPath = join14(workDir, `video-only${videoExt}`);
108177
+ const videoOnlyPath = join15(workDir, `video-only${videoExt}`);
107924
108178
  const preset = getEncoderPreset(job.config.quality, outputFormat);
107925
108179
  const effectiveQuality = job.config.crf ?? preset.quality;
107926
108180
  const effectiveBitrate = job.config.videoBitrate;
@@ -108196,7 +108450,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108196
108450
  }
108197
108451
  if (job.config.debug) {
108198
108452
  if (existsSync15(outputPath)) {
108199
- const debugOutput = join14(workDir, `output${videoExt}`);
108453
+ const debugOutput = join15(workDir, `output${videoExt}`);
108200
108454
  copyFileSync2(outputPath, debugOutput);
108201
108455
  }
108202
108456
  } else {
@@ -108291,7 +108545,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
108291
108545
 
108292
108546
  // src/services/hyperframeLint.ts
108293
108547
  import { existsSync as existsSync16, readFileSync as readFileSync10, statSync as statSync6 } from "node:fs";
108294
- import { resolve as resolve10, join as join15 } from "node:path";
108548
+ import { resolve as resolve11, join as join16 } from "node:path";
108295
108549
  function isStringRecord(value) {
108296
108550
  if (!value || typeof value !== "object" || Array.isArray(value)) {
108297
108551
  return false;
@@ -108318,7 +108572,7 @@ function pickEntryFile(files, preferredEntryFile) {
108318
108572
  return null;
108319
108573
  }
108320
108574
  function readProjectEntryFile(projectDir, preferredEntryFile) {
108321
- const absProjectDir = resolve10(projectDir);
108575
+ const absProjectDir = resolve11(projectDir);
108322
108576
  if (!existsSync16(absProjectDir) || !statSync6(absProjectDir).isDirectory()) {
108323
108577
  return { error: `Project directory not found: ${absProjectDir}` };
108324
108578
  }
@@ -108326,7 +108580,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
108326
108580
  (value) => typeof value === "string" && value.trim().length > 0
108327
108581
  );
108328
108582
  for (const entryFile of entryCandidates) {
108329
- const absoluteEntryPath = resolve10(absProjectDir, entryFile);
108583
+ const absoluteEntryPath = resolve11(absProjectDir, entryFile);
108330
108584
  if (!absoluteEntryPath.startsWith(absProjectDir)) {
108331
108585
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
108332
108586
  }
@@ -108339,7 +108593,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
108339
108593
  }
108340
108594
  }
108341
108595
  return {
108342
- error: `No HTML entry file found in project directory: ${join15(absProjectDir, preferredEntryFile || "index.html")}`
108596
+ error: `No HTML entry file found in project directory: ${join16(absProjectDir, preferredEntryFile || "index.html")}`
108343
108597
  };
108344
108598
  }
108345
108599
  function prepareHyperframeLintBody(body) {
@@ -108379,17 +108633,6 @@ function runHyperframeLint(prepared) {
108379
108633
  return lintHyperframeHtml(prepared.html, { filePath: prepared.entryFile });
108380
108634
  }
108381
108635
 
108382
- // src/utils/paths.ts
108383
- import { resolve as resolve11, basename as basename2, join as join16 } from "node:path";
108384
- var DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? resolve11(new URL(import.meta.url).pathname, "../../..", "renders");
108385
- function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS_DIR) {
108386
- const absoluteProjectDir = resolve11(projectDir);
108387
- const projectName = basename2(absoluteProjectDir);
108388
- const resolvedOutputPath = outputPath ?? join16(rendersDir, `${projectName}.mp4`);
108389
- const absoluteOutputPath = resolve11(resolvedOutputPath);
108390
- return { absoluteProjectDir, absoluteOutputPath };
108391
- }
108392
-
108393
108636
  // src/utils/semaphore.ts
108394
108637
  var Semaphore = class {
108395
108638
  constructor(maxConcurrent) {