Skip to content

Instantly share code, notes, and snippets.

@malerba118
Created August 20, 2025 18:37
Show Gist options
  • Select an option

  • Save malerba118/c8a94d1b5e31f191ab1ee31e1de29c94 to your computer and use it in GitHub Desktop.

Select an option

Save malerba118/c8a94d1b5e31f191ab1ee31e1de29c94 to your computer and use it in GitHub Desktop.
ffmpeg.wasm converter
import { fetchFile } from "@ffmpeg/util";
import { action, makeObservable, observable, runInAction } from "mobx";
import { uuidv4 } from "uuidv7";
import { createFfmpeg } from "./ffmpeg";
export enum ConversionPreset {
MP3 = "mp3",
MP3_44100_128 = "mp3_44100_128",
MP3_44100_192 = "mp3_44100_192",
MP3_44100_256 = "mp3_44100_256",
WAV = "wav",
WAV_PCM_44100 = "wav_pcm_44100",
WAV_PCM_48000 = "wav_pcm_48000",
M4A = "m4a",
FLAC = "flac",
}
const PRESET_EXTENSIONS: Record<ConversionPreset, string> = {
[ConversionPreset.MP3]: "mp3",
[ConversionPreset.MP3_44100_128]: "mp3",
[ConversionPreset.MP3_44100_192]: "mp3",
[ConversionPreset.MP3_44100_256]: "mp3",
[ConversionPreset.WAV]: "wav",
[ConversionPreset.WAV_PCM_44100]: "wav",
[ConversionPreset.WAV_PCM_48000]: "wav",
[ConversionPreset.M4A]: "m4a",
[ConversionPreset.FLAC]: "flac",
};
const PRESET_MIME_TYPES: Record<ConversionPreset, string> = {
[ConversionPreset.MP3]: "audio/mpeg",
[ConversionPreset.MP3_44100_128]: "audio/mpeg",
[ConversionPreset.MP3_44100_192]: "audio/mpeg",
[ConversionPreset.MP3_44100_256]: "audio/mpeg",
[ConversionPreset.WAV]: "audio/wav",
[ConversionPreset.WAV_PCM_44100]: "audio/wav",
[ConversionPreset.WAV_PCM_48000]: "audio/wav",
[ConversionPreset.M4A]: "audio/m4a",
[ConversionPreset.FLAC]: "audio/flac",
};
export type ConverterStatus = "unused" | "converting" | "success" | "error";
export class Converter {
status: ConverterStatus = "unused";
progress = 0;
runs = 0;
result: Blob | undefined;
error: unknown;
constructor() {
makeObservable(this, {
runs: observable.ref,
status: observable.ref,
progress: observable.ref,
result: observable.ref,
error: observable.ref,
});
}
get isUnused() {
return this.status === "unused";
}
get isConverting() {
return this.status === "converting";
}
get isSuccess() {
return this.status === "success";
}
get isError() {
return this.status === "error";
}
async convert(
source: string | Blob | File,
preset: ConversionPreset
): Promise<Blob> {
const iteration = this.runs + 1;
runInAction(() => {
this.runs = iteration;
this.progress = 0;
this.status = "converting";
this.result = undefined;
this.error = undefined;
});
try {
const blob = await this.convertUsingFfmpeg(source, preset, {
onProgress: action(progress => {
this.progress = progress;
}),
});
if (iteration !== this.runs) {
throw new Error("Newer conversion has started");
}
runInAction(() => {
this.status = "success";
this.result = blob;
});
return blob;
} catch (error) {
runInAction(() => {
this.status = "error";
this.error = error;
});
throw error;
}
}
private async convertUsingFfmpeg(
source: string | Blob | File,
preset: ConversionPreset,
{ onProgress }: { onProgress?: (progress: number) => void }
): Promise<Blob> {
const ffmpeg = await createFfmpeg();
const tempId = uuidv4();
const inputName = `input-${tempId}`;
const outputName = `output-${tempId}.${PRESET_EXTENSIONS[preset]}`;
try {
const inputData = await fetchFile(source);
await ffmpeg.writeFile(inputName, inputData);
ffmpeg.on("progress", ({ progress }) => onProgress?.(progress));
// Build ffmpeg args per preset
const baseArgs = ["-stats_period", "0.1", "-i", inputName];
let args: string[] = [];
switch (preset) {
case ConversionPreset.MP3:
args = [
...baseArgs,
"-vn",
"-c:a",
"mp3",
"-b:a",
"192k",
outputName,
];
break;
case ConversionPreset.MP3_44100_128:
args = [
...baseArgs,
"-vn",
"-c:a",
"mp3",
"-ar",
"44100",
"-b:a",
"128k",
outputName,
];
break;
case ConversionPreset.MP3_44100_192:
args = [
...baseArgs,
"-vn",
"-c:a",
"mp3",
"-ar",
"44100",
"-b:a",
"192k",
outputName,
];
break;
case ConversionPreset.MP3_44100_256:
args = [
...baseArgs,
"-vn",
"-c:a",
"mp3",
"-ar",
"44100",
"-b:a",
"256k",
outputName,
];
break;
case ConversionPreset.WAV:
args = [...baseArgs, "-vn", "-c:a", "pcm_s16le", outputName];
break;
case ConversionPreset.WAV_PCM_44100:
args = [
...baseArgs,
"-vn",
"-c:a",
"pcm_s16le",
"-ar",
"44100",
outputName,
];
break;
case ConversionPreset.WAV_PCM_48000:
args = [
...baseArgs,
"-vn",
"-c:a",
"pcm_s16le",
"-ar",
"48000",
outputName,
];
break;
case ConversionPreset.M4A:
args = [
...baseArgs,
"-vn",
"-c:a",
"aac",
"-b:a",
"192k",
"-movflags",
"+faststart",
outputName,
];
break;
case ConversionPreset.FLAC:
args = [
...baseArgs,
"-vn",
"-c:a",
"flac",
"-compression_level",
"8",
outputName,
];
break;
default:
throw new Error(`Unsupported preset for ffmpeg fallback: ${preset}`);
}
await ffmpeg.exec(args);
const data: any = await ffmpeg.readFile(outputName);
return new Blob([data.buffer], { type: PRESET_MIME_TYPES[preset] });
} finally {
await Promise.allSettled([
ffmpeg.deleteFile(inputName),
ffmpeg.deleteFile(outputName),
]);
ffmpeg.terminate();
}
}
}
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { toBlobURL } from "@ffmpeg/util";
let coreBlobUrl: Promise<string> | null = null;
let wasmBlobUrl: Promise<string> | null = null;
/** Create new single-thread ffmpeg.wasm instance. */
export async function createFfmpeg(): Promise<FFmpeg> {
const ffmpeg = new FFmpeg();
const base = "https://unpkg.com/@ffmpeg/core@0.12.10/dist/umd";
coreBlobUrl =
coreBlobUrl || toBlobURL(`${base}/ffmpeg-core.js`, "text/javascript");
wasmBlobUrl =
wasmBlobUrl || toBlobURL(`${base}/ffmpeg-core.wasm`, "application/wasm");
await ffmpeg.load({
coreURL: await coreBlobUrl,
wasmURL: await wasmBlobUrl,
});
return ffmpeg;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment