@hyperframes/engine 0.8.207 → 0.8.208

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.
@@ -61,32 +61,6 @@ export declare const _closeBrowserAfterFailedProbeForTests: typeof closeBrowserA
61
61
  export declare function _resetAutoBrowserGpuModeCacheForTests(): void;
62
62
  /** True when a `resolvedMode` render must launch without WAYLAND_DISPLAY to reach the GPU. */
63
63
  export declare function captureWithoutWaylandDisplay(resolvedMode: "software" | "hardware"): Promise<boolean>;
64
- /**
65
- * Resolve `browserGpuMode` to a concrete `"software" | "hardware"` answer.
66
- *
67
- * For `"software"` this is a pure pass-through. For `"auto"` it launches a
68
- * tiny Chrome with the platform's hardware GPU args, runs a one-shot WebGL
69
- * availability probe, and falls back to `"software"` if hardware-mode WebGL
70
- * is unavailable. The Promise is cached for the process lifetime, so
71
- * concurrent callers (parallel workers) share the same probe.
72
- *
73
- * `"hardware"` (an explicit `--browser-gpu` / `PRODUCER_BROWSER_GPU_MODE=
74
- * hardware`) is honoured verbatim — the operator asked for it — but runs the
75
- * SAME probe to VERIFY it, because Chrome's hardware GL args are advisory:
76
- * with no usable GPU in the sandbox (no `/dev/dri`, no NVIDIA container
77
- * runtime, missing EGL/driver libraries) Chrome silently falls back to
78
- * software WebGL and the render just runs at CPU speed. Without this check
79
- * the only trace is a buried `Automatic fallback to software WebGL` browser
80
- * warning — heygen-com/hyperframes#2967 rendered 19186 frames on CPU while
81
- * `--browser-gpu` was set and nothing said so. The probe result never
82
- * changes the returned mode; it only makes the fallback loud.
83
- *
84
- * Any probe failure (Chrome launch error, navigation timeout, missing canvas
85
- * API, etc.) is treated as a `"software"` result. The render path with
86
- * SwiftShader always works, so a misclassification toward software is the
87
- * safe failure mode; misclassifying toward hardware would error on the real
88
- * render.
89
- */
90
64
  export declare function resolveBrowserGpuMode(mode: EngineConfig["browserGpuMode"], options?: GpuProbeOptions): Promise<"software" | "hardware">;
91
65
  type BrowserLaunchConfig = Partial<Pick<EngineConfig, "browserTimeout" | "protocolTimeout" | "chromePath" | "forceScreenshot">> & {
92
66
  withoutWaylandDisplay?: boolean;
@@ -5,6 +5,7 @@
5
5
  * Manages Puppeteer browser lifecycle: Chrome executable resolution,
6
6
  * launch args, pooled browser acquisition/release.
7
7
  */
8
+ import { explainChromeLaunchFailure } from "./chromeStartup.js";
8
9
  import { withHyperframesBrowserMarker } from "@hyperframes/core/browser-marker";
9
10
  import { scanHtmlOpeningTags } from "@hyperframes/parsers/html-attribute-spans";
10
11
  import { execSync } from "child_process";
@@ -462,7 +463,8 @@ async function probeWebGlMode(ppt, options, env, label = "") {
462
463
  }
463
464
  catch (err) {
464
465
  logResolvedBrowserGpuMode("software", label + formatProbeFailure(err));
465
- return { mode: "software", cause: "probe-error" };
466
+ const aborted = err instanceof Error && err.name === "AbortError";
467
+ return { mode: "software", cause: "probe-error", ...(aborted && { aborted }) };
466
468
  }
467
469
  }
