Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions electron/ipc/recording/diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,42 @@ describe("getCompanionAudioFallbackPaths", () => {
]);
});

it("returns both mac sidecars instead of the video when a system sidecar exists", async () => {
const videoPath = path.join(tempRoot, "recording.mp4");
const systemPath = path.join(tempRoot, "recording.system.m4a");
const micPath = path.join(tempRoot, "recording.mic.m4a");

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");

// The inline mp4 track holds system audio only, so returning [videoPath]
// here silently dropped the microphone.
await expect(getCompanionAudioFallbackPaths(videoPath)).resolves.toEqual([
systemPath,
micPath,
]);
});

it("prefers the mac mic companion alone when embedded audio already exists and no system sidecar is present", async () => {
const videoPath = path.join(tempRoot, "recording.mp4");
const micPath = path.join(tempRoot, "recording.mic.m4a");
Expand Down
22 changes: 21 additions & 1 deletion electron/ipc/recording/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,16 @@ export async function getCompanionAudioFallbackPaths(videoPath: string) {
return paths;
}

/**
* Resolve which audio files the editor should play alongside `videoPath`, and
* the start delay recorded for each.
*
* The renderer treats a `.system.`/`.mic.` pair as independent tracks and mutes
* the video's own track when both are present. The macOS helper writes system
* audio to the inline track but keeps both sources as sidecars, so once a mac
* system sidecar exists the sidecars are authoritative and are returned in place
* of the video. Other layouts keep the embedded track and add the mic sidecar.
*/
export async function getCompanionAudioFallbackInfo(videoPath: string) {
const companionCandidates = await getUsableCompanionAudioCandidates(videoPath);
if (companionCandidates.length === 0) {
Expand Down Expand Up @@ -524,7 +534,17 @@ export async function getCompanionAudioFallbackInfo(videoPath: string) {
if (!hasUsableMacSystemCompanion && usableMacMicOnlyCompanions.length > 0) {
paths = usableMacMicOnlyCompanions;
} else if (hasUsableMacSystemCompanion) {
paths = [videoPath];
// The inline mp4 audio track carries system audio only (the helper skips
// the microphone while system audio is captured), so returning the video
// alone drops the mic entirely. Hand over both mac sidecars instead and
// let the renderer route them as independent system/mic tracks.
paths = Array.from(
new Set(
companionCandidates.flatMap((candidate) =>
candidate.platform === "mac" ? candidate.usablePaths : [],
),
),
);
} else {
const companionPaths = Array.from(
new Set(
Expand Down