mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(producer): honor FFMPEG_PROCESS_TIMEOUT_MS during video-frame extraction (#3918)
* fix: honor FFMPEG_PROCESS_TIMEOUT_MS during video-frame extraction
FFMPEG_PROCESS_TIMEOUT_MS resolves correctly into EngineConfig and is
correctly forwarded to the audio-extraction ffmpeg call, but
runExtractVideosStage (shared by both the in-process renderer and
distributed plan()) built the config object it passes to
extractAllVideoFrames with only { extractCacheDir, extractCacheMaxBytes },
silently dropping ffmpegProcessTimeout even though the full EngineConfig
was already in scope. That config flows straight into
extractVideoFramesRange, which falls back to a hardcoded 300s deadline
whenever the field is undefined -- so video-frame extraction always
used a fixed 300s timeout no matter what the env var was set to.
Threads cfg.ffmpegProcessTimeout into that config literal the same way
the two cache fields already are.
Co-Authored-By: Miguel Angel <miguel.sierra@heygen.com>
* fix(producer): pass the whole engine config to video-frame extraction
The stage hand-picked fields from its resolved config, so any field it forgot
(the ffmpeg process timeout) silently fell back to the engine default.
---------
Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
This commit is contained in:
co-authored by
Miguel Angel
parent
9d6ca45857
commit
e0d4c96003
+109
@@ -0,0 +1,109 @@
|
||||
import { resolveConfig, type EngineConfig, type ExtractionResult } from "@hyperframes/engine";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
/** The optional engine-config argument the stage passes to `extractAllVideoFrames`. */
|
||||
type ExtractionConfigArg = Partial<EngineConfig> | undefined;
|
||||
|
||||
const capturedConfigs = vi.hoisted(() => new Array<ExtractionConfigArg>());
|
||||
|
||||
vi.mock("@hyperframes/engine", async (importOriginal) => {
|
||||
const real = await importOriginal<typeof import("@hyperframes/engine")>();
|
||||
return {
|
||||
...real,
|
||||
extractAllVideoFrames: async (
|
||||
_videos: unknown,
|
||||
_baseDir: unknown,
|
||||
_options: unknown,
|
||||
_signal: unknown,
|
||||
config: ExtractionConfigArg,
|
||||
): Promise<ExtractionResult> => {
|
||||
capturedConfigs.push(config);
|
||||
return {
|
||||
success: true,
|
||||
extracted: [],
|
||||
errors: [],
|
||||
totalFramesExtracted: 0,
|
||||
durationMs: 0,
|
||||
phaseBreakdown: {
|
||||
resolveMs: 0,
|
||||
cachePublishFailures: 0,
|
||||
cacheGcEvictions: 0,
|
||||
cacheGcBytesFreed: 0,
|
||||
cacheAgedPartialsCleared: 0,
|
||||
hdrProbeMs: 0,
|
||||
hdrPreflightMs: 0,
|
||||
hdrPreflightCount: 0,
|
||||
vfrProbeMs: 0,
|
||||
vfrPreflightMs: 0,
|
||||
vfrPreflightCount: 0,
|
||||
extractMs: 0,
|
||||
cacheHits: 0,
|
||||
cacheMisses: 0,
|
||||
transientRetries: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { createRenderJob } from "../../renderOrchestrator.js";
|
||||
import { runExtractVideosStage } from "./extractVideosStage.js";
|
||||
|
||||
async function runStage(cfg: EngineConfig): Promise<void> {
|
||||
const composition = {
|
||||
duration: 5,
|
||||
videos: [
|
||||
{
|
||||
id: "root-video",
|
||||
src: "clip.mp4",
|
||||
start: 0,
|
||||
end: 5,
|
||||
mediaStart: 0,
|
||||
loop: false,
|
||||
hasAudio: false,
|
||||
},
|
||||
],
|
||||
audios: [],
|
||||
images: [],
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
};
|
||||
await runExtractVideosStage({
|
||||
projectDir: "/tmp/hf-ffmpeg-timeout-project",
|
||||
compiledDir: "/tmp/hf-ffmpeg-timeout-compiled",
|
||||
job: createRenderJob({
|
||||
fps: { num: 30, den: 1 },
|
||||
quality: "standard",
|
||||
hdrMode: "force-sdr",
|
||||
}),
|
||||
cfg,
|
||||
composition,
|
||||
abortSignal: undefined,
|
||||
assertNotAborted: () => {},
|
||||
materializeSymlinks: false,
|
||||
});
|
||||
}
|
||||
|
||||
describe("video extraction ffmpeg timeout threading", () => {
|
||||
beforeEach(() => {
|
||||
capturedConfigs.splice(0);
|
||||
});
|
||||
|
||||
it("forwards every resolved extraction setting, including FFMPEG_PROCESS_TIMEOUT_MS", async () => {
|
||||
await runStage(
|
||||
resolveConfig({
|
||||
ffmpegProcessTimeout: 5_000,
|
||||
extractCacheDir: "/tmp/hf-ffmpeg-timeout-cache",
|
||||
extractCacheMaxBytes: 1234,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(capturedConfigs).toEqual([
|
||||
expect.objectContaining({
|
||||
ffmpegProcessTimeout: 5_000,
|
||||
extractCacheDir: "/tmp/hf-ffmpeg-timeout-cache",
|
||||
extractCacheMaxBytes: 1234,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -503,7 +503,7 @@ export async function runExtractVideosStage(
|
||||
collectProbeFailures: extractionPolicy.failureMode === "enforce",
|
||||
},
|
||||
abortSignal,
|
||||
{ extractCacheDir: cfg.extractCacheDir, extractCacheMaxBytes: cfg.extractCacheMaxBytes },
|
||||
cfg,
|
||||
compiledDir,
|
||||
);
|
||||
extractionResult.phaseBreakdown.transientRetries =
|
||||
|
||||
Reference in New Issue
Block a user