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
18 changes: 17 additions & 1 deletion src/lib/exporter/modernVideoExporter.nativeStaticLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,21 @@ describe("ModernVideoExporter native static-layout eligibility", () => {
});
});

it("mixes companion sidecar audio when the source MP4 also has an audio track", () => {
const videoPath = "C:\\recordly\\recording.mp4";
const micPath = "C:\\recordly\\recording.mic.wav";
const exporter = createExporter({
videoUrl: `file:///${videoPath.replace(/\\/g, "/")}`,
sourceAudioFallbackPaths: [micPath],
});

expect(exporter.buildNativeAudioPlan(videoInfo)).toMatchObject({
audioMode: "edited-track",
strategy: "offline-render-fallback",
sourceAudioFallbackPaths: [expect.stringMatching(/recording\.mp4$/), micPath],
});
});

it("keeps timed companion audio on the offline render path", () => {
const audioPath = "C:\\recordly\\recording.system.wav";
const speedRegions: SpeedRegion[] = [
Expand All @@ -167,9 +182,10 @@ describe("ModernVideoExporter native static-layout eligibility", () => {
audioCodec: undefined,
audioSampleRate: undefined,
}),
).toEqual({
).toMatchObject({
audioMode: "edited-track",
strategy: "offline-render-fallback",
sourceAudioFallbackPaths: [audioPath],
});
});

Expand Down
34 changes: 30 additions & 4 deletions src/lib/exporter/modernVideoExporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
import { VideoMuxer } from "./muxer";
import { roundNativeStaticLayoutContentSize } from "./nativeStaticLayoutGeometry";
import { buildNativeStaticLayoutCursorTelemetry } from "./nativeStaticLayoutTelemetry";
import { resolveSourceAudioFallbackPaths } from "./sourceAudioFallback";
import { type DecodedVideoInfo, StreamingVideoDecoder } from "./streamingDecoder";
import type {
ExportConfig,
Expand Down Expand Up @@ -164,6 +165,7 @@ type NativeAudioPlan =
| {
audioMode: "edited-track";
strategy: "offline-render-fallback";
sourceAudioFallbackPaths?: string[];
}
| {
audioMode: "edited-track";
Expand Down Expand Up @@ -1224,6 +1226,26 @@ export class ModernVideoExporter {
return buildNativeStaticLayoutTimelineSegments(sourceSegments);
}

private getNativeAudioFallbackPaths(videoInfo: DecodedVideoInfo): string[] {
const sourceAudioFallbackPaths = (this.config.sourceAudioFallbackPaths ?? []).filter(
(audioPath) => typeof audioPath === "string" && audioPath.trim().length > 0,
);
const localVideoSourcePath = this.getNativeVideoSourcePath();
if (!videoInfo.hasAudio || !localVideoSourcePath) {
return sourceAudioFallbackPaths;
}

const { externalAudioPaths } = resolveSourceAudioFallbackPaths(
localVideoSourcePath,
sourceAudioFallbackPaths,
);
if (externalAudioPaths.length === 0) {
return sourceAudioFallbackPaths;
}

return [localVideoSourcePath, ...externalAudioPaths];
}

private shouldUseNativeStaticLayoutTimelineMap(
videoInfo: DecodedVideoInfo,
effectiveDurationSec: number,
Expand All @@ -1243,9 +1265,7 @@ export class ModernVideoExporter {
private buildNativeAudioPlan(videoInfo: DecodedVideoInfo): NativeAudioPlan {
const speedRegions = this.config.speedRegions ?? [];
const audioRegions = this.config.audioRegions ?? [];
const sourceAudioFallbackPaths = (this.config.sourceAudioFallbackPaths ?? []).filter(
(audioPath) => typeof audioPath === "string" && audioPath.trim().length > 0,
);
const sourceAudioFallbackPaths = this.getNativeAudioFallbackPaths(videoInfo);
const hasTimedSourceAudioFallback = sourceAudioFallbackPaths.some(
(audioPath) =>
(this.config.sourceAudioFallbackStartDelayMsByPath?.[audioPath] ?? 0) > 0,
Expand Down Expand Up @@ -1338,13 +1358,15 @@ export class ModernVideoExporter {
return {
audioMode: "edited-track",
strategy: "offline-render-fallback",
sourceAudioFallbackPaths,
};
}

if (!primaryAudioSourcePath) {
return {
audioMode: "edited-track",
strategy: "offline-render-fallback",
sourceAudioFallbackPaths,
};
}

Expand Down Expand Up @@ -1892,6 +1914,7 @@ export class ModernVideoExporter {
private async renderEditedAudioForNativeMux(
description: string,
onProgress: (progress: number) => void,
sourceAudioFallbackPaths = this.config.sourceAudioFallbackPaths,
) {
this.audioProcessor = new AudioProcessor();
this.audioProcessor.setOnProgress(onProgress);
Expand All @@ -1902,7 +1925,7 @@ export class ModernVideoExporter {
this.config.trimRegions,
this.config.speedRegions,
this.config.audioRegions,
this.config.sourceAudioFallbackPaths,
sourceAudioFallbackPaths,
this.config.sourceAudioFallbackStartDelayMsByPath,
this.config.sourceAudioTrackSettings,
this.config.clipRegions,
Expand Down Expand Up @@ -1950,6 +1973,7 @@ export class ModernVideoExporter {
"Native static-layout edited audio rendering",
(progress) =>
this.reportProgress(0, totalFrames, "preparing", undefined, progress),
audioPlan.sourceAudioFallbackPaths,
);

return {
Expand Down Expand Up @@ -2738,6 +2762,7 @@ export class ModernVideoExporter {
const renderedAudio = await this.renderEditedAudioForNativeMux(
`${NATIVE_EXPORT_ENGINE_NAME} edited audio rendering`,
(progress) => this.reportFinalizingProgress(this.processedFrameCount, 99, progress),
audioPlan.sourceAudioFallbackPaths,
);
editedAudioBuffer = renderedAudio.editedAudioData;
editedAudioMimeType = renderedAudio.editedAudioMimeType;
Expand Down Expand Up @@ -2835,6 +2860,7 @@ export class ModernVideoExporter {
const renderedAudio = await this.renderEditedAudioForNativeMux(
"FFmpeg edited audio rendering",
(progress) => this.reportFinalizingProgress(this.processedFrameCount, 99, progress),
audioPlan.sourceAudioFallbackPaths,
);
editedAudioBuffer = renderedAudio.editedAudioData;
editedAudioMimeType = renderedAudio.editedAudioMimeType;
Expand Down