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.
This commit is contained in:
parent
50a5b79d70
commit
44a7c3b61c
7 changed files with 60 additions and 21 deletions
3
bun.lock
3
bun.lock
|
|
@ -5,6 +5,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "monochrome",
|
"name": "monochrome",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ffmpeg/core": "^0.12.10",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
"@kawarp/core": "^1.1.1",
|
"@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=="],
|
"@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/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=="],
|
"@ffmpeg/types": ["@ffmpeg/types@0.12.4", "", {}, "sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A=="],
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import { APICache } from './cache.js';
|
||||||
import { addMetadataToAudio } from './metadata.js';
|
import { addMetadataToAudio } from './metadata.js';
|
||||||
import { DashDownloader } from './dash-downloader.js';
|
import { DashDownloader } from './dash-downloader.js';
|
||||||
import { encodeToMp3, MP3EncodingError } from './mp3-encoder.js';
|
import { encodeToMp3, MP3EncodingError } from './mp3-encoder.js';
|
||||||
import { ffmpeg } from './ffmpeg.js';
|
import { ffmpeg, loadFfmpeg } from './ffmpeg.js';
|
||||||
import { initTagLib } from './taglib.js';
|
import { initTagLib } from './taglib.js';
|
||||||
|
|
||||||
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
|
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 = {}) {
|
async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) {
|
||||||
// Initialize taglib in the background.
|
// Initialize taglib in the background.
|
||||||
initTagLib().catch(console.error);
|
initTagLib().catch(console.error);
|
||||||
|
|
||||||
|
// Load ffmpeg in the background.
|
||||||
|
loadFfmpeg().catch(console.error);
|
||||||
const { onProgress, track } = options;
|
const { onProgress, track } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { addMetadataToAudio } from './metadata.js';
|
||||||
import { DashDownloader } from './dash-downloader.js';
|
import { DashDownloader } from './dash-downloader.js';
|
||||||
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
|
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
|
||||||
import { encodeToMp3 } from './mp3-encoder.js';
|
import { encodeToMp3 } from './mp3-encoder.js';
|
||||||
import { ffmpeg } from './ffmpeg.js';
|
import { ffmpeg, loadFfmpeg } from './ffmpeg.js';
|
||||||
import { initTagLib } from './taglib.js';
|
import { initTagLib } from './taglib.js';
|
||||||
|
|
||||||
const downloadTasks = new Map();
|
const downloadTasks = new Map();
|
||||||
|
|
@ -273,6 +273,9 @@ async function downloadTrackBlob(track, quality, api, lyricsManager = null, sign
|
||||||
// Initialize taglib in the background.
|
// Initialize taglib in the background.
|
||||||
initTagLib().catch(console.error);
|
initTagLib().catch(console.error);
|
||||||
|
|
||||||
|
// Load ffmpeg in the background.
|
||||||
|
loadFfmpeg().catch(console.error);
|
||||||
|
|
||||||
let enrichedTrack = {
|
let enrichedTrack = {
|
||||||
...track,
|
...track,
|
||||||
artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null),
|
artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null),
|
||||||
|
|
|
||||||
24
js/ffmpeg.js
24
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 {
|
class FfmpegError extends Error {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
super(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(
|
async function ffmpegWorker(
|
||||||
audioBlob,
|
audioBlob,
|
||||||
args = {},
|
args = {},
|
||||||
|
|
@ -15,6 +34,7 @@ async function ffmpegWorker(
|
||||||
signal = null
|
signal = null
|
||||||
) {
|
) {
|
||||||
const audioData = await audioBlob.arrayBuffer();
|
const audioData = await audioBlob.arrayBuffer();
|
||||||
|
const assets = loadFfmpeg();
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const worker = new Worker(new URL('./ffmpeg.worker.js', import.meta.url), { type: 'module' });
|
const worker = new Worker(new URL('./ffmpeg.worker.js', import.meta.url), { type: 'module' });
|
||||||
|
|
@ -57,7 +77,7 @@ async function ffmpegWorker(
|
||||||
reject(new FfmpegError('Worker failed: ' + error.message));
|
reject(new FfmpegError('Worker failed: ' + error.message));
|
||||||
};
|
};
|
||||||
|
|
||||||
// Transfer audio data to worker
|
(async () => {
|
||||||
worker.postMessage(
|
worker.postMessage(
|
||||||
{
|
{
|
||||||
audioData,
|
audioData,
|
||||||
|
|
@ -66,9 +86,11 @@ async function ffmpegWorker(
|
||||||
name: outputName,
|
name: outputName,
|
||||||
mime: outputMime,
|
mime: outputMime,
|
||||||
},
|
},
|
||||||
|
loadOptions: await assets,
|
||||||
},
|
},
|
||||||
[audioData]
|
[audioData]
|
||||||
);
|
);
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
import { FFmpeg } from '@ffmpeg/ffmpeg';
|
||||||
import { toBlobURL } from '@ffmpeg/util';
|
|
||||||
|
|
||||||
let ffmpeg = null;
|
let ffmpeg = null;
|
||||||
let loadingPromise = null;
|
let loadingPromise = null;
|
||||||
|
|
||||||
async function loadFFmpeg() {
|
async function loadFFmpeg(loadOptions = {}) {
|
||||||
if (loadingPromise) return loadingPromise;
|
if (loadingPromise) return loadingPromise;
|
||||||
|
|
||||||
loadingPromise = (async () => {
|
loadingPromise = (async () => {
|
||||||
|
|
@ -25,11 +24,7 @@ async function loadFFmpeg() {
|
||||||
|
|
||||||
self.postMessage({ type: 'progress', stage: 'loading', message: 'Loading FFmpeg...' });
|
self.postMessage({ type: 'progress', stage: 'loading', message: 'Loading FFmpeg...' });
|
||||||
|
|
||||||
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
|
await ffmpeg.load(loadOptions);
|
||||||
await ffmpeg.load({
|
|
||||||
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
|
|
||||||
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
|
|
||||||
});
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return loadingPromise;
|
return loadingPromise;
|
||||||
|
|
@ -45,10 +40,12 @@ self.onmessage = async (e) => {
|
||||||
},
|
},
|
||||||
encodeStartMessage = 'Encoding...',
|
encodeStartMessage = 'Encoding...',
|
||||||
encodeEndMessage = 'Finalizing...',
|
encodeEndMessage = 'Finalizing...',
|
||||||
|
loadOptions = {},
|
||||||
} = e.data;
|
} = e.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await loadFFmpeg();
|
console.log(loadOptions);
|
||||||
|
await loadFFmpeg(loadOptions);
|
||||||
|
|
||||||
self.postMessage({ type: 'progress', stage: 'encoding', message: encodeStartMessage });
|
self.postMessage({ type: 'progress', stage: 'encoding', message: encodeStartMessage });
|
||||||
|
|
||||||
|
|
|
||||||
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ffmpeg/core": "^0.12.10",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
"@kawarp/core": "^1.1.1",
|
"@kawarp/core": "^1.1.1",
|
||||||
|
|
@ -2578,6 +2579,15 @@
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"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": {
|
"node_modules/@ffmpeg/ffmpeg": {
|
||||||
"version": "0.12.15",
|
"version": "0.12.15",
|
||||||
"resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz",
|
"resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz",
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@
|
||||||
"source-map": "^0.7.4"
|
"source-map": "^0.7.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ffmpeg/core": "^0.12.10",
|
||||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||||
"@ffmpeg/util": "^0.12.2",
|
"@ffmpeg/util": "^0.12.2",
|
||||||
"@kawarp/core": "^1.1.1",
|
"@kawarp/core": "^1.1.1",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue