@hyperframes/engine 0.8.219 → 0.8.221

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.
@@ -83,8 +83,9 @@ export async function explainChromeLaunchFailure(error, executablePath, args, si
83
83
  const { code, errno, syscall } = error;
84
84
  if (code === "UNKNOWN" && syscall === "spawn")
85
85
  return new Error(`Windows refused to start the browser${executablePath ? ` at ${executablePath}` : ""} (error ${errno ?? "unknown"}, spawn UNKNOWN). ` +
86
- "Security software may be blocking it, or the computer may be out of memory. " +
87
- "Close other apps and retry; if it persists, run `hyperframes browser ensure --force` to download it again.", { cause: error });
86
+ "Windows Application Control or security software may be blocking it, or the computer may be out of memory. " +
87
+ "Close other apps and retry. If a policy blocks the file, allow that path or set HYPERFRAMES_BROWSER_PATH to a Chrome or Edge your policy allows; " +
88
+ "if the file is damaged, run `hyperframes browser ensure --force` to download it again.", { cause: error });
88
89
  if (!executablePath)
89
90
  return error;
90
91
  const diagnosis = await diagnoseChromeStartup(executablePath, args ?? [], { signal });
@@ -430,7 +430,7 @@ export interface IsolatedComposition {
430
430
  compositionId: string;
431
431
  elementId: string | null;
432
432
  }
433
- export declare function authorScriptsMayRegisterTimelines(runtimeAttr: string, authoredBegin: string, authoredEnd: string): Promise<boolean>;
433
+ export declare function authorScriptsMayRegisterTimelines(runtimeAttr: string, authoredBegin: string, authoredEnd: string, skipIds?: readonly string[]): Promise<boolean>;
434
434
  export interface SubTimelinePollOptions {
435
435
  intervalMs?: number;
436
436
  getScriptLoadFailures?: () => readonly string[];
@@ -1362,9 +1362,9 @@ export async function waitForSubCompositionTimelines(session, page, timeoutMs, o
1362
1362
  memo.unregisteredIds = session.pendingTimelineIds ?? [];
1363
1363
  }
1364
1364
  }
