mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 17:27:09 +00:00
fix(engine): mux uppercase WebM exports with Opus audio (#6579)
* fix(engine): mux uppercase WebM exports with Opus audio * test(engine): terminate ffprobe options before input
This commit is contained in:
@@ -95,6 +95,10 @@ Paths passed to `encodeFramesChunkedConcat()` and `concatVideoFiles()` may be
|
||||
relative or absolute. Relative paths resolve from the calling process's working
|
||||
directory.
|
||||
|
||||
`muxVideoWithAudio()` recognizes output filename extensions regardless of case.
|
||||
WebM output, including `.WEBM` and `.WeBm`, converts audio to Opus while copying
|
||||
the video stream.
|
||||
|
||||
These are rendering internals. Their result and error conventions differ by
|
||||
operation: browser and capture orchestration throws, FFmpeg wrappers generally
|
||||
return result objects, and teardown helpers avoid masking the original failure.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { getFfmpegBinary, getFfprobeBinary } from "../utils/ffmpegBinaries.js";
|
||||
import { muxVideoWithAudio } from "./chunkEncoder.js";
|
||||
|
||||
const HAS_FFMPEG = spawnSync(getFfmpegBinary(), ["-version"]).status === 0;
|
||||
|
||||
function ffmpeg(args: string[]): Buffer {
|
||||
const result = spawnSync(getFfmpegBinary(), ["-nostdin", "-v", "error", "-y", ...args]);
|
||||
if (result.status !== 0) throw new Error(result.stderr.toString());
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
describe.skipIf(!HAS_FFMPEG)("WebM audio muxing", () => {
|
||||
let dir: string;
|
||||
let video: string;
|
||||
let audio: string;
|
||||
let sourceFrames: Buffer;
|
||||
|
||||
beforeAll(() => {
|
||||
dir = mkdtempSync(join(tmpdir(), "hf-mux-container-"));
|
||||
video = join(dir, "video.webm");
|
||||
audio = join(dir, "audio.m4a");
|
||||
ffmpeg([
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"testsrc2=size=64x36:rate=10:duration=1",
|
||||
"-c:v",
|
||||
"libvpx-vp9",
|
||||
"-deadline",
|
||||
"realtime",
|
||||
video,
|
||||
]);
|
||||
ffmpeg([
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"sine=frequency=440:sample_rate=48000:duration=1",
|
||||
"-c:a",
|
||||
"aac",
|
||||
audio,
|
||||
]);
|
||||
sourceFrames = ffmpeg(["-i", video, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"]);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (dir) rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it.each(["webm", "WEBM", "WeBm"])(
|
||||
"muxes AAC as Opus into .%s without changing the video frames",
|
||||
async (extension) => {
|
||||
const output = join(dir, `output.${extension}`);
|
||||
const result = await muxVideoWithAudio(video, audio, output, undefined, {
|
||||
audioCodec: "aac",
|
||||
});
|
||||
expect(result.success, result.error).toBe(true);
|
||||
|
||||
const probe = spawnSync(
|
||||
getFfprobeBinary(),
|
||||
["-v", "error", "-show_entries", "stream=codec_name", "-of", "csv=p=0", "--", output],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
expect(probe.status, probe.stderr).toBe(0);
|
||||
expect(probe.stdout.trim().split(/\r?\n/)).toEqual(["vp9", "opus"]);
|
||||
expect(ffmpeg(["-i", output, "-an", "-f", "rawvideo", "-pix_fmt", "rgb24", "-"])).toEqual(
|
||||
sourceFrames,
|
||||
);
|
||||
|
||||
const pcm = ffmpeg(["-i", output, "-vn", "-t", "0.5", "-ac", "1", "-f", "s16le", "-"]);
|
||||
let energy = 0;
|
||||
for (let i = 0; i < pcm.length; i += 2) energy += pcm.readInt16LE(i) ** 2;
|
||||
expect(Math.sqrt(energy / (pcm.length / 2))).toBeGreaterThan(100);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -857,8 +857,9 @@ export async function muxVideoWithAudio(
|
||||
const outputDir = dirname(outputPath);
|
||||
if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const isWebm = outputPath.endsWith(".webm");
|
||||
const isMov = outputPath.endsWith(".mov");
|
||||
const outputExtension = extname(outputPath).toLowerCase();
|
||||
const isWebm = outputExtension === ".webm";
|
||||
const isMov = outputExtension === ".mov";
|
||||
const shouldCopyAudio = isWebm ? false : await shouldCopyAacSidecar(audioPath, config);
|
||||
const args = [...(config?.videoInputArgs ?? ["-i", videoPath]), "-i", audioPath, "-c:v", "copy"];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user