import path from "node:path"; import { rename, rm } from "node:fs/promises"; import { execa } from "execa"; import type { Paths } from "../lib/paths"; import type { AudioFormat } from "../lib/channelConfig"; import { tierMediaFile } from "../lib/mediaTier-server"; const CODEC_ARGS: Record = { m4a: ["-c:a", "aac"], mp3: ["-c:a", "libmp3lame"], opus: ["-c:a", "libopus"], }; export type TranscodeAudioOptions = { paths: Paths; videoDir: string; sourceFilename: string; targetFormat: AudioFormat; onLog: (s: string) => void; signal: AbortSignal; }; export async function transcodeAudio(opts: TranscodeAudioOptions): Promise { const src = path.join(opts.videoDir, opts.sourceFilename); const out = path.join(opts.videoDir, `audio.${opts.targetFormat}`); if (src === out) { throw new Error("Source and target are the same file"); } // Keep the target extension last so ffmpeg can infer the output format. const tmp = path.join( opts.videoDir, `audio.tmp-${process.pid}.${opts.targetFormat}`, ); const args = [ "-y", "-i", src, "-vn", ...CODEC_ARGS[opts.targetFormat], tmp, ]; opts.onLog(`$ ${opts.paths.ffmpegBin} ${args.join(" ")}\n`); const child = execa(opts.paths.ffmpegBin, args, { cancelSignal: opts.signal, all: true, buffer: false, reject: false, }); child.all?.on("data", (c: Buffer) => opts.onLog(c.toString("utf8"))); const result = await child; if (result.exitCode !== 0) { await rm(tmp, { force: true }); if (opts.signal.aborted) return; throw new Error(`ffmpeg exited with code ${result.exitCode}`); } await rename(tmp, out); opts.onLog(`Wrote ${out}\n`); // THE MEDIA TIER'S HOOK (release 17). The rename above put a real file over // the name — over a tiered LINK, when the channel had one — so the fresh // file moves into channels//media (over the stale copy there) and the // link is made again. Every finalising transcode is this function: the app // extraction, the audio-checked download, the video page's Transcode. Never // throws; on a classic channel it leaves the file where it is. await tierMediaFile(opts.videoDir, path.basename(out), { onLog: opts.onLog }); }