@hyperframes/producer 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.
package/dist/index.js CHANGED
@@ -9256,10 +9256,11 @@ var VideoExtractionStageError = class extends Error {
9256
9256
  kind: failure.kind,
9257
9257
  affectedElementCount: failure.count
9258
9258
  }))
9259
- )) {
9259
+ ), ffmpegReason) {
9260
9260
  const total = failures.reduce((sum2, failure) => sum2 + failure.count, 0);
9261
9261
  const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
9262
- super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]`);
9262
+ const reason = ffmpegReason ? `: ffmpeg said: ${ffmpegReason}` : "";
9263
+ super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]${reason}`);
9263
9264
  this.code = code;
9264
9265
  this.retryable = retryable;
9265
9266
  this.failures = failures;
@@ -9306,7 +9307,8 @@ function buildVideoExtractionStageError(result) {
9306
9307
  retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE",
9307
9308
  retryable,
9308
9309
  failures,
9309
- extractionFailureMetadataFromResult(result)
9310
+ extractionFailureMetadataFromResult(result),
9311
+ result.errors.find((failure) => failure.ffmpegReason)?.ffmpegReason
9310
9312
  );
9311
9313
  }
9312
9314
  function buildHdrProbeStageError(failures) {
@@ -10303,6 +10305,7 @@ async function runCaptureStage(input) {
10303
10305
  dedupPerfs,
10304
10306
  pageErrorLines: workerPageErrors,
10305
10307
  openAttempt: input.openAttempt,
10308
+ verifyFirstSession: input.verifyFirstSession,
10306
10309
  onProgress: (progress) => {
10307
10310
  if (progress.latestWorkerPhase) {
10308
10311
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -12362,7 +12365,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
12362
12365
  });
12363
12366
  } : void 0,
12364
12367
  void 0,
12365
- options.cfg
12368
+ options.cfg,
12369
+ { verifyFirstSession: options.verifyFirstSession }
12366
12370
  );
12367
12371
  pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
12368
12372
  if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
@@ -20245,16 +20249,18 @@ function observedChunkCaptureMode(perfs) {
20245
20249
  }
20246
20250
  return mode;
20247
20251
  }
20252
+ var PROBE_SELECTED_SCREENSHOT = "[renderChunk] probe selected screenshot capture";
20248
20253
  function shouldRetryChunkCaptureWithScreenshot(error) {
20249
20254
  const failure = classifyCaptureFailure4(error);
20250
20255
  if (failure.kind === "cancelled" || failure.kind === "memory_exhaustion") return false;
20251
- return /HeadlessExperimental\.beginFrame/i.test(failure.message) || /beginFrame probe timeout/i.test(failure.message) || /Another frame is pending|Frame still pending/i.test(failure.message);
20256
+ return /HeadlessExperimental\.beginFrame/i.test(failure.message) || /beginFrame probe timeout/i.test(failure.message) || failure.message.includes(PROBE_SELECTED_SCREENSHOT) || /Another frame is pending|Frame still pending/i.test(failure.message);
20252
20257
  }
