@hyperframes/engine 0.7.71 → 0.7.73

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.
Files changed (40) hide show
  1. package/dist/index.d.ts +3 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/services/audioMixer.d.ts.map +1 -1
  6. package/dist/services/audioMixer.js +187 -28
  7. package/dist/services/audioMixer.js.map +1 -1
  8. package/dist/services/audioMixer.types.d.ts +12 -0
  9. package/dist/services/audioMixer.types.d.ts.map +1 -1
  10. package/dist/services/captureWarning.d.ts +5 -0
  11. package/dist/services/captureWarning.d.ts.map +1 -0
  12. package/dist/services/captureWarning.js +22 -0
  13. package/dist/services/captureWarning.js.map +1 -0
  14. package/dist/services/chunkEncoder.d.ts +2 -0
  15. package/dist/services/chunkEncoder.d.ts.map +1 -1
  16. package/dist/services/chunkEncoder.js +6 -1
  17. package/dist/services/chunkEncoder.js.map +1 -1
  18. package/dist/services/frameCapture.d.ts.map +1 -1
  19. package/dist/services/frameCapture.js +6 -9
  20. package/dist/services/frameCapture.js.map +1 -1
  21. package/dist/services/screenshotService.d.ts.map +1 -1
  22. package/dist/services/screenshotService.js +10 -2
  23. package/dist/services/screenshotService.js.map +1 -1
  24. package/dist/services/videoFrameExtractor.d.ts +52 -4
  25. package/dist/services/videoFrameExtractor.d.ts.map +1 -1
  26. package/dist/services/videoFrameExtractor.js +257 -27
  27. package/dist/services/videoFrameExtractor.js.map +1 -1
  28. package/dist/services/videoFrameInjector.d.ts.map +1 -1
  29. package/dist/services/videoFrameInjector.js +0 -15
  30. package/dist/services/videoFrameInjector.js.map +1 -1
  31. package/dist/types.d.ts +4 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/utils/ffprobe.d.ts.map +1 -1
  34. package/dist/utils/ffprobe.js +44 -17
  35. package/dist/utils/ffprobe.js.map +1 -1
  36. package/dist/utils/urlDownloader.d.ts +8 -1
  37. package/dist/utils/urlDownloader.d.ts.map +1 -1
  38. package/dist/utils/urlDownloader.js +283 -74
  39. package/dist/utils/urlDownloader.js.map +1 -1
  40. package/package.json +3 -3
@@ -50,6 +50,17 @@ export interface ExtractionOptions {
50
50
  quality?: number;
51
51
  format?: VideoFrameFormat;
52
52
  sdrToHdrTransfer?: HdrTransfer;
53
+ /**
54
+ * Bounded per-source FFmpeg retries. Default 0 preserves stable behavior;
55
+ * the producer may canary at most one retry after observing typed failures.
56
+ */
57
+ maxTransientRetries?: number;
58
+ /**
59
+ * Collect metadata-probe failures into `ExtractionResult.errors` instead
60
+ * of preserving the legacy Promise rejection. Default false; only the
61
+ * candidate enforce lane may opt into typed aggregation.
62
+ */
63
+ collectProbeFailures?: boolean;
53
64
  }
