( videoPath: string, audioPath: string, outputPath: string, signal?: AbortSignal, config?: MuxVideoWithAudioOptions, fps?: Fps, )
| 736 | } |
| 737 | |
| 738 | export async function muxVideoWithAudio( |
| 739 | videoPath: string, |
| 740 | audioPath: string, |
| 741 | outputPath: string, |
| 742 | signal?: AbortSignal, |
| 743 | config?: MuxVideoWithAudioOptions, |
| 744 | fps?: Fps, |
| 745 | ): Promise<MuxResult> { |
| 746 | const outputDir = dirname(outputPath); |
| 747 | if (!existsSync(outputDir)) mkdirSync(outputDir, { recursive: true }); |
| 748 | |
| 749 | const isWebm = outputPath.endsWith(".webm"); |
| 750 | const isMov = outputPath.endsWith(".mov"); |
| 751 | const shouldCopyAudio = isWebm ? false : await shouldCopyAacSidecar(audioPath, config); |
| 752 | const args = ["-i", videoPath, "-i", audioPath, "-c:v", "copy"]; |
| 753 | |
| 754 | if (isWebm) { |
| 755 | args.push("-c:a", "libopus", "-b:a", "128k"); |
| 756 | } else if (isMov) { |
| 757 | if (shouldCopyAudio) { |
| 758 | args.push("-c:a", "copy"); |
| 759 | } else { |
| 760 | args.push("-c:a", "aac", "-b:a", "192k"); |
| 761 | } |
| 762 | } else { |
| 763 | // processCompositionAudio (audioMixer.ts) performs the AAC encode and |
| 764 | // owns the single encoder-priming interval. Copying that sidecar into |
| 765 | // MP4 preserves the correct priming metadata; re-encoding it during mux |
| 766 | // creates another priming interval that ffmpeg writes as an empty leading |
| 767 | // video edit list, which QuickTime/Safari render as a black first frame. |
| 768 | if (shouldCopyAudio) { |
| 769 | args.push("-c:a", "copy", "-movflags", "+faststart"); |
| 770 | } else { |
| 771 | args.push("-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart"); |
| 772 | } |
| 773 | } |
| 774 | // PTS bases can diverge during mux and reintroduce negative DTS. See |
| 775 | // buildEncoderArgs for the full reasoning on why that breaks playback. |
| 776 | args.push("-avoid_negative_ts", "make_zero"); |
| 777 | if (fps !== undefined) { |
| 778 | // Set the exact output framerate so the muxer doesn't PTS-average a |
| 779 | // fractional rational like `360000/12001` instead of `30/1` into the |
| 780 | // output container metadata. `-c:v copy` is retained; no re-encode. |
| 781 | args.push("-r", fpsToFfmpegArg(fps)); |
| 782 | } |
| 783 | args.push("-y", outputPath); |
| 784 | |
| 785 | const processTimeout = config?.ffmpegProcessTimeout ?? DEFAULT_CONFIG.ffmpegProcessTimeout; |
| 786 | const result = await runFfmpeg(args, { signal, timeout: processTimeout }); |
| 787 | |
| 788 | if (signal?.aborted) { |
| 789 | return { |
| 790 | success: false, |
| 791 | outputPath, |
| 792 | durationMs: result.durationMs, |
| 793 | error: "FFmpeg mux cancelled", |
| 794 | }; |
| 795 | } |
no test coverage detected