mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
Merge pull request #645 from webadderallorg/fix/native-mac-duplicate-mic-audio
fix: avoid duplicate mic audio in native mac recordings
This commit is contained in:
@@ -134,6 +134,32 @@ describe("getCompanionAudioFallbackPaths", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
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");
|
||||
|
||||
await Promise.all([fs.writeFile(videoPath, "video"), 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([micPath]);
|
||||
});
|
||||
|
||||
it("loads saved sidecar timing metadata alongside companion audio paths", async () => {
|
||||
const videoPath = path.join(tempRoot, "recording.mp4");
|
||||
const micPath = path.join(tempRoot, "recording.mic.webm");
|
||||
|
||||
@@ -508,20 +508,41 @@ export async function getCompanionAudioFallbackInfo(videoPath: string) {
|
||||
|
||||
let paths: string[];
|
||||
if (await hasEmbeddedAudioStream(videoPath)) {
|
||||
const companionPaths = Array.from(
|
||||
const hasUsableMacSystemCompanion = companionCandidates.some(
|
||||
(candidate) =>
|
||||
candidate.platform === "mac" &&
|
||||
candidate.usablePaths.includes(candidate.systemPath),
|
||||
);
|
||||
const usableMacMicOnlyCompanions = Array.from(
|
||||
new Set(
|
||||
companionCandidates.flatMap((candidate) =>
|
||||
candidate.usablePaths.filter(
|
||||
(companionPath) => companionPath === candidate.micPath,
|
||||
),
|
||||
candidate.platform === "mac" &&
|
||||
!candidate.usablePaths.includes(candidate.systemPath) &&
|
||||
candidate.usablePaths.includes(candidate.micPath)
|
||||
? [candidate.micPath]
|
||||
: [],
|
||||
),
|
||||
),
|
||||
);
|
||||
if (companionPaths.length === 0) {
|
||||
return { paths: [], startDelayMsByPath: {} };
|
||||
}
|
||||
|
||||
paths = [videoPath, ...companionPaths];
|
||||
if (!hasUsableMacSystemCompanion && usableMacMicOnlyCompanions.length > 0) {
|
||||
paths = usableMacMicOnlyCompanions;
|
||||
} else {
|
||||
const companionPaths = Array.from(
|
||||
new Set(
|
||||
companionCandidates.flatMap((candidate) =>
|
||||
candidate.usablePaths.filter(
|
||||
(companionPath) => companionPath === candidate.micPath,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (companionPaths.length === 0) {
|
||||
return { paths: [], startDelayMsByPath: {} };
|
||||
}
|
||||
|
||||
paths = [videoPath, ...companionPaths];
|
||||
}
|
||||
} else {
|
||||
paths = Array.from(
|
||||
new Set(companionCandidates.flatMap((candidate) => candidate.usablePaths)),
|
||||
|
||||
@@ -77,9 +77,6 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
|
||||
}
|
||||
writesSystemAudioToSeparateTrack = capturesSystemAudio
|
||||
writesMicrophoneToSeparateTrack = capturesSystemAudio && capturesMicrophone
|
||||
if capturesMicrophone && !capturesSystemAudio {
|
||||
writesMicrophoneToSeparateTrack = true
|
||||
}
|
||||
let requestedFPS = max(targetCaptureFPS, config.fps ?? targetCaptureFPS)
|
||||
streamConfig.minimumFrameInterval = CMTime(value: 1, timescale: CMTimeScale(requestedFPS))
|
||||
streamConfig.queueDepth = 6
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -161,6 +161,28 @@ describe("AudioProcessor offline render preparation", () => {
|
||||
expect(renderAndMuxOfflineAudio).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("avoids the single-sidecar fast path for legacy mac mic sidecars that still need embedded audio", async () => {
|
||||
const processor = new AudioProcessor() as unknown as OfflineRenderTestHarness;
|
||||
const loadAudioFileDemuxer = vi.spyOn(processor, "loadAudioFileDemuxer");
|
||||
const renderAndMuxOfflineAudio = vi
|
||||
.spyOn(processor, "renderAndMuxOfflineAudio")
|
||||
.mockResolvedValue();
|
||||
|
||||
await processor.process(
|
||||
{} as never,
|
||||
{} as never,
|
||||
"file:///tmp/recording.mp4",
|
||||
[],
|
||||
[],
|
||||
undefined,
|
||||
[],
|
||||
["/tmp/recording.mic.m4a"],
|
||||
);
|
||||
|
||||
expect(loadAudioFileDemuxer).not.toHaveBeenCalled();
|
||||
expect(renderAndMuxOfflineAudio).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("soft-limits mixed peaks before encoding or WAV conversion", () => {
|
||||
const samples = new Float32Array([
|
||||
-1.6,
|
||||
|
||||
@@ -263,12 +263,18 @@ export class AudioProcessor {
|
||||
videoUrl,
|
||||
sortedSourceAudioFallbackPaths,
|
||||
);
|
||||
const requiresLegacyMacMicSidecarMix =
|
||||
routingPolicy.includeEmbeddedInExport &&
|
||||
!routingPolicy.hasEmbeddedSourceAudio &&
|
||||
routingPolicy.playbackPaths.length === 1 &&
|
||||
routingPolicy.playbackPaths[0]?.toLowerCase().endsWith(".mic.m4a") === true;
|
||||
const hasTimedCompanionAudio = routingPolicy.playbackPaths.some(
|
||||
(audioPath) => (sourceAudioFallbackStartDelayMsByPath?.[audioPath] ?? 0) > 0,
|
||||
);
|
||||
const needsSourceAudioMixing =
|
||||
routingPolicy.playbackPaths.length > 1 ||
|
||||
(routingPolicy.hasEmbeddedSourceAudio && routingPolicy.playbackPaths.length > 0) ||
|
||||
requiresLegacyMacMicSidecarMix ||
|
||||
hasTimedCompanionAudio;
|
||||
|
||||
// When speed edits, audio regions, or multiple audio sources need mixing, use offline AudioContext pipeline.
|
||||
|
||||
Reference in New Issue
Block a user