54
65
  /**
55
66
  * Per-phase timings and counters emitted by `extractAllVideoFrames`.
@@ -94,14 +105,50 @@ export interface ExtractionPhaseBreakdown {
94
105
  extractMs: number;
95
106
  cacheHits: number;
96
107
  cacheMisses: number;
108
+ /** Number of per-source transient failures retried inside this extraction. */
109
+ transientRetries?: number;
110
+ }
111
+ export type VideoExtractionFailureKind = "cancelled" | "source_missing" | "source_rejected" | "download_not_found" | "download_transient" | "invalid_media" | "media_start_out_of_range" | "ffmpeg_unavailable" | "ffmpeg_timeout" | "ffmpeg_transient" | "ffmpeg_failed" | "zero_output" | "internal";
112
+ export interface VideoExtractionFailure {
113
+ videoId: string;
114
+ /** Always populated by this engine version; optional for source compatibility with older consumers. */
115
+ kind?: VideoExtractionFailureKind;
116
+ /** Always populated by this engine version; absent legacy values fail closed. */
117
+ retryable?: boolean;
118
+ /**
119
+ * Operator diagnostic retained inside the engine result. Producer-facing
120
+ * errors must summarize `kind`/counts and must not forward this field: it
121
+ * can contain a local path or a signed source URL.
122
+ */
123
+ error: string;
124
+ }
125
+ export declare class VideoSourceExtractionError extends Error {
126
+ readonly kind: VideoExtractionFailureKind;
127
+ readonly retryable: boolean;
128
+ readonly diagnostic: string;
129
+ readonly hyperframesVideoSourceExtractionError: true;
130
+ constructor(kind: VideoExtractionFailureKind, retryable: boolean, message: string, diagnostic?: string);
97
131
  }