20253
20258
  async function runCaptureWithScreenshotFallback(input) {
20254
20259
  try {
20255
20260
  return await input.run(input.forceScreenshot);
20256
20261
  } catch (error) {
20257
- if (input.forceScreenshot || !shouldRetryChunkCaptureWithScreenshot(error)) throw error;
20262
+ if (input.forceScreenshot || await input.canRetry?.() === false) throw error;
20263
+ if (!shouldRetryChunkCaptureWithScreenshot(error)) throw error;
20258
20264
  input.onFallback?.(error);
20259
20265
  await input.resetForScreenshotRetry();
20260
20266
  return await input.run(true);
@@ -20463,7 +20469,24 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20463
20469
  let extraction;
20464
20470
  const overlap = { current: null };
20465
20471
  try {
20466
- if (chunkWorkerCount === 1 || !forceScreenshotForChunk) {
20472
+ const verifyInWorker = chunkWorkerCount > 1 && !forceScreenshotForChunk;
20473
+ let boxesMeasured;
20474
+ let boxesFailed;
20475
+ const boxesReady = new Promise((resolve13, reject) => {
20476
+ boxesMeasured = resolve13;
20477
+ boxesFailed = reject;
20478
+ });
20479
+ boxesReady.catch(() => {
20480
+ });
20481
+ const measureBoxes = async (page) => {
20482
+ shownBoxes = await measureVideoShownBoxes(
20483
+ page,
20484
+ [...shownFrames.keys()],
20485
+ chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
20486
+ );
20487
+ boxesMeasured();
20488
+ };
20489
+ if (chunkWorkerCount === 1) {
20467
20490
  const bootStarted = Date.now();
20468
20491
  session = await createVerifiedDistributedCaptureSession(
20469
20492
  fileServer.url,
@@ -20472,11 +20495,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20472
20495
  cfg
20473
20496
  );
20474
20497
  sessionBootMs = Date.now() - bootStarted;
20475
- shownBoxes = await measureVideoShownBoxes(
20476
- session.page,
20477
- [...shownFrames.keys()],
20478
- chunkFrameNumbers.map((frame) => frame * chunkFps.den / chunkFps.num)
20479
- );
20498
+ await measureBoxes(session.page);
20480
20499
  const browserSelectedScreenshot = session.launchCaptureMode === "screenshot";
20481
20500
  const beginFrameStalled = !browserSelectedScreenshot && await beginFrameSessionNeedsScreenshotFallback(session);
20482
20501
  if (browserSelectedScreenshot) {
@@ -20485,10 +20504,6 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20485
20504
  "[renderChunk] Browser capability probe selected screenshot capture for the entire chunk",
20486
20505
  { chunkIndex }
20487
20506
  );
20488
- if (chunkWorkerCount > 1) {
20489
- await closeCaptureSession11(session);
20490
- session = null;
20491
- }
20492
20507
  } else if (beginFrameStalled) {
20493
20508
  forceScreenshotForChunk = true;
20494
20509
  log.warn(
@@ -20498,41 +20513,66 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20498
20513
  await closeCaptureSession11(session).catch(() => {
20499
20514
  });
20500
20515
  session = null;
20501
- if (chunkWorkerCount === 1) {
20502
- const screenshotBootStarted = Date.now();
20503
- const screenshotCfg = {
20504
- ...cfg,
20505
- forceScreenshot: true,
20506
- forceScreenshotExplicitlyOptedOut: false
20507
- };
20508
- session = await createVerifiedDistributedCaptureSession(
20509
- fileServer.url,
20510
- framesDir,
20511
- captureOptions,
20512
- screenshotCfg
20516
+ const screenshotBootStarted = Date.now();
20517
+ const screenshotCfg = {
20518
+ ...cfg,
20519
+ forceScreenshot: true,
20520
+ forceScreenshotExplicitlyOptedOut: false
20521
+ };
20522
+ session = await createVerifiedDistributedCaptureSession(
20523
+ fileServer.url,
20524
+ framesDir,
20525
+ captureOptions,
20526
+ screenshotCfg
20527
+ );
20528
+ sessionBootMs += Date.now() - screenshotBootStarted;
20529
+ }
20530
+ } else if (!verifyInWorker) {
20531
+ boxesMeasured();
20532
+ }
20533
+ let verification;
20534
+ const verifyFirstWorker = (first) => verification ??= (async () => {
20535
+ sessionBootMs = Date.now() - verifyStarted;
20536
+ try {
20537
+ await measureBoxes(first.page);
20538
+ const selectedScreenshot = first.launchCaptureMode === "screenshot";
20539
+ if (selectedScreenshot || await beginFrameSessionNeedsScreenshotFallback(first)) {
20540
+ forceScreenshotForChunk = true;
20541
+ log.warn(
20542
+ selectedScreenshot ? "[renderChunk] Browser capability probe selected screenshot capture for the entire chunk" : "[renderChunk] BeginFrame liveness probe failed; using screenshot capture for the entire chunk",
20543
+ { chunkIndex }
20513
20544
  );
20514
- sessionBootMs += Date.now() - screenshotBootStarted;
20545
+ throw new Error(PROBE_SELECTED_SCREENSHOT);
20515
20546
  }
20516
- } else if (chunkWorkerCount > 1) {
20517
- await closeCaptureSession11(session);
20518
- session = null;
20547
+ } catch (error) {
20548
+ if (!forceScreenshotForChunk) {
20549
+ verifyFailure = error;
20550
+ boxesFailed(error);
20551
+ }
20552
+ throw error;
20519
20553
  }
20520
- }
20521
- const extractStarted = Date.now();
20522
- const deferredVideoFrames = planVideos ? extractDeferredVideoFramesForChunk({
20523
- planDir,
20524
- planVideos,
20525
- frames: chunkFrameNumbers,
20526
- fps: chunkFps,
20527
- outputRoot: videoFramesDir,
20528
- cfg,
20529
- remoteSources,
20530
- shownBoxes
20531
- }).then((frames) => {
20532
- videoExtractMs = Date.now() - extractStarted;
20533
- return frames;
20534
- }) : Promise.resolve([]);
20554
+ })();
20555
+ let verifyStarted = 0;
20556
+ let verifyFailure;
20557
+ const deferredVideoFrames = planVideos ? boxesReady.then(() => {
20558
+ const extractStarted = Date.now();
20559
+ return extractDeferredVideoFramesForChunk({
20560
+ planDir,
20561
+ planVideos,
20562
+ frames: chunkFrameNumbers,
20563
+ fps: chunkFps,
20564
+ outputRoot: videoFramesDir,
20565
+ cfg,
20566
+ remoteSources,
20567
+ shownBoxes
20568
+ }).then((frames) => {
20569
+ videoExtractMs = Date.now() - extractStarted;
20570
+ return frames;
20571
+ });
20572
+ }) : boxesReady.then(() => []);
20535
20573
  extraction = deferredVideoFrames;
20574
+ deferredVideoFrames.catch(() => {
20575
+ });
20536
20576
  const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
20537
20577
  (deferred) => createFrameLookupTable5(
20538
20578
  planVideos.videos,
@@ -20611,6 +20651,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20611
20651
  const captureStarted = Date.now();
20612
20652
  captureMode = await runCaptureWithScreenshotFallback({
20613
20653
  forceScreenshot: forceScreenshotForChunk,
20654
+ canRetry: () => verifyFailure === void 0,
20614
20655
  // fallow-ignore-next-line complexity
20615
20656
  run: async (forceScreenshot) => {
20616
20657
  const captureCfg = cfg.forceScreenshot === forceScreenshot ? cfg : {
@@ -20640,6 +20681,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20640
20681
  }
20641
20682
  const captureSession = session;
20642
20683
  session = null;
20684
+ verifyStarted = Date.now();
20643
20685
  overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus3().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
20644
20686
  await observeRenderStage(
20645
20687
  new RenderObservabilityRecorder({ pipelineStartMs: start, log }),
@@ -20667,7 +20709,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20667
20709
  frameRange: range,
20668
20710
  onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
20669
20711
  interleaveFrames: interleavesCapture(overlap.current !== null, options),
20670
- openAttempt
20712
+ openAttempt,
20713
+ verifyFirstSession: forceScreenshot || !verifyInWorker ? void 0 : verifyFirstWorker
20671
20714
  }),
20672
20715
  { stall: frameStallRule(job) }
20673
20716
  ).catch(async (captureError) => {
@@ -20679,6 +20722,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20679
20722
  },
20680
20723
  resetForScreenshotRetry: () => {
20681
20724
  rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20725
+ boxesMeasured();
20682
20726
  mkdirSync19(framesDir, { recursive: true });
20683
20727
  capturePerfs.length = 0;
20684
20728
  captureAttempts.length = 0;
@@ -20694,6 +20738,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
20694
20738
  );
20695
20739
  }
20696
20740
  }).catch(async (captureError) => {
20741
+ if (verifyFailure !== void 0) throw verifyFailure;
20742
+ boxesFailed(captureError);
20697
20743
  await deferredVideoFrames;
20698
20744
  throw captureError;
20699
20745
  });
@@ -9242,10 +9242,11 @@ var VideoExtractionStageError = class extends Error {
9242
9242
  kind: failure.kind,
9243
9243
  affectedElementCount: failure.count
9244
9244
  }))
9245
- )) {
9245
+ ), ffmpegReason) {
9246
9246
  const total = failures.reduce((sum, failure) => sum + failure.count, 0);
9247
9247
  const breakdown = failures.map((failure) => `${failure.kind}=${failure.count}`).join(",");
9248
- super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]`);
9248
+ const reason = ffmpegReason ? `: ffmpeg said: ${ffmpegReason}` : "";
9249
+ super(`Video extraction failed for ${total} source(s) [${code}; ${breakdown}]${reason}`);
9249
9250
  this.code = code;
9250
9251
  this.retryable = retryable;
9251
9252
  this.failures = failures;
@@ -9287,7 +9288,8 @@ function buildVideoExtractionStageError(result) {
9287
9288
  retryable ? "VIDEO_EXTRACTION_FAILED" : "VIDEO_SOURCE_UNRENDERABLE",
9288
9289
  retryable,
9289
9290
  failures,
9290
- extractionFailureMetadataFromResult(result)
9291
+ extractionFailureMetadataFromResult(result),
9292
+ result.errors.find((failure) => failure.ffmpegReason)?.ffmpegReason
9291
9293
  );
9292
9294
  }
9293
9295
  function buildHdrProbeStageError(failures) {
@@ -10284,6 +10286,7 @@ async function runCaptureStage(input) {
10284
10286
  dedupPerfs,
10285
10287
  pageErrorLines: workerPageErrors,
10286
10288
  openAttempt: input.openAttempt,
10289
+ verifyFirstSession: input.verifyFirstSession,
10287
10290
  onProgress: (progress) => {
10288
10291
  if (progress.latestWorkerPhase) {
10289
10292
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -12343,7 +12346,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
12343
12346
  });
12344
12347
  } : void 0,
12345
12348
  void 0,
12346
- options.cfg
12349
+ options.cfg,
12350
+ { verifyFirstSession: options.verifyFirstSession }
12347
12351
  );
12348
12352
  pushWorkerDedupPerfs(workerResults, options.dedupPerfs);
12349
12353
  if (options.pageErrorLines) pushWorkerPageErrors(workerResults, options.pageErrorLines);
@@ -156,6 +156,8 @@ export declare function shouldRetryChunkCaptureWithScreenshot(error: unknown): b
156
156
  */
157
157
  export declare function runCaptureWithScreenshotFallback<T>(input: {
158
158
  forceScreenshot: boolean;
159
+ /** A failure that is not capture's (the probe's own) is never retried. */
160
+ canRetry?: () => boolean | Promise<boolean>;
159
161
  run: (forceScreenshot: boolean) => Promise<T>;
160
162
  resetForScreenshotRetry: () => Promise<void> | void;
161
163
  onFallback?: (error: unknown) => void;
@@ -88,6 +88,7 @@ export interface CaptureStageInput {
88
88
  interleaveFrames?: boolean;
89
89
  /** Each capture attempt opens one, so a stalled attempt is retried by the ladders below. */
90
90
  openAttempt?: OpenStallAttempt;
91
+ verifyFirstSession?: (session: CaptureSession) => Promise<void>;
91
92
  }
92
93
  export interface CaptureStageResult {
93
94
  /** Final worker count after any adaptive retry. */
@@ -117,7 +117,7 @@ export declare class VideoExtractionStageError extends Error {
117
117
  * validating and applying policy to it.
118
118
  */
119
119
  readonly publicMetadata: Readonly<Record<string, unknown>>;
120
- constructor(code: VideoExtractionStageErrorCode, retryable: boolean, failures: readonly VideoExtractionStageFailureSummary[], extractionFailure?: ExtractionFailureMetadataV1);
120
+ constructor(code: VideoExtractionStageErrorCode, retryable: boolean, failures: readonly VideoExtractionStageFailureSummary[], extractionFailure?: ExtractionFailureMetadataV1, ffmpegReason?: string);
121
121
  }
122
122
  export declare function assertVideoExtractionSucceeded(result: ExtractionResult): void;
123
123
  export declare function buildHdrProbeStageError(failures: readonly Pick<ReturnType<typeof classifyVideoExtractionError>, "kind" | "retryable">[]): VideoExtractionStageError;
@@ -610,6 +610,7 @@ export declare function executeDiskCaptureWithAdaptiveRetry(options: {
610
610
  /** Mutated in place: the workers' page-error console lines, for the render's error count. */
611
611
  pageErrorLines?: string[];
612
612
  openAttempt?: OpenStallAttempt;
613
+ verifyFirstSession?: (session: CaptureSession) => Promise<void>;
613
614
  }): Promise<CaptureAttemptSummary[]>;
614
615
  export type RenderConfigInput = Omit<RenderConfig, "fps"> & {
615
616
  fps: FpsInput;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.8.207",
3
+ "version": "0.8.208",
4
4
  "description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,11 +35,11 @@
35
35
  "postcss": "^8.4.0",
36
36
  "puppeteer": "^25.2.1",
37
37
  "puppeteer-core": "^25.2.1",
38
- "@hyperframes/engine": "0.8.207",
39
- "@hyperframes/core": "0.8.207",
40
- "@hyperframes/lint": "0.8.207",
41
- "@hyperframes/parsers": "0.8.207",
42
- "@hyperframes/studio-server": "0.8.207"
38
+ "@hyperframes/core": "0.8.208",
39
+ "@hyperframes/lint": "0.8.208",
40
+ "@hyperframes/parsers": "0.8.208",
41
+ "@hyperframes/studio-server": "0.8.208",
42
+ "@hyperframes/engine": "0.8.208"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.0.10",
@@ -48,7 +48,7 @@
48
48
  "tsx": "^4.21.0",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^4.1.11",
51
- "@hyperframes/aws-lambda": "0.8.207"
51
+ "@hyperframes/aws-lambda": "0.8.208"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"