From 44a7c3b61c00e19c5940d6cc80a5f46c0678789f Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Tue, 3 Mar 2026 23:24:18 +0000 Subject: [PATCH] fix(downloads): cache ffmpeg core js and wasm This creates a blob url outside of the worker for for the core .js and .wasm files so they aren't downloaded on each run. --- bun.lock | 3 +++ js/api.js | 5 ++++- js/downloads.js | 5 ++++- js/ffmpeg.js | 44 +++++++++++++++++++++++++++++++++----------- js/ffmpeg.worker.js | 13 +++++-------- package-lock.json | 10 ++++++++++ package.json | 1 + 7 files changed, 60 insertions(+), 21 deletions(-) diff --git a/bun.lock b/bun.lock index c498d44..bbd5ff1 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "monochrome", "dependencies": { + "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", "@kawarp/core": "^1.1.1", @@ -328,6 +329,8 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + "@ffmpeg/core": ["@ffmpeg/core@0.12.10", "", {}, "sha512-dzNplnn2Nxle2c2i2rrDhqcB19q9cglCkWnoMTDN9Q9l3PvdjZWd1HfSPjCNWc/p8Q3CT+Es9fWOR0UhAeYQZA=="], + "@ffmpeg/ffmpeg": ["@ffmpeg/ffmpeg@0.12.15", "", { "dependencies": { "@ffmpeg/types": "^0.12.4" } }, "sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw=="], "@ffmpeg/types": ["@ffmpeg/types@0.12.4", "", {}, "sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A=="], diff --git a/js/api.js b/js/api.js index fef8435..1e6cd40 100644 --- a/js/api.js +++ b/js/api.js @@ -11,7 +11,7 @@ import { APICache } from './cache.js'; import { addMetadataToAudio } from './metadata.js'; import { DashDownloader } from './dash-downloader.js'; import { encodeToMp3, MP3EncodingError } from './mp3-encoder.js'; -import { ffmpeg } from './ffmpeg.js'; +import { ffmpeg, loadFfmpeg } from './ffmpeg.js'; import { initTagLib } from './taglib.js'; export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE'; @@ -1112,6 +1112,9 @@ export class LosslessAPI { async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) { // Initialize taglib in the background. initTagLib().catch(console.error); + + // Load ffmpeg in the background. + loadFfmpeg().catch(console.error); const { onProgress, track } = options; try { diff --git a/js/downloads.js b/js/downloads.js index a67a59b..9a9fffb 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -16,7 +16,7 @@ import { addMetadataToAudio } from './metadata.js'; import { DashDownloader } from './dash-downloader.js'; import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js'; import { encodeToMp3 } from './mp3-encoder.js'; -import { ffmpeg } from './ffmpeg.js'; +import { ffmpeg, loadFfmpeg } from './ffmpeg.js'; import { initTagLib } from './taglib.js'; const downloadTasks = new Map(); @@ -273,6 +273,9 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign // Initialize taglib in the background. initTagLib().catch(console.error); + // Load ffmpeg in the background. + loadFfmpeg().catch(console.error); + let enrichedTrack = { ...track, artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null), diff --git a/js/ffmpeg.js b/js/ffmpeg.js index 2a280bd..9485ed2 100644 --- a/js/ffmpeg.js +++ b/js/ffmpeg.js @@ -1,3 +1,8 @@ +import { toBlobURL } from '@ffmpeg/util'; +const ffmpegBase = 'https://unpkg.com/@ffmpeg/core/dist/esm'; +const coreJs = `${ffmpegBase}/ffmpeg-core.js`; +const coreWasm = `${ffmpegBase}/ffmpeg-core.wasm`; + class FfmpegError extends Error { constructor(message) { super(message); @@ -6,6 +11,20 @@ class FfmpegError extends Error { } } +export function loadFfmpeg() { + return ( + loadFfmpeg.promise || + (loadFfmpeg.promise = (async () => { + const data = { + coreURL: await toBlobURL(coreJs, 'text/javascript'), + wasmURL: await toBlobURL(coreWasm, 'application/wasm'), + }; + + return data; + })()) + ); +} + async function ffmpegWorker( audioBlob, args = {}, @@ -15,6 +34,7 @@ async function ffmpegWorker( signal = null ) { const audioData = await audioBlob.arrayBuffer(); + const assets = loadFfmpeg(); return new Promise((resolve, reject) => { const worker = new Worker(new URL('./ffmpeg.worker.js', import.meta.url), { type: 'module' }); @@ -57,18 +77,20 @@ async function ffmpegWorker( reject(new FfmpegError('Worker failed: ' + error.message)); }; - // Transfer audio data to worker - worker.postMessage( - { - audioData, - ...args, - output: { - name: outputName, - mime: outputMime, + (async () => { + worker.postMessage( + { + audioData, + ...args, + output: { + name: outputName, + mime: outputMime, + }, + loadOptions: await assets, }, - }, - [audioData] - ); + [audioData] + ); + })(); }); } diff --git a/js/ffmpeg.worker.js b/js/ffmpeg.worker.js index 763388a..8b5ba90 100644 --- a/js/ffmpeg.worker.js +++ b/js/ffmpeg.worker.js @@ -1,10 +1,9 @@ import { FFmpeg } from '@ffmpeg/ffmpeg'; -import { toBlobURL } from '@ffmpeg/util'; let ffmpeg = null; let loadingPromise = null; -async function loadFFmpeg() { +async function loadFFmpeg(loadOptions = {}) { if (loadingPromise) return loadingPromise; loadingPromise = (async () => { @@ -25,11 +24,7 @@ async function loadFFmpeg() { self.postMessage({ type: 'progress', stage: 'loading', message: 'Loading FFmpeg...' }); - const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm'; - await ffmpeg.load({ - coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), - wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), - }); + await ffmpeg.load(loadOptions); })(); return loadingPromise; @@ -45,10 +40,12 @@ self.onmessage = async (e) => { }, encodeStartMessage = 'Encoding...', encodeEndMessage = 'Finalizing...', + loadOptions = {}, } = e.data; try { - await loadFFmpeg(); + console.log(loadOptions); + await loadFFmpeg(loadOptions); self.postMessage({ type: 'progress', stage: 'encoding', message: encodeStartMessage }); diff --git a/package-lock.json b/package-lock.json index b76bb22..f492dc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", "@kawarp/core": "^1.1.1", @@ -2578,6 +2579,15 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@ffmpeg/core": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@ffmpeg/core/-/core-0.12.10.tgz", + "integrity": "sha512-dzNplnn2Nxle2c2i2rrDhqcB19q9cglCkWnoMTDN9Q9l3PvdjZWd1HfSPjCNWc/p8Q3CT+Es9fWOR0UhAeYQZA==", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=16.x" + } + }, "node_modules/@ffmpeg/ffmpeg": { "version": "0.12.15", "resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz", diff --git a/package.json b/package.json index 8ad2398..af789e9 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "source-map": "^0.7.4" }, "dependencies": { + "@ffmpeg/core": "^0.12.10", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", "@kawarp/core": "^1.1.1",