mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(producer): copy extracted frames on Windows to avoid symlink EPERM
The local render path (renderOrchestrator → runExtractVideosStage) materialized each video's extracted frames into the compiled dir via symlinkSync, with no materializeSymlinks flag. On Windows without Developer Mode/Administrator, symlinkSync throws EPERM, so local video renders failed at the video_extract stage (users worked around it with materializeSymlinks patches / snapshot-frame hacks). The distributed plan() path already copies (materializeSymlinks: true). Pass materializeSymlinks: shouldCopyExtractedFrames(process.platform) at the local caller — copy on win32 (symlinks unavailable), symlink elsewhere (cheap). New pure shouldCopyExtractedFrames() helper + unit tests.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { appendAutoDetectedVideoAudio } from "./extractVideosStage.js";
|
import { appendAutoDetectedVideoAudio, shouldCopyExtractedFrames } from "./extractVideosStage.js";
|
||||||
import type { ExtractedFrames, VideoElement } from "@hyperframes/engine";
|
import type { ExtractedFrames, VideoElement } from "@hyperframes/engine";
|
||||||
|
|
||||||
function makeVideo(overrides: Partial<VideoElement> = {}): VideoElement {
|
function makeVideo(overrides: Partial<VideoElement> = {}): VideoElement {
|
||||||
@@ -81,3 +81,14 @@ describe("appendAutoDetectedVideoAudio", () => {
|
|||||||
expect(composition.audios).toHaveLength(1);
|
expect(composition.audios).toHaveLength(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("shouldCopyExtractedFrames", () => {
|
||||||
|
it("copies frames on Windows (symlinkSync throws EPERM without Developer Mode)", () => {
|
||||||
|
expect(shouldCopyExtractedFrames("win32")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("symlinks on macOS and Linux (cheaper, symlinks allowed)", () => {
|
||||||
|
expect(shouldCopyExtractedFrames("darwin")).toBe(false);
|
||||||
|
expect(shouldCopyExtractedFrames("linux")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -96,6 +96,18 @@ export interface ExtractVideosStageResult {
|
|||||||
videoExtractMs: number;
|
videoExtractMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the extract stage should COPY frames into the compiled dir instead of
|
||||||
|
* symlinking them. Windows without Developer Mode / Administrator can't create
|
||||||
|
* symlinks (`symlinkSync` throws EPERM), which failed local video renders; copy
|
||||||
|
* there instead. Elsewhere symlinking is cheaper, so keep it. (The distributed
|
||||||
|
* `plan()` path already forces copying for a different reason — a self-contained
|
||||||
|
* planDir — by passing `materializeSymlinks: true` explicitly.)
|
||||||
|
*/
|
||||||
|
export function shouldCopyExtractedFrames(platform: NodeJS.Platform): boolean {
|
||||||
|
return platform === "win32";
|
||||||
|
}
|
||||||
|
|
||||||
export async function runExtractVideosStage(
|
export async function runExtractVideosStage(
|
||||||
input: ExtractVideosStageInput,
|
input: ExtractVideosStageInput,
|
||||||
): Promise<ExtractVideosStageResult> {
|
): Promise<ExtractVideosStageResult> {
|
||||||
|
|||||||
@@ -110,7 +110,10 @@ import {
|
|||||||
import { type HdrPerfCollector, type HdrPerfSummary } from "./render/hdrPerf.js";
|
import { type HdrPerfCollector, type HdrPerfSummary } from "./render/hdrPerf.js";
|
||||||
import { runCompileStage } from "./render/stages/compileStage.js";
|
import { runCompileStage } from "./render/stages/compileStage.js";
|
||||||
import { runProbeStage } from "./render/stages/probeStage.js";
|
import { runProbeStage } from "./render/stages/probeStage.js";
|
||||||
import { runExtractVideosStage } from "./render/stages/extractVideosStage.js";
|
import {
|
||||||
|
runExtractVideosStage,
|
||||||
|
shouldCopyExtractedFrames,
|
||||||
|
} from "./render/stages/extractVideosStage.js";
|
||||||
import { runAudioStage } from "./render/stages/audioStage.js";
|
import { runAudioStage } from "./render/stages/audioStage.js";
|
||||||
import { runCaptureStage } from "./render/stages/captureStage.js";
|
import { runCaptureStage } from "./render/stages/captureStage.js";
|
||||||
import { runCaptureStreamingStage } from "./render/stages/captureStreamingStage.js";
|
import { runCaptureStreamingStage } from "./render/stages/captureStreamingStage.js";
|
||||||
@@ -1350,6 +1353,9 @@ export async function executeRenderJob(
|
|||||||
composition,
|
composition,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
assertNotAborted,
|
assertNotAborted,
|
||||||
|
// Copy (don't symlink) extracted frames on Windows — symlinkSync throws
|
||||||
|
// EPERM there without Developer Mode/admin, which failed local renders.
|
||||||
|
materializeSymlinks: shouldCopyExtractedFrames(process.platform),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const {
|
const {
|
||||||
|
|||||||
Reference in New Issue
Block a user