468
470
  function envWithoutWaylandDisplay() {
@@ -524,12 +526,24 @@ function wslAccess(options) {
524
526
  * safe failure mode; misclassifying toward hardware would error on the real
525
527
  * render.
526
528
  */
529
+ async function sharedGpuProbe(options) {
530
+ const shared = (_autoBrowserGpuModeCache ??= probeAutoBrowserGpuMode(options));
531
+ const outcome = await shared;
532
+ if (!outcome.aborted)
533
+ return outcome;
534
+ if (_autoBrowserGpuModeCache === shared)
535
+ _autoBrowserGpuModeCache = undefined;
536
+ const own = await probeAutoBrowserGpuMode(options);
537
+ if (!own.aborted)
538
+ _autoBrowserGpuModeCache ??= Promise.resolve(own);
539
+ return own;
540
+ }
527
541
  export function resolveBrowserGpuMode(mode, options = {}) {
528
542
  if (mode === "software")
529
543
  return Promise.resolve(mode);
530
- _autoBrowserGpuModeCache ??= probeAutoBrowserGpuMode(options);
544
+ const probe = sharedGpuProbe(options);
531
545
  if (mode === "auto") {
532
- return _autoBrowserGpuModeCache.then((probed) => {
546
+ return probe.then((probed) => {
533
547
  if (probed.mode === "software" && probed.wslAccess && !_wslSoftwareNoteLogged) {
534
548
  _wslSoftwareNoteLogged = true;
535
549
  console.error(buildWslSoftwareNote(probed));
@@ -537,7 +551,7 @@ export function resolveBrowserGpuMode(mode, options = {}) {
537
551
  return probed.mode;
538
552
  });
539
553
  }
540
- return _autoBrowserGpuModeCache.then((probed) => {
554
+ return probe.then((probed) => {
541
555
  // Warn once per cache lifetime, not once per caller: `createCaptureSession`
542
556
  // resolves the mode for the probe browser AND every parallel worker, so
543
557
  // an un-deduplicated warning prints N+1 times and buries itself.
@@ -658,7 +672,8 @@ async function launchBrowser(fingerprint) {
658
672
  const env = launchEnv(fingerprint);
659
673
  let captureMode = fingerprint.requestedCaptureMode;
660
674
  let browser;
661
- const launch = (args) => ppt.launch({
675
+ const launch = (args) => ppt
676
+ .launch({
662
677
  ...ownedLaunchOptions(),
663
678
  headless: true,
664
679
  args: withHyperframesBrowserMarker(args),
@@ -667,6 +682,13 @@ async function launchBrowser(fingerprint) {
667
682
  env,
668
683
  timeout: fingerprint.browserTimeoutMs,
669
684
  protocolTimeout: fingerprint.protocolTimeoutMs,
685
+ })
686
+ .catch(async (error) => {
687
+ const defaults = await ppt.defaultArgs({
688
+ headless: true,
689
+ args: withHyperframesBrowserMarker(args),
690
+ });
691
+ throw await explainChromeLaunchFailure(error, fingerprint.executablePath, defaults);
670
692
  });
671
693
  const probe = (target, timeoutMs) => probeBeginFrameSupport(target, timeoutMs).catch((error) => ({
672
694
  supported: true,
@@ -7,7 +7,7 @@ export declare const isManagedChromeDir: (versionName: string) => boolean;
7
7
  /** Newest first, by numeric build (`linux-148.0.1` before `linux-99.0.1`). */
8
8
  export declare function compareBrowserVersionsDescending(left: string, right: string): number;
9
9
  /** Whether a shell loads a blank page and exits: an old build can die at launch on a newer OS. Asked once per path. */
10
- export declare function shellStarts(binary: string): boolean;
10
+ export declare function shellStarts(binary: string, timeoutMs?: number): boolean;
11
11
  /** The shared Puppeteer cache's shell: the managed build, else the newest under the host's ceiling that starts.
12
12
  * `shell` names a version dir's executable, or undefined when it holds no usable file. */
13
13
  export declare function pickSharedShell(versions: readonly string[], shell: (version: string) => string | undefined): string | undefined;
@@ -52,11 +52,13 @@ export function compareBrowserVersionsDescending(left, right) {
52
52
  const starts = new Map();
53
53
  const debug = debuglog("hyperframes:browser");
54
54
  /** Whether a shell loads a blank page and exits: an old build can die at launch on a newer OS. Asked once per path. */
55
- export function shellStarts(binary) {
55
+ export function shellStarts(binary, timeoutMs = 10_000) {
56
56
  let answer = starts.get(binary);
57
57
  if (answer === undefined) {
58
58
  const probe = ["--no-sandbox", "--dump-dom", "about:blank"];
59
- answer = spawnSync(binary, probe, { stdio: "ignore", timeout: 10_000 })?.status === 0;
59
+ answer =
60
+ spawnSync(binary, probe, { stdio: "ignore", timeout: timeoutMs, killSignal: "SIGKILL" })
61
+ ?.status === 0;
60
62
  if (!answer)
61
63
  debug("skipping %s: it did not start", binary);
62
64
  starts.set(binary, answer);
@@ -0,0 +1,10 @@
1
+ /** Names an exit code the way a person can act on it; undefined when it has no name. */
2
+ export declare function nameChromeExit(code: number | null, platform?: NodeJS.Platform): string | undefined;
3
+ /** Starts the browser once more with the failed launch's flags and says how it ended. */
4
+ export declare function diagnoseChromeStartup(executablePath: string, args: readonly string[], options?: {
5
+ timeoutMs?: number;
6
+ platform?: NodeJS.Platform;
7
+ signal?: AbortSignal;
8
+ }): Promise<string>;
9
+ /** The launch error, with the startup diagnosis appended; the original stays as `cause`. */
10
+ export declare function explainChromeLaunchFailure(error: unknown, executablePath: string | undefined, args: readonly string[] | undefined, signal?: AbortSignal): Promise<unknown>;
@@ -0,0 +1,85 @@
1
+ import { spawn } from "node:child_process";
2
+ import { mkdtempSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { removeTreeBestEffort } from "@hyperframes/core/remove-tree";
6
+ const STDERR_TAIL_LINES = 8;
7
+ const PROBE_TIMEOUT_MS = 8_000;
8
+ // NTSTATUS codes a Windows process dies with before it can print anything.
9
+ const WINDOWS_EXITS = [
10
+ [0xc0000135, "0xC0000135: a required runtime or DLL is missing (Visual C++ runtime)."],
11
+ [0xc0000017, "0xC0000017: Windows had no memory to start it (out of memory)."],
12
+ [0xc0000142, "0xC0000142: a DLL failed to initialise, often security software blocking it."],
13
+ [0xc0000022, "0xC0000022: access denied, often antivirus or a policy blocking the executable."],
14
+ [0xc000007b, "0xC000007B: the executable and a DLL do not match (32-bit and 64-bit)."],
15
+ ];
16
+ /** Names an exit code the way a person can act on it; undefined when it has no name. */
17
+ export function nameChromeExit(code, platform = process.platform) {
18
+ if (platform !== "win32" || code === null)
19
+ return undefined;
20
+ const unsigned = code >>> 0;
21
+ return WINDOWS_EXITS.find(([known]) => known === unsigned)?.[1];
22
+ }
23
+ function startupArgs(args, profile) {
24
+ const kept = args.filter((arg) => arg.startsWith("--") &&
25
+ !arg.startsWith("--remote-debugging") &&
26
+ !arg.startsWith("--user-data-dir"));
27
+ return [...kept, `--user-data-dir=${profile}`, "--remote-debugging-port=0", "about:blank"];
28
+ }
29
+ function tail(stderr) {
30
+ return stderr
31
+ .split(/\r?\n/)
32
+ .map((line) => line.trim())
33
+ .filter(Boolean)
34
+ .slice(-STDERR_TAIL_LINES)
35
+ .join("\n");
36
+ }
37
+ /** Starts the browser once more with the failed launch's flags and says how it ended. */
38
+ export async function diagnoseChromeStartup(executablePath, args, options = {}) {
39
+ const profile = mkdtempSync(join(tmpdir(), "hf-chrome-probe-"));
40
+ const timeoutMs = options.timeoutMs ?? PROBE_TIMEOUT_MS;
41
+ try {
42
+ return await new Promise((resolve) => {
43
+ let stderr = "";
44
+ const child = spawn(executablePath, startupArgs(args, profile), {
45
+ stdio: ["ignore", "ignore", "pipe"],
46
+ windowsHide: true,
47
+ });
48
+ const timer = setTimeout(() => {
49
+ child.kill();
50
+ resolve(`Chrome started and kept running for ${timeoutMs} ms when launched on its own.`);
51
+ }, timeoutMs);
52
+ options.signal?.addEventListener("abort", () => {
53
+ clearTimeout(timer);
54
+ child.kill();
55
+ resolve("The startup check was cancelled.");
56
+ }, { once: true });
57
+ child.stderr.on("data", (chunk) => {
58
+ stderr = (stderr + chunk.toString()).slice(-4000);
59
+ });
60
+ child.once("error", (error) => {
61
+ clearTimeout(timer);
62
+ resolve(`Chrome could not be started: ${error.message}`);
63
+ });
64
+ child.once("exit", (code, signal) => {
65
+ clearTimeout(timer);
66
+ const how = signal ? `was stopped by ${signal}` : `exited with code ${code}`;
67
+ const named = nameChromeExit(code, options.platform);
68
+ const lines = tail(stderr);
69
+ resolve([`Chrome ${how} when launched on its own.`, named, lines && `stderr:\n${lines}`]
70
+ .filter(Boolean)
71
+ .join("\n"));
72
+ });
73
+ });
74
+ }
75
+ finally {
76
+ removeTreeBestEffort(profile);
77
+ }
78
+ }
79
+ /** The launch error, with the startup diagnosis appended; the original stays as `cause`. */
80
+ export async function explainChromeLaunchFailure(error, executablePath, args, signal) {
81
+ if (!executablePath || !(error instanceof Error))
82
+ return error;
83
+ const diagnosis = await diagnoseChromeStartup(executablePath, args ?? [], { signal });
84
+ return new Error(`${error.message}\n${diagnosis}`, { cause: error });
85
+ }
@@ -239,6 +239,8 @@ export interface ParallelCaptureHooks {
239
239
  */
240
240
  onWorkerFailure?: (failure: CaptureFailure) => void;
241
241
  onWorkerOperation?: (diagnostic: ParallelWorkerPhaseDiagnostic) => void;
242
+ /** Worker 0 runs this on its initialised session; the other workers wait for it. A rejection fails the pool. */
243
+ verifyFirstSession?: (session: CaptureSession) => Promise<void>;
242
244
  }
243
245
  /**
244
246
  * The pool's single failure gate. Both halves of the contract the streaming
@@ -552,7 +552,7 @@ export async function verifyDiskDrawElementSamples(session, task, streaming) {
552
552
  // flagged only because the disk self-verify call shifted its line range into
553
553
  // the changed-code audit. Not restructured by this PR.
554
554
  // fallow-ignore-next-line complexity
555
- async function executeWorkerTask(task, serverUrl, captureOptions, createBeforeCaptureHook, signal, onFrameCaptured, onFrameBuffer, config, parallel, onFailure, onWorkerPhase, onWorkerOperation) {
555
+ async function executeWorkerTask(task, serverUrl, captureOptions, createBeforeCaptureHook, signal, onFrameCaptured, onFrameBuffer, config, parallel, onFailure, onWorkerPhase, onWorkerOperation, gate) {
556
556
  const startTime = Date.now();
557
557
  let framesCaptured = 0;
558
558
  if (!existsSync(task.outputDir))
@@ -603,6 +603,8 @@ async function executeWorkerTask(task, serverUrl, captureOptions, createBeforeCa
603
603
  await assertSwiftShader(activeSession.page, readWebGlVendorInfoFromCanvas);
604
604
  }
605
605
  await initializeSession(activeSession);
606
+ if (gate)
607
+ await (task.workerId === 0 ? gate.verify(activeSession) : gate.wait());
606
608
  });
607
609
  canvasDrawElement = activeSession.captureMode === "drawelement";
608
610
  gpuBackend = activeSession.gpuRenderer ?? "unknown";
@@ -622,6 +624,8 @@ async function executeWorkerTask(task, serverUrl, captureOptions, createBeforeCa
622
624
  };
623
625
  }
624
626
  catch (error) {
627
+ if (task.workerId === 0)
628
+ gate?.fail(error);
625
629
  const diagnostics = session ? selectWorkerDiagnostics(session.browserConsoleBuffer) : [];
626
630
  const workerDiagnostic = {
627
631
  workerId: task.workerId,
@@ -687,6 +691,23 @@ export function isPoolFatalWorkerFailure(failure, streaming) {
687
691
  return failure.kind !== "cancelled";
688
692
  return isFatalCaptureFailure(failure);
689
693
  }
694
+ function createFirstSessionGate(verify) {
695
+ let fail;
696
+ let pass;
697
+ const settled = new Promise((resolve, reject) => {
698
+ pass = resolve;
699
+ fail = reject;
700
+ });
701
+ settled.catch(() => { });
702
+ return {
703
+ verify: (session) => verify(session).then(pass, (error) => {
704
+ fail(error);
705
+ throw error;
706
+ }),
707
+ wait: () => settled,
708
+ fail,
709
+ };
710
+ }
690
711
  function pageErrorLinesOf(session) {
691
712
  return session.browserConsoleBuffer.filter((line) => line.includes("PAGEERROR"));
692
713
  }
@@ -777,7 +798,8 @@ export async function executeParallelCapture(serverUrl, workDir, tasks, captureO
777
798
  hooks,
778
799
  });
779
800
  const onFailure = failureHandler.onFailure;
780
- const results = await Promise.all(tasks.map((task) => executeWorkerTask(task, serverUrl, workerCaptureOptions, createBeforeCaptureHook, workerSignal, onFrameCaptured, onFrameBuffer, config, parallel, onFailure, onWorkerPhase, hooks?.onWorkerOperation)));
801
+ const gate = hooks?.verifyFirstSession && createFirstSessionGate(hooks.verifyFirstSession);
802
+ const results = await Promise.all(tasks.map((task) => executeWorkerTask(task, serverUrl, workerCaptureOptions, createBeforeCaptureHook, workerSignal, onFrameCaptured, onFrameBuffer, config, parallel, onFailure, onWorkerPhase, hooks?.onWorkerOperation, gate)));
781
803
  flagSilentWorkerExits(results);
782
804
  const errors = results.filter((r) => r.failure || r.error);
783
805
  if (errors.length > 0) {
@@ -188,6 +188,8 @@ export interface VideoExtractionFailure {
188
188
  * can contain a local path or a signed source URL.
189
189
  */
190
190
  error: string;
191
+ /** ffmpeg's last stderr lines, without paths or URLs: what a producer-facing error may say. */
192
+ ffmpegReason?: string;
191
193
  }
192
194
  export type VideoExtractionFailureStatusClass = "http_4xx" | "http_5xx" | "timeout" | "network" | "other";
193
195
  export interface VideoExtractionFailureRetry {
@@ -211,8 +213,9 @@ export declare class VideoSourceExtractionError extends Error {
211
213
  readonly kind: VideoExtractionFailureKind;
212
214
  readonly retryable: boolean;
213
215
  readonly diagnostic: string;
216
+ readonly ffmpegReason?: string | undefined;
214
217
  readonly hyperframesVideoSourceExtractionError: true;
215
- constructor(kind: VideoExtractionFailureKind, retryable: boolean, message: string, diagnostic?: string);
218
+ constructor(kind: VideoExtractionFailureKind, retryable: boolean, message: string, diagnostic?: string, ffmpegReason?: string | undefined);
216
219
  }
217
220
  export declare function isVideoSourceExtractionError(error: unknown): error is VideoSourceExtractionError;
218
221
  /**
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { audioGroupsById, isMemberGroupHidden, isSelfOrAncestorHidden } from "./mediaHidden.js";
9
9
  import { copyFileSync, existsSync, linkSync, mkdirSync, rmSync } from "fs";
10
- import { join } from "path";
10
+ import { basename, join } from "path";
11
11
  import { cpus } from "os";
12
12
  import { createConcurrencyLimit } from "../utils/concurrencyLimit.js";
13
13
  import { parseHTML } from "linkedom";
@@ -208,15 +208,34 @@ export class VideoSourceExtractionError extends Error {
208
208
  kind;
209
209
  retryable;
210
210
  diagnostic;
211
+ ffmpegReason;
211
212
  hyperframesVideoSourceExtractionError = true;
212
- constructor(kind, retryable, message, diagnostic = message) {
213
+ constructor(kind, retryable, message, diagnostic = message, ffmpegReason) {
213
214
  super(message);
214
215
  this.kind = kind;
215
216
  this.retryable = retryable;
216
217
  this.diagnostic = diagnostic;
218
+ this.ffmpegReason = ffmpegReason;
217
219
  this.name = "VideoSourceExtractionError";
218
220
  }
219
221
  }
222
+ const FFMPEG_REASON_LINES = 3;
223
+ const FFMPEG_REASON_CHARS = 300;
224
+ /** ffmpeg's last stderr lines for an error that leaves this host: no local path, file name or URL. */
225
+ function ffmpegReasonFrom(stderr, paths) {
226
+ let text = stderr;
227
+ for (const known of paths.filter(Boolean).sort((a, b) => b.length - a.length))
228
+ text = text.split(known).join("<path>");
229
+ return text
230
+ .replace(/[a-z][a-z\d+.-]*:\/\/[^\s'"]+/gi, "<url>")
231
+ .replace(/(?:[a-z]:)?(?:[\\/][^\s\\/:'"]+){2,}/gi, "<path>")
232
+ .split(/\r?\n/)
233
+ .map((line) => line.trim())
234
+ .filter(Boolean)
235
+ .slice(-FFMPEG_REASON_LINES)
236
+ .join(" | ")
237
+ .slice(-FFMPEG_REASON_CHARS);
238
+ }
220
239
  export function isVideoSourceExtractionError(error) {
221
240
  return (typeof error === "object" &&
222
241
  error !== null &&
@@ -745,13 +764,18 @@ outputDirOverride) {
745
764
  const timeoutSuffix = timedOut ? ` (no new frame for ${ffmpegProcessTimeout} ms)` : "";
746
765
  const diagnostic = `${hdrPrefix}FFmpeg exited with code ${processResult.exitCode}${timeoutSuffix}: ` +
747
766
  processResult.stderr.slice(-500);
767
+ const ffmpegReason = ffmpegReasonFrom(processResult.stderr, [
768
+ videoPath,
769
+ basename(videoPath),
770
+ videoOutputDir,
771
+ ]);
748
772
  if (timedOut) {
749
- throw new VideoSourceExtractionError("ffmpeg_timeout", true, "Video frame extraction timed out", diagnostic);
773
+ throw new VideoSourceExtractionError("ffmpeg_timeout", true, "Video frame extraction timed out", diagnostic, ffmpegReason);
750
774
  }
751
775
  const transientIo = /resource temporarily unavailable|device or resource busy|input\/output error/i.test(processResult.stderr);
752
776
  throw new VideoSourceExtractionError(transientIo ? "ffmpeg_transient" : "ffmpeg_failed", transientIo, transientIo
753
777
  ? "Video frame extraction hit a transient I/O failure"
754
- : "Video source could not be decoded", diagnostic);
778
+ : "Video source could not be decoded", diagnostic, ffmpegReason);
755
779
  }
756
780
  const framePaths = framePathsFromDirectory(videoOutputDir, format, !!options.frameRanges);
757
781
  if (framePaths.size === 0 && duration > 0) {
@@ -1223,13 +1247,9 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
1223
1247
  resolvedVideos.push({ video, videoPath });
1224
1248
  }
1225
1249
  catch (err) {
1226
- const classified = classifyVideoExtractionError(err);
1227
1250
  errors.push({
1228
- videoId: video.id,
1229
- kind: classified.kind,
1230
- retryable: classified.retryable,
1251
+ ...extractionError(video.id, err),
1231
1252
  ...(err instanceof UrlDownloadError ? { group: downloadFailureGroup(video.src, err) } : {}),
1232
- error: classified.diagnostic,
1233
1253
  });
1234
1254
  }
1235
1255
  }
@@ -1417,6 +1437,7 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
1417
1437
  kind: classified.kind,
1418
1438
  retryable: classified.retryable,
1419
1439
  error: classified.diagnostic,
1440
+ ...(classified.ffmpegReason ? { ffmpegReason: classified.ffmpegReason } : {}),
1420
1441
  };
1421
1442
  }
1422
1443
  function scopedExtractionOptions(work) {
@@ -1775,12 +1796,7 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
1775
1796
  ? `[shared extraction, leader ${outcome.error.videoId}] ${outcome.error.error}`
1776
1797
  : outcome.error.error;
1777
1798
  results.push({
1778
- error: {
1779
- videoId: prepared.work.video.id,
1780
- kind: outcome.error.kind,
1781
- retryable: outcome.error.retryable,
1782
- error: message,
1783
- },
1799
+ error: { ...outcome.error, videoId: prepared.work.video.id, error: message },
1784
1800
  });
1785
1801
  continue;
1786
1802
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.207",
3
+ "version": "0.8.208",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,6 +27,10 @@
27
27
  "import": "./dist/services/chromeHostCeiling.js",
28
28
  "types": "./dist/services/chromeHostCeiling.d.ts"
29
29
  },
30
+ "./chrome-startup": {
31
+ "import": "./dist/services/chromeStartup.js",
32
+ "types": "./dist/services/chromeStartup.d.ts"
33
+ },
30
34
  "./system-memory": {
31
35
  "import": "./dist/services/systemMemory.js",
32
36
  "types": "./dist/services/systemMemory.d.ts"
@@ -50,8 +54,8 @@
50
54
  "linkedom": "^0.18.12",
51
55
  "puppeteer": "^25.8.0",
52
56
  "puppeteer-core": "^25.8.0",
53
- "@hyperframes/core": "^0.8.207",
54
- "@hyperframes/parsers": "^0.8.207"
57
+ "@hyperframes/core": "^0.8.208",
58
+ "@hyperframes/parsers": "^0.8.208"
55
59
  },
56
60
  "devDependencies": {
57
61
  "@types/node": "^25.0.10",