Created
August 20, 2025 18:37
-
-
Save malerba118/c8a94d1b5e31f191ab1ee31e1de29c94 to your computer and use it in GitHub Desktop.
ffmpeg.wasm converter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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