132
+ export declare function isVideoSourceExtractionError(error: unknown): error is VideoSourceExtractionError;
133
+ /**
134
+ * Convert legacy/raw downloader and filesystem errors into the bounded
135
+ * extraction taxonomy. New extraction code should throw
136
+ * `VideoSourceExtractionError` directly; this classifier keeps older utility
137
+ * boundaries safe while they migrate.
138
+ */
139
+ export declare function classifyVideoExtractionError(error: unknown): VideoSourceExtractionError;
140
+ export declare function runVideoExtractionWithRetry<T>(operation: () => Promise<T>, options?: {
141
+ signal?: AbortSignal;
142
+ onRetry?: () => Promise<void> | void;
143
+ maxTransientRetries?: number;
144
+ }): Promise<{
145
+ result: T;
146
+ retries: number;
147
+ }>;
98
148
  export interface ExtractionResult {
99
149
  success: boolean;
100
150
  extracted: ExtractedFrames[];
101
- errors: Array<{
102
- videoId: string;
103
- error: string;
104
- }>;
151
+ errors: VideoExtractionFailure[];
105
152
  totalFramesExtracted: number;
106
153
  durationMs: number;
107
154
  phaseBreakdown: ExtractionPhaseBreakdown;
@@ -122,6 +169,7 @@ export declare function extractVideoFramesRange(videoPath: string, videoId: stri
122
169
  * cache entry directory.
123
170
  */
124
171
  outputDirOverride?: string): Promise<ExtractedFrames>;
172
+ export declare function classifyFfmpegSpawnError(error: unknown, stderr?: string): VideoSourceExtractionError;
125
173
  export declare function codecMayHaveAlpha(codec: string | undefined): boolean;
126
174
  export declare function decoderForCodec(codec: string | undefined): string;
127
175
  export declare function resolveFrameFormat(metadata: VideoMetadata, requested?: VideoFrameFormat): CacheFrameFormat;
@@ -1 +1 @@
1
- {"version":3,"file":"videoFrameExtractor.d.ts","sourceRoot":"","sources":["../../src/services/videoFrameExtractor.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"videoFrameExtractor.d.ts","sourceRoot":"","sources":["../../src/services/videoFrameExtractor.ts"],"names":[],"mappings":"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"}
@@ -12,7 +12,7 @@ import { decodeUrlPathVariants, MEDIA_DURATION_CLAMP_EPSILON_SECONDS } from "@hy
12
12
  import { resolveReferencedStart } from "./referenceResolver.js";
13
13
  import { extractMediaMetadata } from "../utils/ffprobe.js";
14
14
  import { analyzeCompositionHdr, isHdrColorSpace as isHdrColorSpaceUtil, } from "../utils/hdr.js";
15
- import { downloadToTemp, isHttpUrl } from "../utils/urlDownloader.js";
15
+ import { downloadToTemp, isHttpUrl, UrlDownloadError } from "../utils/urlDownloader.js";
16
16
  import { runFfmpeg } from "../utils/runFfmpeg.js";
17
17
  import { DEFAULT_CONFIG } from "../config.js";
18
18
  import { unwrapTemplate } from "../utils/htmlTemplate.js";
@@ -34,6 +34,123 @@ const SDR_TO_HDR_COLORSPACE_FILTER = "colorspace=all=bt2020:iall=bt709:range=tv"
34
34
  function sdrToHdrTransformKey(transfer) {
35
35
  return `sdr2hdr-${transfer}`;
36
36
  }
37
+ export class VideoSourceExtractionError extends Error {
38
+ kind;
39
+ retryable;
40
+ diagnostic;
41
+ hyperframesVideoSourceExtractionError = true;
42
+ constructor(kind, retryable, message, diagnostic = message) {
43
+ super(message);
44
+ this.kind = kind;
45
+ this.retryable = retryable;
46
+ this.diagnostic = diagnostic;
47
+ this.name = "VideoSourceExtractionError";
48
+ }
49
+ }
50
+ export function isVideoSourceExtractionError(error) {
51
+ return (typeof error === "object" &&
52
+ error !== null &&
53
+ "hyperframesVideoSourceExtractionError" in error &&
54
+ error.hyperframesVideoSourceExtractionError === true);
55
+ }
56
+ function boundedTransientRetryBudget(value) {
57
+ return Number.isFinite(value) && (value ?? 0) >= 1 ? 1 : 0;
58
+ }
59
+ function errorText(error) {
60
+ return error instanceof Error ? error.message : String(error);
61
+ }
62
+ /**
63
+ * Convert legacy/raw downloader and filesystem errors into the bounded
64
+ * extraction taxonomy. New extraction code should throw
65
+ * `VideoSourceExtractionError` directly; this classifier keeps older utility
66
+ * boundaries safe while they migrate.
67
+ */
68
+ export function classifyVideoExtractionError(error) {
69
+ if (isVideoSourceExtractionError(error))
70
+ return error;
71
+ const diagnostic = errorText(error);
72
+ const lowered = diagnostic.toLowerCase();
73
+ if (error instanceof UrlDownloadError) {
74
+ if (error.kind === "cancelled") {
75
+ return new VideoSourceExtractionError("cancelled", false, "Video extraction cancelled", diagnostic);
76
+ }
77
+ if (error.kind === "http_not_found") {
78
+ return new VideoSourceExtractionError("download_not_found", false, "Video source was not found", diagnostic);
79
+ }
80
+ if (error.kind === "http_rejected") {
81
+ return new VideoSourceExtractionError("source_rejected", false, "Video source download was rejected", diagnostic);
82
+ }
83
+ if (error.retryable) {
84
+ return new VideoSourceExtractionError("download_transient", true, "Video source download failed transiently", diagnostic);
85
+ }
86
+ return new VideoSourceExtractionError("internal", false, "Video source download failed internally", diagnostic);
87
+ }
88
+ if (lowered.includes("cancelled") || lowered.includes("aborted")) {
89
+ return new VideoSourceExtractionError("cancelled", false, "Video extraction cancelled", diagnostic);
90
+ }
91
+ if (lowered.includes("video file not found")) {
92
+ return new VideoSourceExtractionError("source_missing", false, "Video source is missing", diagnostic);
93
+ }
94
+ if (lowered.includes("only https urls are permitted") ||
95
+ lowered.includes("private/reserved address") ||
96
+ lowered.includes("invalid url")) {
97
+ return new VideoSourceExtractionError("source_rejected", false, "Video source URL is not permitted", diagnostic);
98
+ }
99
+ const httpStatus = diagnostic.match(/\bHTTP\s+(\d{3})\b/i)?.[1];
100
+ if (httpStatus) {
101
+ const status = Number(httpStatus);
102
+ if (status === 404 || status === 410) {
103
+ return new VideoSourceExtractionError("download_not_found", false, "Video source was not found", diagnostic);
104
+ }
105
+ if (status === 408 || status === 429 || status >= 500) {
106
+ return new VideoSourceExtractionError("download_transient", true, "Video source download failed transiently", diagnostic);
107
+ }
108
+ return new VideoSourceExtractionError("source_rejected", false, "Video source download was rejected", diagnostic);
109
+ }
110
+ if (lowered.includes("[urldownloader] download timeout") ||
111
+ lowered.includes("[urldownloader] download failed") ||
112
+ lowered.includes("fetch failed") ||
113
+ lowered.includes("network")) {
114
+ return new VideoSourceExtractionError("download_transient", true, "Video source download failed transiently", diagnostic);
115
+ }
116
+ if (lowered.includes("ffprobe not found")) {
117
+ return new VideoSourceExtractionError("ffmpeg_unavailable", false, "FFprobe is unavailable", diagnostic);
118
+ }
119
+ if (lowered.includes("ffprobe deadline")) {
120
+ return new VideoSourceExtractionError("ffmpeg_timeout", true, "Video inspection timed out", diagnostic);
121
+ }
122
+ if (lowered.includes("ffprobe") ||
123
+ lowered.includes("failed to parse ffprobe output") ||
124
+ lowered.includes("no video stream found")) {
125
+ return new VideoSourceExtractionError("invalid_media", false, "Video source could not be inspected", diagnostic);
126
+ }
127
+ return new VideoSourceExtractionError("internal", false, "Video extraction failed internally", diagnostic);
128
+ }
129
+ export async function runVideoExtractionWithRetry(operation, options = {}) {
130
+ const maxTransientRetries = boundedTransientRetryBudget(options.maxTransientRetries);
131
+ let retries = 0;
132
+ for (;;) {
133
+ if (options.signal?.aborted) {
134
+ throw new VideoSourceExtractionError("cancelled", false, "Video extraction cancelled");
135
+ }
136
+ try {
137
+ return { result: await operation(), retries };
138
+ }
139
+ catch (error) {
140
+ const classified = classifyVideoExtractionError(error);
141
+ if (options.signal?.aborted) {
142
+ throw new VideoSourceExtractionError("cancelled", false, "Video extraction cancelled", classified.diagnostic);
143
+ }
144
+ if (classified.kind === "cancelled" ||
145
+ !classified.retryable ||
146
+ retries >= maxTransientRetries) {
147
+ throw classified;
148
+ }
149
+ retries += 1;
150
+ await options.onRetry?.();
151
+ }
152
+ }
153
+ }
37
154
  export function parseVideoElements(html) {
38
155
  const videos = [];
39
156
  const { document } = parseHTML(unwrapTemplate(html));
@@ -134,7 +251,19 @@ outputDirOverride) {
134
251
  const videoOutputDir = outputDirOverride ?? join(outputDir, videoId);
135
252
  if (!existsSync(videoOutputDir))
136
253
  mkdirSync(videoOutputDir, { recursive: true });
137
- const metadata = await extractMediaMetadata(videoPath);
254
+ let metadata;
255
+ try {
256
+ metadata = await extractMediaMetadata(videoPath);
257
+ }
258
+ catch (error) {
259
+ throw classifyVideoExtractionError(error);
260
+ }
261
+ if (!(metadata.durationSeconds > 0)) {
262
+ throw new VideoSourceExtractionError("invalid_media", false, "Video source has no positive duration", `Video source duration is ${metadata.durationSeconds}s`);
263
+ }
264
+ if (startTime >= metadata.durationSeconds) {
265
+ throw new VideoSourceExtractionError("media_start_out_of_range", false, "Video media start is outside the source duration", `Video media start ${startTime}s is outside source duration ${metadata.durationSeconds}s`);
266
+ }
138
267
  const format = resolveFrameFormat(metadata, options.format);
139
268
  const framePattern = `${FRAME_FILENAME_PREFIX}%05d.${format}`;
140
269
  const outputPattern = join(videoOutputDir, framePattern);
@@ -190,13 +319,10 @@ outputDirOverride) {
190
319
  args.push("-y", outputPattern);
191
320
  const processResult = await runFfmpeg(args, { signal, timeout: ffmpegProcessTimeout });
192
321
  if (processResult.terminationReason === "abort") {
193
- throw new Error("Video frame extraction cancelled");
322
+ throw new VideoSourceExtractionError("cancelled", false, "Video extraction cancelled");
194
323
  }
195
324
  if (processResult.terminationReason === "spawn_error") {
196
- if (processResult.error?.code === "ENOENT") {
197
- throw new Error("[FFmpeg] ffmpeg not found");
198
- }
199
- throw processResult.error ?? new Error(processResult.stderr);
325
+ throw classifyFfmpegSpawnError(processResult.error, processResult.stderr);
200
326
  }
201
327
  if (!processResult.success) {
202
328
  // With the SDR-to-HDR remap folded into this pass, a filter failure
@@ -206,10 +332,17 @@ outputDirOverride) {
206
332
  const hdrPrefix = options.sdrToHdrTransfer
207
333
  ? `SDR→HDR conversion failed (colorspace filter in extract pass, target ${options.sdrToHdrTransfer}): `
208
334
  : "";
209
- const timeoutSuffix = processResult.terminationReason === "deadline"
210
- ? ` (timed out after ${ffmpegProcessTimeout} ms)`
211
- : "";
212
- throw new Error(`${hdrPrefix}FFmpeg exited with code ${processResult.exitCode}${timeoutSuffix}: ${processResult.stderr.slice(-500)}`);
335
+ const timedOut = processResult.terminationReason === "deadline";
336
+ const timeoutSuffix = timedOut ? ` (timed out after ${ffmpegProcessTimeout} ms)` : "";
337
+ const diagnostic = `${hdrPrefix}FFmpeg exited with code ${processResult.exitCode}${timeoutSuffix}: ` +
338
+ processResult.stderr.slice(-500);
339
+ if (timedOut) {
340
+ throw new VideoSourceExtractionError("ffmpeg_timeout", true, "Video frame extraction timed out", diagnostic);
341
+ }
342
+ const transientIo = /resource temporarily unavailable|device or resource busy|input\/output error/i.test(processResult.stderr);
343
+ throw new VideoSourceExtractionError(transientIo ? "ffmpeg_transient" : "ffmpeg_failed", transientIo, transientIo
344
+ ? "Video frame extraction hit a transient I/O failure"
345
+ : "Video source could not be decoded", diagnostic);
213
346
  }
214
347
  const framePaths = new Map();
215
348
  const files = readdirSync(videoOutputDir)
@@ -218,6 +351,9 @@ outputDirOverride) {
218
351
  files.forEach((file, index) => {
219
352
  framePaths.set(index, join(videoOutputDir, file));
220
353
  });
354
+ if (framePaths.size === 0 && duration > 0) {
355
+ throw new VideoSourceExtractionError("zero_output", false, "Video source produced no decodable frames", `FFmpeg exited successfully but produced no frames (start=${startTime}, duration=${duration})`);
356
+ }
221
357
  return {
222
358
  videoId,
223
359
  srcPath: videoPath,
@@ -229,6 +365,20 @@ outputDirOverride) {
229
365
  framePaths,
230
366
  };
231
367
  }
368
+ const TRANSIENT_FFMPEG_SPAWN_CODES = new Set(["EAGAIN", "EMFILE", "ENFILE"]);
369
+ export function classifyFfmpegSpawnError(error, stderr = "") {
370
+ const code = typeof error === "object" && error !== null && "code" in error && typeof error.code === "string"
371
+ ? error.code
372
+ : "";
373
+ if (code === "ENOENT") {
374
+ return new VideoSourceExtractionError("ffmpeg_unavailable", false, "FFmpeg is unavailable", "[FFmpeg] ffmpeg not found");
375
+ }
376
+ const diagnostic = error instanceof Error ? error.message : stderr;
377
+ const retryable = TRANSIENT_FFMPEG_SPAWN_CODES.has(code);
378
+ return new VideoSourceExtractionError(retryable ? "ffmpeg_transient" : "ffmpeg_failed", retryable, retryable
379
+ ? "FFmpeg could not be started due to transient resource pressure"
380
+ : "FFmpeg could not be started", diagnostic);
381
+ }
232
382
  /**
233
383
  * Resolve the used-segment duration for a video, falling back to the source's
234
384
  * natural duration when the caller hasn't specified bounds (end=Infinity) or
@@ -481,6 +631,10 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
481
631
  extractMs: 0,
482
632
  cacheHits: 0,
483
633
  cacheMisses: 0,
634
+ transientRetries: 0,
635
+ };
636
+ const recordTransientRetries = (count) => {
637
+ breakdown.transientRetries = (breakdown.transientRetries ?? 0) + count;
484
638
  };
485
639
  // Phase 1: Resolve paths and download remote videos
486
640
  const phase1Start = Date.now();
@@ -499,7 +653,7 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
499
653
  if (isHttpUrl(videoPath)) {
500
654
  const downloadDir = join(options.outputDir, "_downloads");
501
655
  mkdirSync(downloadDir, { recursive: true });
502
- videoPath = await downloadToTemp(videoPath, downloadDir);
656
+ videoPath = await downloadToTemp(videoPath, downloadDir, undefined, signal, () => recordTransientRetries(1));
503
657
  }
504
658
  if (!existsSync(videoPath)) {
505
659
  // Loud: silent miss leaves the rendered video frozen at frame 0 with
@@ -513,13 +667,24 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
513
667
  `If your <video> lives inside a sub-composition, prefer project-root-relative paths ` +
514
668
  `(e.g. src="assets/foo.mp4") over "../assets/foo.mp4".\n`);
515
669
  }
516
- errors.push({ videoId: video.id, error: `Video file not found: ${videoPath}` });
670
+ errors.push({
671
+ videoId: video.id,
672
+ kind: "source_missing",
673
+ retryable: false,
674
+ error: `Video file not found: ${videoPath}`,
675
+ });
517
676
  continue;
518
677
  }
519
678
  resolvedVideos.push({ video, videoPath });
520
679
  }
521
680
  catch (err) {
522
- errors.push({ videoId: video.id, error: err instanceof Error ? err.message : String(err) });
681
+ const classified = classifyVideoExtractionError(err);
682
+ errors.push({
683
+ videoId: video.id,
684
+ kind: classified.kind,
685
+ retryable: classified.retryable,
686
+ error: classified.diagnostic,
687
+ });
523
688
  }
524
689
  }
525
690
  breakdown.resolveMs = Date.now() - phase1Start;
@@ -547,7 +712,37 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
547
712
  });
548
713
  // Phase 2: Probe color spaces and normalize if mixed HDR/SDR
549
714
  const phase2ProbeStart = Date.now();
550
- const videoMetadata = await Promise.all(resolvedVideos.map(({ videoPath }) => extractMediaMetadata(videoPath)));
715
+ const metadataResults = await Promise.all(resolvedVideos.map(async ({ video, videoPath }, index) => {
716
+ try {
717
+ // Keep the default/off path byte-for-byte compatible with the legacy
718
+ // Promise.all rejection. Classification is introduced only when a
719
+ // bounded retry or explicit typed aggregation is enabled.
720
+ const attempted = !options.collectProbeFailures &&
721
+ boundedTransientRetryBudget(options.maxTransientRetries) === 0
722
+ ? { result: await extractMediaMetadata(videoPath), retries: 0 }
723
+ : await runVideoExtractionWithRetry(() => extractMediaMetadata(videoPath), {
724
+ signal,
725
+ maxTransientRetries: options.maxTransientRetries,
726
+ onRetry: () => recordTransientRetries(1),
727
+ });
728
+ return {
729
+ video,
730
+ videoPath,
731
+ metadata: attempted.result,
732
+ cacheKeyInput: cacheKeyInputs[index] ?? null,
733
+ };
734
+ }
735
+ catch (error) {
736
+ if (!options.collectProbeFailures)
737
+ throw error;
738
+ errors.push(extractionError(video.id, error));
739
+ return null;
740
+ }
741
+ }));
742
+ const probedVideos = metadataResults.filter((entry) => entry !== null);
743
+ resolvedVideos.splice(0, resolvedVideos.length, ...probedVideos.map(({ video, videoPath }) => ({ video, videoPath })));
744
+ cacheKeyInputs.splice(0, cacheKeyInputs.length, ...probedVideos.map(({ cacheKeyInput }) => cacheKeyInput));
745
+ const videoMetadata = probedVideos.map(({ metadata }) => metadata);
551
746
  const videoColorSpaces = videoMetadata.map((m) => m.colorSpace);
552
747
  // Canonical per-index record of the SDR-to-HDR transform decision. BOTH the
553
748
  // cache key (transform discriminator) and the extraction options read from
@@ -591,6 +786,8 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
591
786
  if (entry.video.mediaStart >= metadata.durationSeconds) {
592
787
  errors.push({
593
788
  videoId: entry.video.id,
789
+ kind: "media_start_out_of_range",
790
+ retryable: false,
594
791
  error: `SDR→HDR conversion skipped: mediaStart (${entry.video.mediaStart}s) ≥ source duration (${metadata.durationSeconds}s)`,
595
792
  });
596
793
  hdrSkippedIndices.add(i);
@@ -625,13 +822,10 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
625
822
  for (let i = 0; i < resolvedVideos.length; i++) {
626
823
  if (signal?.aborted)
627
824
  break;
628
- const entry = resolvedVideos[i];
629
- if (!entry)
630
- continue;
631
825
  const vfrProbeStart = Date.now();
632
- const metadata = await extractMediaMetadata(entry.videoPath);
826
+ const metadata = videoMetadata[i];
633
827
  breakdown.vfrProbeMs += Date.now() - vfrProbeStart;
634
- if (metadata.isVFR)
828
+ if (metadata?.isVFR)
635
829
  breakdown.vfrPreflightCount += 1;
636
830
  }
637
831
  breakdown.vfrPreflightMs = Date.now() - vfrPreflightStart;
@@ -648,7 +842,13 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
648
842
  }
649
843
  }
650
844
  function extractionError(videoId, err) {
651
- return { videoId, error: err instanceof Error ? err.message : String(err) };
845
+ const classified = classifyVideoExtractionError(err);
846
+ return {
847
+ videoId,
848
+ kind: classified.kind,
849
+ retryable: classified.retryable,
850
+ error: classified.diagnostic,
851
+ };
652
852
  }
653
853
  function scopedExtractionOptions(work) {
654
854
  return { ...options, format: work.format, sdrToHdrTransfer: work.sdrToHdrTransfer };
@@ -693,14 +893,33 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
693
893
  result: rehydratePublishedCache(work, { entry: lookup.entry, srcPath: keyInput.videoPath }),
694
894
  };
695
895
  }
696
- async function extractDirectMiss(miss) {
896
+ async function extractDirectMiss(miss, maxTransientRetries = options.maxTransientRetries ?? 0) {
697
897
  const { work, cacheTarget } = miss;
698
898
  if (!cacheTarget) {
699
- return extractVideoFramesRange(work.videoPath, work.video.id, work.video.mediaStart, work.videoDuration, scopedExtractionOptions(work), signal, config);
899
+ const outputDir = join(options.outputDir, work.video.id);
900
+ const attempted = await runVideoExtractionWithRetry(() => extractVideoFramesRange(work.videoPath, work.video.id, work.video.mediaStart, work.videoDuration, scopedExtractionOptions(work), signal, config), {
901
+ signal,
902
+ maxTransientRetries,
903
+ onRetry: () => {
904
+ recordTransientRetries(1);
905
+ rmSync(outputDir, { recursive: true, force: true });
906
+ },
907
+ });
908
+ return attempted.result;
700
909
  }
701
910
  const partialDir = partialCacheEntryDir(cacheTarget.entry);
911
+ rmSync(partialDir, { recursive: true, force: true });
702
912
  mkdirSync(partialDir, { recursive: true });
703
- const result = await extractVideoFramesRange(work.videoPath, work.video.id, work.video.mediaStart, work.videoDuration, scopedExtractionOptions(work), signal, config, partialDir);
913
+ const attempted = await runVideoExtractionWithRetry(() => extractVideoFramesRange(work.videoPath, work.video.id, work.video.mediaStart, work.videoDuration, scopedExtractionOptions(work), signal, config, partialDir), {
914
+ signal,
915
+ maxTransientRetries,
916
+ onRetry: () => {
917
+ recordTransientRetries(1);
918
+ rmSync(partialDir, { recursive: true, force: true });
919
+ mkdirSync(partialDir, { recursive: true });
920
+ },
921
+ });
922
+ const result = attempted.result;
704
923
  const published = publishCacheEntry(cacheTarget.entry, partialDir);
705
924
  if (!published.published) {
706
925
  breakdown.cachePublishFailures += 1;
@@ -708,9 +927,9 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
708
927
  }
709
928
  return rehydratePublishedCache(work, cacheTarget);
710
929
  }
711
- async function executeDirectMiss(miss) {
930
+ async function executeDirectMiss(miss, maxTransientRetries = options.maxTransientRetries ?? 0) {
712
931
  try {
713
- return { result: await extractDirectMiss(miss) };
932
+ return { result: await extractDirectMiss(miss, maxTransientRetries) };
714
933
  }
715
934
  catch (err) {
716
935
  return { error: extractionError(miss.work.video.id, err) };
@@ -747,6 +966,10 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
747
966
  : join(options.outputDir, group.groupId);
748
967
  try {
749
968
  rmSync(tempDir, { recursive: true, force: true });
969
+ // A long union can hit the fixed FFmpeg deadline even when each shorter
970
+ // member range succeeds. Do not retry the optimization itself; preserve
971
+ // the established grouped→direct fallback and apply bounded retries only
972
+ // to the individual source ranges below.
750
973
  const superset = await extractVideoFramesRange(first.videoPath, group.groupId, group.baseStart, group.unionDuration, scopedExtractionOptions(first), signal, config, tempDir);
751
974
  const outcomes = [];
752
975
  for (const member of group.members) {
@@ -844,7 +1067,14 @@ export async function extractAllVideoFrames(videos, baseDir, options, signal, co
844
1067
  const message = isFollower
845
1068
  ? `[shared extraction, leader ${outcome.error.videoId}] ${outcome.error.error}`
846
1069
  : outcome.error.error;
847
- return { error: { videoId: prepared.work.video.id, error: message } };
1070
+ return {
1071
+ error: {
1072
+ videoId: prepared.work.video.id,
1073
+ kind: outcome.error.kind,
1074
+ retryable: outcome.error.retryable,
1075
+ error: message,
1076
+ },
1077
+ };
848
1078
  }
849
1079
  return { result: { ...outcome.result, videoId: prepared.work.video.id } };
850
1080
  });