From 1c2df9325ead5abed4b2d4a326a2c0d5373e5c47 Mon Sep 17 00:00:00 2001 From: Abhijeet Dash Date: Wed, 9 Sep 2026 17:12:13 +0530 Subject: [PATCH 1/2] fix: keep mic audio when system audio is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getCompanionAudioFallbackInfo returned only the video path when a macOS system sidecar existed, assuming the inline mp4 track was a complete mix. The capture helper writes system audio alone to that track, so the microphone was dropped from preview and export whenever both sources were enabled — the recorded narration was silent even though it had been captured correctly to recording-.mic.m4a. Return both macOS sidecars instead, so the renderer routes them as separate system and mic tracks and mutes the silent embedded track. Existing recordings recover their audio on reopen. Fixes webadderallorg/Recordly#912 --- electron/ipc/recording/diagnostics.test.ts | 36 ++++++++++++++++++++++ electron/ipc/recording/diagnostics.ts | 12 +++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/electron/ipc/recording/diagnostics.test.ts b/electron/ipc/recording/diagnostics.test.ts index 9f7ddeb08..a0092a602 100644 --- a/electron/ipc/recording/diagnostics.test.ts +++ b/electron/ipc/recording/diagnostics.test.ts @@ -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, + 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"); diff --git a/electron/ipc/recording/diagnostics.ts b/electron/ipc/recording/diagnostics.ts index 985209f22..7cc8d06eb 100644 --- a/electron/ipc/recording/diagnostics.ts +++ b/electron/ipc/recording/diagnostics.ts @@ -524,7 +524,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( From 9b72ff0ee8820e7b222bae171623989f18871731 Mon Sep 17 00:00:00 2001 From: Abhijeet Dash Date: Thu, 10 Sep 2026 10:48:20 +0530 Subject: [PATCH 2/2] docs: document getCompanionAudioFallbackInfo resolution rules --- electron/ipc/recording/diagnostics.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/electron/ipc/recording/diagnostics.ts b/electron/ipc/recording/diagnostics.ts index 7cc8d06eb..76a9a343d 100644 --- a/electron/ipc/recording/diagnostics.ts +++ b/electron/ipc/recording/diagnostics.ts @@ -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) {