@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.
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/services/audioMixer.d.ts.map +1 -1
- package/dist/services/audioMixer.js +187 -28
- package/dist/services/audioMixer.js.map +1 -1
- package/dist/services/audioMixer.types.d.ts +12 -0
- package/dist/services/audioMixer.types.d.ts.map +1 -1
- package/dist/services/captureWarning.d.ts +5 -0
- package/dist/services/captureWarning.d.ts.map +1 -0
- package/dist/services/captureWarning.js +22 -0
- package/dist/services/captureWarning.js.map +1 -0
- package/dist/services/chunkEncoder.d.ts +2 -0
- package/dist/services/chunkEncoder.d.ts.map +1 -1
- package/dist/services/chunkEncoder.js +6 -1
- package/dist/services/chunkEncoder.js.map +1 -1
- package/dist/services/frameCapture.d.ts.map +1 -1
- package/dist/services/frameCapture.js +6 -9
- package/dist/services/frameCapture.js.map +1 -1
- package/dist/services/screenshotService.d.ts.map +1 -1
- package/dist/services/screenshotService.js +10 -2
- package/dist/services/screenshotService.js.map +1 -1
- package/dist/services/videoFrameExtractor.d.ts +52 -4
- package/dist/services/videoFrameExtractor.d.ts.map +1 -1
- package/dist/services/videoFrameExtractor.js +257 -27
- package/dist/services/videoFrameExtractor.js.map +1 -1
- package/dist/services/videoFrameInjector.d.ts.map +1 -1
- package/dist/services/videoFrameInjector.js +0 -15
- package/dist/services/videoFrameInjector.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/ffprobe.d.ts.map +1 -1
- package/dist/utils/ffprobe.js +44 -17
- package/dist/utils/ffprobe.js.map +1 -1
- package/dist/utils/urlDownloader.d.ts +8 -1
- package/dist/utils/urlDownloader.d.ts.map +1 -1
- package/dist/utils/urlDownloader.js +283 -74
- package/dist/utils/urlDownloader.js.map +1 -1
- 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:
|
|
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":"AACA;;;;;GAKG;AAOH,OAAO,EAAwB,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAGL,KAAK,WAAW,EACjB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjE,OAAO,EAWL,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,aAAa,CAAC;IACxB,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,iCAAkC,CAAC;AACnE,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED,MAAM,WAAW,iBAAiB;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,gBAAgB,CAAC,EAAE,WAAW,CAAC;CAChC;AAUD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB;;sEAEkE;IAClE,oBAAoB,EAAE,MAAM,CAAC;IAC7B,wDAAwD;IACxD,gBAAgB,EAAE,MAAM,CAAC;IACzB,wCAAwC;IACxC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,wBAAwB,EAAE,MAAM,CAAC;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;
|
|
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
|
-
|
|
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
|
|
322
|
+
throw new VideoSourceExtractionError("cancelled", false, "Video extraction cancelled");
|
|
194
323
|
}
|
|
195
324
|
if (processResult.terminationReason === "spawn_error") {
|
|
196
|
-
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
|
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 =
|
|
826
|
+
const metadata = videoMetadata[i];
|
|
633
827
|
breakdown.vfrProbeMs += Date.now() - vfrProbeStart;
|
|
634
|
-
if (metadata
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
});
|