1365
- // Runs in the page. False once every inline, template and same-origin script is read and none
1366
- // mentions __timelines or imports code; an unreadable or module script, or a page still loading, answers true.
1367
- export async function authorScriptsMayRegisterTimelines(runtimeAttr, authoredBegin, authoredEnd) {
1365
+ // Runs in the page. False once every script is read and none can register an id still waited on;
1366
+ // an unreadable, module or importing script, a computed key, or a page still loading answers true.
1367
+ export async function authorScriptsMayRegisterTimelines(runtimeAttr, authoredBegin, authoredEnd, skipIds = []) {
1368
1368
  if (document.readyState !== "complete")
1369
1369
  return true;
1370
1370
  const memo = globalThis;
@@ -1373,38 +1373,66 @@ export async function authorScriptsMayRegisterTimelines(runtimeAttr, authoredBeg
1373
1373
  ...Array.from(document.querySelectorAll("script")),
1374
1374
  ...Array.from(document.querySelectorAll("template")).flatMap((t) => Array.from(t.content.querySelectorAll("script"))),
1375
1375
  ].filter((script) => !script.hasAttribute(runtimeAttr));
1376
+ const scan = { opaque: false, mentions: false, ids: [] };
1376
1377
  // Code that imports another file may register through it, which is not read here.
1377
- const mayRegister = (code) => code.includes("__timelines") || /\bimport\s*\(|^\s*import\s*[\w{*"']/m.test(code);
1378
+ const mayImport = (code) => /\bimport\s*\(|^\s*import\s*[\w{*"']/m.test(code);
1379
+ const read = (code) => {
1380
+ if (mayImport(code))
1381
+ scan.opaque = true;
1382
+ if (!code.includes("__timelines"))
1383
+ return;
1384
+ scan.mentions = true;
1385
+ const literalKey = /__timelines\s*(?:\[\s*(["'])((?:(?!\1).)+)\1\s*\]|\.([A-Za-z_$][\w$]*))/g;
1386
+ const rest = code
1387
+ .replace(/__timelines\s*=\s*(?:window\.__timelines\s*\|\|\s*)?\{\s*\}/g, "")
1388
+ .replace(literalKey, (_m, _q, bracketId, dotId) => {
1389
+ scan.ids.push(bracketId ?? dotId);
1390
+ return "";
1391
+ });
1392
+ if (rest.includes("__timelines"))
1393
+ scan.opaque = true;
1394
+ };
1378
1395
  for (const script of scripts) {
1379
1396
  if (script.type === "module")
1380
- return true;
1397
+ return { ...scan, opaque: true };
1381
1398
  if (!script.src) {
1382
1399
  const text = script.textContent ?? "";
1383
- const authored = text.includes(authoredBegin)
1400
+ read(text.includes(authoredBegin)
1384
1401
  ? text
1385
1402
  .split(authoredBegin)
1386
1403
  .slice(1)
1387
1404
  .map((region) => region.split(authoredEnd)[0])
1388
1405
  .join("\n")
1389
- : text;
1390
- if (mayRegister(authored))
1391
- return true;
1406
+ : text);
1392
1407
  continue;
1393
1408
  }
1394
1409
  const url = new URL(script.src, location.href);
1395
1410
  if (url.origin !== location.origin)
1396
- return true;
1411
+ return { ...scan, opaque: true };
1397
1412
  try {
1398
- if (mayRegister(await (await fetch(url)).text()))
1399
- return true;
1413
+ read(await (await fetch(url)).text());
1400
1414
  }
1401
1415
  catch {
1402
- return true;
1416
+ return { ...scan, opaque: true };
1403
1417
  }
1404
1418
  }
1405
- return false;
1419
+ return scan;
1406
1420
  })();
1407
- return memo.__hfAuthorScan;
1421
+ const { opaque, mentions, ids } = memo.__hfAuthorScan;
1422
+ if (opaque)
1423
+ return true;
1424
+ if (!mentions)
1425
+ return false;
1426
+ const timelines = window.__timelines ?? {};
1427
+ return Array.from(document.querySelectorAll("[data-composition-id]")).some((host) => {
1428
+ const id = host.getAttribute("data-composition-id");
1429
+ const authoredId = host.getAttribute("data-hf-original-composition-id") ?? id;
1430
+ return (id &&
1431
+ !host.hasAttribute("data-no-timeline") &&
1432
+ !timelines[id] &&
1433
+ !skipIds.includes(id) &&
1434
+ ids.includes(authoredId ?? id));
1435
+ });
1408
1436
  }
1409
1437
  export async function pollSubCompositionTimelines(page, timeoutMs, { intervalMs = 150, getScriptLoadFailures, scriptFailureGraceMs = 2_000, getPageErrors, scriptErrorGraceMs = 5_000, onPending, shouldStop, knownUnregisteredIds = [], } = {}) {
1410
1438
  // Hosts may opt out of the timeline wait with `data-no-timeline` —
@@ -1438,7 +1466,7 @@ export async function pollSubCompositionTimelines(page, timeoutMs, { intervalMs
1438
1466
  ready =
1439
1467
  Boolean(await page.evaluate(expression)) ||
1440
1468
  (failed.length === 0 &&
1441
- !(await page.evaluate(authorScriptsMayRegisterTimelines, RUNTIME_BOOTSTRAP_ATTR, AUTHORED_SOURCE_BEGIN, AUTHORED_SOURCE_END)));
1469
+ !(await page.evaluate(authorScriptsMayRegisterTimelines, RUNTIME_BOOTSTRAP_ATTR, AUTHORED_SOURCE_BEGIN, AUTHORED_SOURCE_END, knownUnregisteredIds)));
1442
1470
  if (ready || shouldStop?.())
1443
1471
  break;
1444
1472
  const now = Date.now();
@@ -697,6 +697,9 @@ outputDirOverride) {
697
697
  vfFilters.push("format=nv12");
698
698
  }
699
699
  const samplingAt = vfFilters.length;
700
+ if (metadata.sampleAspectRatio) {
701
+ vfFilters.push("scale=trunc(iw*sar/2)*2:ih", "setsar=1");
702
+ }
700
703
  // Each slot takes the last frame starting at or before its time. Ticks of 2 ms or finer
701
704
  // first shift half a tick, absorbing container rounding (WebM/MKV use whole ms).
702
705
  const samplingFilters = (seek) => sampleAtOutputFps
@@ -42,6 +42,7 @@ export interface VideoMetadata {
42
42
  colorSpace: VideoColorSpace | null;
43
43
  colorRange?: string;
44
44
  pixelFormat?: string;
45
+ sampleAspectRatio?: number;
45
46
  /** Decoded frame count from the video stream's `nb_frames`. Omitted when the
46
47
  * container does not surface a reliable count (still images, malformed
47
48
  * streams, or muxes that require `-count_packets` to populate). Callers
@@ -79,6 +80,7 @@ interface FFProbeStream {
79
80
  nb_frames?: string;
80
81
  nb_read_packets?: string;
81
82
  pix_fmt?: string;
83
+ sample_aspect_ratio?: string;
82
84
  r_frame_rate?: string;
83
85
  avg_frame_rate?: string;
84
86
  sample_rate?: string;
@@ -169,6 +171,7 @@ export declare function readTagCI(tags: Record<string, string | undefined> | und
169
171
  * smallest representable 2dp rate instead.
170
172
  */
171
173
  export declare function parseFrameRate(frameRateStr: string | undefined): number;
174
+ export declare function parseSampleAspectRatio(value: string | undefined): number | undefined;
172
175
  /**
173
176
  * Whether the first frame decoded with `decoder` (alpha-aware) has an alpha pixel format; undefined on any
174
177
  * failure. ffprobe cannot pick a decoder, so ffmpeg writes the frame as rawvideo, which keeps its pix_fmt.
@@ -629,6 +629,13 @@ export function parseFrameRate(frameRateStr) {
629
629
  // caller's 30fps default.
630
630
  return rounded > 0 ? rounded : 0.01;
631
631
  }
632
+ export function parseSampleAspectRatio(value) {
633
+ const [num, den] = (value ?? "").split(":").map(Number);
634
+ if (!num || !den || !Number.isFinite(num) || !Number.isFinite(den))
635
+ return undefined;
636
+ const ratio = num / den;
637
+ return Math.abs(ratio - 1) < 1e-3 ? undefined : ratio;
638
+ }
632
639
  /**
633
640
  * Whether the first frame decoded with `decoder` (alpha-aware) has an alpha pixel format; undefined on any
634
641
  * failure. ffprobe cannot pick a decoder, so ffmpeg writes the frame as rawvideo, which keeps its pix_fmt.
@@ -756,6 +763,7 @@ export async function extractMediaMetadata(filePath) {
756
763
  };
757
764
  const colorSpace = merged.colorTransfer || merged.colorPrimaries || merged.colorSpace ? merged : null;
758
765
  const pixelFormat = videoStream.pix_fmt || "";
766
+ const sampleAspectRatio = parseSampleAspectRatio(videoStream.sample_aspect_ratio);
759
767
  const hasAlpha = videoStreamHasAlpha(videoStream);
760
768
  const alphaDecoder = hasAlpha ? null : alphaDecoderFor(videoStream.codec_name);
761
769
  const alphaPlaneDecoded = alphaDecoder
@@ -808,6 +816,7 @@ export async function extractMediaMetadata(filePath) {
808
816
  colorSpace,
809
817
  colorRange: videoStream.color_range,
810
818
  pixelFormat,
819
+ ...(sampleAspectRatio === undefined ? {} : { sampleAspectRatio }),
811
820
  frames,
812
821
  };
813
822
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.219",
3
+ "version": "0.8.221",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,8 +55,8 @@
55
55
  "puppeteer": "~25.11.0",
56
56
  "puppeteer-core": "^25.11.0",
57
57
  "undici": "7.25.0",
58
- "@hyperframes/parsers": "^0.8.219",
59
- "@hyperframes/core": "^0.8.219"
58
+ "@hyperframes/core": "^0.8.221",
59
+ "@hyperframes/parsers": "^0.8.221"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/node": "^25.0.10",