mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
fix(recording): preserve mic companion audio when source has audio
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type ExecFileCallback = (error: Error | null, stdout?: string, stderr?: string) => void;
|
||||
|
||||
describe("getCompanionAudioFallbackPaths", () => {
|
||||
let tempRoot: string;
|
||||
let appDataPath: string;
|
||||
let userDataPath: string;
|
||||
let tempPath: string;
|
||||
let appPath: string;
|
||||
let execFileMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "recordly-companion-audio-"));
|
||||
appDataPath = path.join(tempRoot, "AppData");
|
||||
userDataPath = path.join(tempRoot, "UserData");
|
||||
tempPath = path.join(tempRoot, "Temp");
|
||||
appPath = path.join(tempRoot, "App");
|
||||
await Promise.all(
|
||||
[appDataPath, userDataPath, tempPath, appPath].map((dirPath) =>
|
||||
fs.mkdir(dirPath, { recursive: true }),
|
||||
),
|
||||
);
|
||||
execFileMock = vi.fn(
|
||||
(
|
||||
_file: string,
|
||||
_args: string[],
|
||||
_options: Record<string, unknown>,
|
||||
callback: ExecFileCallback,
|
||||
) => {
|
||||
callback(null, "", "");
|
||||
},
|
||||
);
|
||||
|
||||
vi.resetModules();
|
||||
vi.doMock("electron", () => ({
|
||||
app: {
|
||||
isPackaged: false,
|
||||
getAppPath: () => appPath,
|
||||
getPath: (name: string) => {
|
||||
if (name === "appData") return appDataPath;
|
||||
if (name === "userData") return userDataPath;
|
||||
if (name === "temp") return tempPath;
|
||||
return tempRoot;
|
||||
},
|
||||
setPath: () => undefined,
|
||||
},
|
||||
}));
|
||||
vi.doMock("node:child_process", () => ({
|
||||
execFile: execFileMock,
|
||||
}));
|
||||
vi.doMock("../ffmpeg/binary", () => ({
|
||||
getFfmpegBinaryPath: () => "ffmpeg",
|
||||
}));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.doUnmock("electron");
|
||||
vi.doUnmock("node:child_process");
|
||||
vi.doUnmock("../ffmpeg/binary");
|
||||
if (tempRoot) {
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns companion audio files directly when the video has no embedded audio", async () => {
|
||||
const videoPath = path.join(tempRoot, "recording.mp4");
|
||||
const systemPath = path.join(tempRoot, "recording.system.wav");
|
||||
const micPath = path.join(tempRoot, "recording.mic.wav");
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile(videoPath, "video"),
|
||||
fs.writeFile(systemPath, "system"),
|
||||
fs.writeFile(micPath, "mic"),
|
||||
]);
|
||||
|
||||
execFileMock.mockImplementation(
|
||||
(
|
||||
_file: string,
|
||||
_args: string[],
|
||||
_options: Record<string, unknown>,
|
||||
callback: ExecFileCallback,
|
||||
) => {
|
||||
const error = new Error("ffmpeg probe failed") as Error & { stderr?: string };
|
||||
error.stderr = "Stream #0:0: Video: h264";
|
||||
callback(error, "", error.stderr);
|
||||
},
|
||||
);
|
||||
|
||||
const { getCompanionAudioFallbackPaths } = await import("./diagnostics");
|
||||
|
||||
await expect(getCompanionAudioFallbackPaths(videoPath)).resolves.toEqual([
|
||||
systemPath,
|
||||
micPath,
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the embedded source audio and adds the mic companion when both are present", async () => {
|
||||
const videoPath = path.join(tempRoot, "recording.mp4");
|
||||
const systemPath = path.join(tempRoot, "recording.system.wav");
|
||||
const micPath = path.join(tempRoot, "recording.mic.wav");
|
||||
|
||||
await Promise.all([
|
||||
fs.writeFile(videoPath, "video"),
|
||||
fs.writeFile(systemPath, "system"),
|
||||
fs.writeFile(micPath, "mic"),
|
||||
]);
|
||||
|
||||
execFileMock.mockImplementation(
|
||||
(
|
||||
_file: string,
|
||||
_args: string[],
|
||||
_options: Record<string, unknown>,
|
||||
callback: ExecFileCallback,
|
||||
) => {
|
||||
const error = new Error("ffmpeg probe found embedded audio") as Error & {
|
||||
stderr?: string;
|
||||
};
|
||||
error.stderr = "Stream #0:1: Audio: aac";
|
||||
callback(error, "", error.stderr);
|
||||
},
|
||||
);
|
||||
|
||||
const { getCompanionAudioFallbackPaths } = await import("./diagnostics");
|
||||
|
||||
await expect(getCompanionAudioFallbackPaths(videoPath)).resolves.toEqual([
|
||||
videoPath,
|
||||
micPath,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import fs from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
import { getFfmpegBinaryPath } from "../ffmpeg/binary";
|
||||
import { COMPANION_AUDIO_LAYOUTS } from "../constants";
|
||||
import type { NativeCaptureDiagnostics, CompanionAudioCandidate } from "../types";
|
||||
import { getFfmpegBinaryPath } from "../ffmpeg/binary";
|
||||
import { lastNativeCaptureDiagnostics, setLastNativeCaptureDiagnostics } from "../state";
|
||||
import type { CompanionAudioCandidate, NativeCaptureDiagnostics } from "../types";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -123,10 +123,23 @@ export async function getCompanionAudioFallbackPaths(videoPath: string) {
|
||||
}
|
||||
|
||||
if (await hasEmbeddedAudioStream(videoPath)) {
|
||||
return [];
|
||||
const microphoneCompanionPaths = Array.from(
|
||||
new Set(
|
||||
companionCandidates.flatMap((candidate) =>
|
||||
candidate.usablePaths.filter(
|
||||
(companionPath) => companionPath === candidate.micPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (microphoneCompanionPaths.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [videoPath, ...microphoneCompanionPaths];
|
||||
}
|
||||
|
||||
return companionCandidates.flatMap((candidate) => candidate.usablePaths);
|
||||
return Array.from(new Set(companionCandidates.flatMap((candidate) => candidate.usablePaths)));
|
||||
}
|
||||
|
||||
export async function validateRecordedVideo(videoPath: string) {
|
||||
|
||||
Reference in New Issue
Block a user