import { ipcMain } from "electron"; import ffmpegPath from "ffmpeg-static"; import ffprobePath from "@andrkrn/ffprobe-static"; import Ffmpeg from "fluent-ffmpeg"; import log from "electron-log/main"; import path from "path"; import fs from "fs-extra"; const logger = log.scope("ffmpeg"); export default class FfmpegWrapper { public ffmpeg: Ffmpeg.FfmpegCommand; constructor() { const ff = Ffmpeg(); logger.debug("Using ffmpeg path:", ffmpegPath); logger.debug("Using ffprobe path:", ffprobePath); ff.setFfmpegPath(ffmpegPath); ff.setFfprobePath(ffprobePath); this.ffmpeg = ff; } checkCommand(): Promise { const sampleFile = path.join(__dirname, "samples", "jfk.wav"); return new Promise((resolve, _reject) => { this.ffmpeg.input(sampleFile).getAvailableFormats((err, _formats) => { if (err) { logger.error("Command not valid:", err); resolve(false); } else { logger.info("Command valid, available formats"); resolve(true); } }); }); } generateMetadata(input: string): Promise { return new Promise((resolve, reject) => { this.ffmpeg .input(input) .on("start", (commandLine) => { logger.info("Spawned FFmpeg with command: " + commandLine); }) .on("error", (err) => { logger.error(err); reject(err); }) .ffprobe((err, metadata) => { if (err) { logger.error(err); reject(err); } resolve(metadata); }); }); } generateCover(input: string, output: string): Promise { return new Promise((resolve, reject) => { this.ffmpeg .input(input) .thumbnail({ count: 1, filename: path.basename(output), folder: path.dirname(output), }) .on("start", (commandLine) => { logger.info("Spawned FFmpeg with command: " + commandLine); fs.ensureDirSync(path.dirname(output)); }) .on("end", () => { logger.info(`File ${output} created`); resolve(output); }) .on("error", (err) => { logger.error(err); reject(err); }); }); } ensureSampleRate( input: string, output: string, sampleRate = 16000 ): Promise { logger.info(`Trying to convert ${input} to 16-bit file ${output}`); if (fs.pathExistsSync(output)) { logger.warn(`File ${output} already exists, deleting.`); fs.removeSync(output); } return new Promise((resolve, reject) => { this.ffmpeg .input(input) .outputOptions("-ar", `${sampleRate}`) .on("error", (err) => { logger.error(err); reject(err); }) .on("end", () => { logger.info(`File ${output} created`); resolve(output); }) .save(output); }); } convertToWav( input: string, output: string, options: string[] = [] ): Promise { return new Promise((resolve, reject) => { this.ffmpeg .input(input) .outputOptions( "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", ...options ) .on("start", (commandLine) => { logger.debug(`Trying to convert ${input} to ${output}`); logger.info("Spawned FFmpeg with command: " + commandLine); fs.ensureDirSync(path.dirname(output)); }) .on("end", (stdout, stderr) => { if (stdout) { logger.debug(stdout); } if (stderr) { logger.error(stderr); } if (fs.existsSync(output)) { resolve(output); } else { reject(new Error("FFmpeg command failed")); } }) .on("error", (err: Error) => { logger.error(err); reject(err); }) .save(output); }); } async prepareForWhisper(input: string, output: string): Promise { const metadata = await this.generateMetadata(input); if (metadata.format.format_name === "wav") { if (metadata.streams[0].sample_rate === 16000) { logger.info(`File ${input} already in 16-bit WAVE format`); return input; } else { return this.ensureSampleRate( input, input.replace(path.extname(input), "_16bit.wav") ); } } logger.info(`Trying to convert ${input} to 16-bit WAVE file ${output}`); if (fs.pathExistsSync(output)) { logger.warn(`File ${output} already exists, deleting.`); fs.removeSync(output); } return this.convertToWav(input, output); } registerIpcHandlers() { ipcMain.handle("ffmpeg-check-command", async (_event) => { return await this.checkCommand(); }); } }