fix(engine): resolve root-absolute media from project (#2399)

This commit is contained in:
Miguel Ángel
2026-07-14 01:11:23 -04:00
committed by GitHub
parent 0dfc85b680
commit 6fc92308d6
4 changed files with 48 additions and 7 deletions
@@ -438,6 +438,38 @@ describe("processCompositionAudio", () => {
const prepareArgs = runFfmpegMock.mock.calls[0]?.[0]; const prepareArgs = runFfmpegMock.mock.calls[0]?.[0];
expect(prepareArgs).toContain(join(baseDir, "assets", filename)); expect(prepareArgs).toContain(join(baseDir, "assets", filename));
}); });
it("prepares browser root-absolute audio srcs from the project root", async () => {
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
tempDirs.push(baseDir, workDir);
mkdirSync(join(baseDir, ".media"), { recursive: true });
writeFileSync(join(baseDir, ".media", "tone.wav"), "stub");
const result = await processCompositionAudio(
[
{
id: "tone",
src: "/.media/tone.wav",
start: 0,
end: 1,
mediaStart: 0,
layer: 0,
volume: 1,
type: "audio",
},
],
baseDir,
workDir,
join(baseDir, "out.m4a"),
1,
);
expect(result.success).toBe(true);
expect(result.error).toBeUndefined();
expect(runFfmpegMock.mock.calls[0]?.[0]).toContain(join(baseDir, ".media", "tone.wav"));
});
}); });
describe("parseAudioElements — relative data-start resolution", () => { describe("parseAudioElements — relative data-start resolution", () => {
+2 -2
View File
@@ -5,7 +5,7 @@
*/ */
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, rmSync, writeFileSync } from "fs"; import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, rmSync, writeFileSync } from "fs";
import { isAbsolute, join, dirname } from "path"; import { join, dirname } from "path";
import { parseHTML } from "linkedom"; import { parseHTML } from "linkedom";
import { extractAudioMetadata } from "../utils/ffprobe.js"; import { extractAudioMetadata } from "../utils/ffprobe.js";
import { downloadToTemp, isHttpUrl } from "../utils/urlDownloader.js"; import { downloadToTemp, isHttpUrl } from "../utils/urlDownloader.js";
@@ -530,7 +530,7 @@ export async function processCompositionAudio(
} }
try { try {
let srcPath = element.src; let srcPath = element.src;
if (!isAbsolute(srcPath) && !isHttpUrl(srcPath)) { if (!isHttpUrl(srcPath)) {
// Same browser-vs-filesystem path semantics as videos — see // Same browser-vs-filesystem path semantics as videos — see
// resolveProjectRelativeSrc in videoFrameExtractor for the full why. // resolveProjectRelativeSrc in videoFrameExtractor for the full why.
srcPath = resolveProjectRelativeSrc(element.src, baseDir, compiledDir); srcPath = resolveProjectRelativeSrc(element.src, baseDir, compiledDir);
@@ -143,6 +143,13 @@ describe("resolveProjectRelativeSrc — sub-composition path clamping", () => {
); );
}); });
it("resolves a browser root-absolute URL from the project root", () => {
const projectDir = join(tmp, "project");
expect(resolveProjectRelativeSrc("/assets/foo.mp4", projectDir)).toBe(
join(projectDir, "assets/foo.mp4"),
);
});
it("clamps a leading `../` so `../assets/foo.mp4` resolves to assets/foo.mp4", () => { it("clamps a leading `../` so `../assets/foo.mp4` resolves to assets/foo.mp4", () => {
const projectDir = join(tmp, "project"); const projectDir = join(tmp, "project");
expect(resolveProjectRelativeSrc("../assets/foo.mp4", projectDir)).toBe( expect(resolveProjectRelativeSrc("../assets/foo.mp4", projectDir)).toBe(
@@ -670,6 +670,12 @@ export function resolveProjectRelativeSrc(
): string { ): string {
const qIdx = src.indexOf("?"); const qIdx = src.indexOf("?");
const cleanSrc = qIdx >= 0 ? src.slice(0, qIdx) : src; const cleanSrc = qIdx >= 0 ? src.slice(0, qIdx) : src;
// Preserve explicit filesystem paths when they really exist. Otherwise a
// leading slash is a browser origin-root URL (`/assets/foo.mp4`), which the
// file server serves from the project root rather than the host filesystem.
if (isAbsolute(cleanSrc) && existsSync(cleanSrc)) return cleanSrc;
const candidates: string[] = []; const candidates: string[] = [];
const addCandidate = (candidate: string): void => { const addCandidate = (candidate: string): void => {
@@ -746,11 +752,7 @@ export async function extractAllVideoFrames(
if (signal?.aborted) break; if (signal?.aborted) break;
try { try {
let videoPath = video.src; let videoPath = video.src;
// Use isAbsolute() rather than startsWith("/"). On Windows, absolute paths if (!isHttpUrl(videoPath)) {
// like "C:\…" are not detected by the latter, so we'd re-join them under
// baseDir and produce duplicated, nonexistent paths
// (e.g. C:\tmp\hf-vfr-test-X\C:\tmp\hf-vfr-test-X\vfr_screen.mp4).
if (!isAbsolute(videoPath) && !isHttpUrl(videoPath)) {
videoPath = resolveProjectRelativeSrc(video.src, baseDir, compiledDir); videoPath = resolveProjectRelativeSrc(video.src, baseDir, compiledDir);
} }