From 9f7b0ff5253838067156a9dc9ced81d9bc36131b Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:36:26 -0500 Subject: [PATCH] refactor(ffmpeg): add ffmpegInfo function and refactor ffmpeg usage --- js/api.js | 17 ++++---- js/downloads.js | 21 ++++++++++ js/ffmpeg.js | 96 ++++++++++++++++++++++++++++++++++----------- js/ffmpeg.types.ts | 2 +- js/ffmpeg.worker.js | 8 ++-- js/ffmpegFormats.ts | 26 ++++++------ js/mp3-encoder.js | 39 ------------------ 7 files changed, 122 insertions(+), 87 deletions(-) delete mode 100644 js/mp3-encoder.js diff --git a/js/api.js b/js/api.js index 68552bc..08b6829 100644 --- a/js/api.js +++ b/js/api.js @@ -14,7 +14,6 @@ import { trackDateSettings } from './storage.js'; import { APICache } from './cache.js'; import { DashDownloader } from './dash-downloader.ts'; import { HlsDownloader } from './hls-downloader.js'; -import { MP3EncodingError } from './mp3-encoder.js'; import { loadFfmpeg, FfmpegError, ffmpeg } from './ffmpeg.js'; import { triggerDownload, applyAudioPostProcessing } from './download-utils.ts'; import { isCustomFormat } from './ffmpegFormats.ts'; @@ -1877,7 +1876,15 @@ export class LosslessAPI { try { if (isVideo) { blob = new File( - [await ffmpeg(blob, ['-c', 'copy'], 'output.mp4', 'video/mp4', onProgress, options.signal)], + [ + await ffmpeg(blob, { + args: ['-c', 'copy'], + outputName: 'output.mp4', + outputMime: 'video/mp4', + onProgress, + signal: options.signal, + }), + ], 'output.mp4', { type: 'video/mp4' } ); @@ -1908,11 +1915,7 @@ export class LosslessAPI { throw error; } console.error('Download failed:', error); - if ( - error instanceof MP3EncodingError || - error instanceof FfmpegError || - error.code === 'MP3_ENCODING_FAILED' - ) { + if (error instanceof FfmpegError || error.code === 'MP3_ENCODING_FAILED') { throw error; } if (error.message === RATE_LIMIT_ERROR_MESSAGE) { diff --git a/js/downloads.js b/js/downloads.js index e4ab505..b42da41 100644 --- a/js/downloads.js +++ b/js/downloads.js @@ -980,11 +980,32 @@ function createBulkDownloadNotification(type, name, _totalItems) { return notifEl; } +/** + * + * @param {HTMLElement} notifEl + * @param {number} current + * @param {number} total + * @param {string} currentItem + * @param {FfmpegProgress | ProgressMessage | null} progress + * @returns + */ function updateBulkDownloadProgress(notifEl, current, total, currentItem, progress = null) { + /** @type {HTMLElement | null} */ const progressFill = notifEl.querySelector('.download-progress-fill'); + + /** @type {HTMLElement | null} */ const statusEl = notifEl.querySelector('.download-status'); + if (!progressFill || !statusEl) { + console.log('Progress elements not found in notification'); + return; + } + if (progress instanceof FfmpegProgress) { + if (progress.stage == 'stdout') { + return; + } + const percent = progress.progress || 0; progressFill.style.width = `${percent}%`; progressFill.style.background = '#3b82f6'; // Blue for encoding diff --git a/js/ffmpeg.js b/js/ffmpeg.js index fff10f0..dc9eef3 100644 --- a/js/ffmpeg.js +++ b/js/ffmpeg.js @@ -38,6 +38,7 @@ export function loadFfmpeg() { * @param {(progress: FfmpegProgress) => void} onProgress * @param {AbortSignal|null} signal * @param {Array<{name: string, data: ArrayBuffer | Uint8Array}>} extraFiles + * @param {Boolean} logConsole - Whether to log FFmpeg output to the console * @returns {Promise} Encoded audio blob */ async function ffmpegWorker( @@ -47,7 +48,8 @@ async function ffmpegWorker( outputMime = 'application/octet-stream', onProgress = null, signal = null, - extraFiles = [] + extraFiles = [], + logConsole = true ) { const audioData = audioBlob ? await audioBlob.arrayBuffer() : null; const assets = loadFfmpeg(); @@ -85,7 +87,10 @@ async function ffmpegWorker( } else if (type === 'progress' && stage != 'loading' && progress !== null) { onProgress?.(new FfmpegProgress(stage, progress || 0, message)); } else if (type === 'log') { - console.log('[FFmpeg]', message); + onProgress?.(new FfmpegProgress('stdout', 0, message)); + if (logConsole) { + console.log('[FFmpeg]', message); + } } }; @@ -127,29 +132,43 @@ async function ffmpegWorker( * Encodes audio using FFmpeg via Web Worker * @async * @param {Blob} audioBlob - The audio blob to encode - * @param {string[]} [args=[]] - FFmpeg command-line arguments - * @param {string} [outputName='output'] - Name of the output file - * @param {string} [outputMime='application/octet-stream'] - MIME type of the output - * @param {(progress: FfmpegProgress) => void} [onProgress=null] - Optional callback for progress updates - * @param {AbortSignal|null} [signal=null] - Optional abort signal to cancel encoding - * @param {Array} [extraFiles=[]] - Additional files to provide to FFmpeg + * @param {Object} [opts] - Options for FFmpeg encoding + * @param {string[]} [opts.args=[]] - FFmpeg command-line arguments + * @param {string} [opts.outputName='output'] - Name of the output file + * @param {string} [opts.outputMime='application/octet-stream'] - MIME type of the output + * @param {(progress: FfmpegProgress) => void} [opts.onProgress=null] - Optional callback for progress updates + * @param {AbortSignal|null} [opts.signal=null] - Optional abort signal to cancel encoding + * @param {Array} [opts.extraFiles=[]] - Additional files to provide to FFmpeg + * @param {Boolean} [opts.logConsole=true] - Whether to log FFmpeg output to the console * @returns {Promise} Encoded audio blob * @throws {FfmpegError} If Web Workers are not available * @throws {Error} If FFmpeg encoding fails */ export async function ffmpeg( audioBlob, - args = [], - outputName = 'output', - outputMime = 'application/octet-stream', - onProgress = null, - signal = null, - extraFiles = [] + { + args = [], + outputName = 'output', + outputMime = 'application/octet-stream', + onProgress = null, + signal = null, + extraFiles = [], + logConsole = true, + } = {} ) { try { // Use Web Worker for non-blocking FFmpeg encoding if (typeof Worker !== 'undefined') { - return await ffmpegWorker(audioBlob, args, outputName, outputMime, onProgress, signal, extraFiles); + return await ffmpegWorker( + audioBlob, + args, + outputName, + outputMime, + onProgress, + signal, + extraFiles, + logConsole + ); } throw new FfmpegError('Web Workers are required for FFMPEG'); @@ -159,24 +178,55 @@ export async function ffmpeg( } } +/** + * Retrieves information about an audio blob using FFmpeg + * @param {Blob} audioBlob - The audio blob to analyze + * @param {Object} [options] - Options for FFmpeg info extraction + * @param {((progress: FfmpegProgress) => void) | null} [options.onProgress] - Callback function to track conversion progress + * @param {AbortSignal|null} [options.signal] - AbortSignal for cancelling the operation + * @returns {Promise} A promise that resolves to an array of output lines + */ +export async function ffmpegInfo(audioBlob, { onProgress = null, signal = null } = {}) { + const outputLines = []; + try { + await ffmpeg(audioBlob, { + args: ['-t', '0.01'], + outputName: 'output.wav', + onProgress: (progress) => { + if (progress.stage === 'stdout' && progress.message) { + outputLines.push(progress.message); + } + + onProgress?.(progress); + }, + logConsole: false, + }); + } catch (err) { + if (err instanceof FfmpegError && !err.message.startsWith('Failed to delete')) { + console.warn('FFmpeg info extraction failed:', err); + } + } + + return outputLines; +} + /** * Creates a new FFmpeg container with copied codec and stripped metadata. * @param {Blob} audioBlob - The audio blob to process * @param {string} outputExtension - The extension for the output file * @param {string} outputMime - The MIME type for the output blob - * @param {Function} onProgress - Callback function to track conversion progress + * @param {((progress: FfmpegProgress) => void) | null} onProgress - Callback function to track conversion progress * @param {AbortSignal} signal - AbortSignal for cancelling the operation * @returns {Promise} A promise that resolves to the processed data blob */ export async function ffmpegNewContainer(audioBlob, outputExtension, outputMime, onProgress, signal) { - return await ffmpeg( - audioBlob, - ['-map_metadata', '-1', '-c', 'copy', '-strict', '-2'], - `output.${outputExtension}`, - outputMime, + return await ffmpeg(audioBlob, { + args: ['-map_metadata', '-1', '-c', 'copy', '-strict', '-2'], + outputName: `output.${outputExtension}`, + outputMime: outputMime, onProgress, - signal - ); + signal: signal, + }); } export { FfmpegError }; diff --git a/js/ffmpeg.types.ts b/js/ffmpeg.types.ts index 43ffa38..2eb82dd 100644 --- a/js/ffmpeg.types.ts +++ b/js/ffmpeg.types.ts @@ -1,6 +1,6 @@ export class FfmpegProgress implements MonochromeProgress { constructor( - public readonly stage: 'loading' | 'encoding' | 'finalizing', + public readonly stage: 'loading' | 'encoding' | 'finalizing' | 'stdout', public readonly progress: number, public readonly message?: string ) {} diff --git a/js/ffmpeg.worker.js b/js/ffmpeg.worker.js index e331ec6..b34c4ce 100644 --- a/js/ffmpeg.worker.js +++ b/js/ffmpeg.worker.js @@ -123,7 +123,7 @@ self.onmessage = async (e) => { await ffmpeg.writeFile(file.name, new Uint8Array(file.data)); } - const ffmpegArgs = ['-i', 'input', ...args, output.name]; + const ffmpegArgs = ['-i', 'input', ...args, ...(output.name ? [output.name] : [])]; self.postMessage({ type: 'log', message: `FFmpeg command: ffmpeg ${ffmpegArgs.join(' ')}` }); const exitCode = await ffmpeg.exec(ffmpegArgs); @@ -134,7 +134,7 @@ self.onmessage = async (e) => { self.postMessage({ type: 'progress', stage: 'finalizing', message: encodeEndMessage, progress: 100.0 }); - const data = await ffmpeg.readFile(output.name); + const data = output.name ? await ffmpeg.readFile(output.name) : []; const outputBlob = new Blob([data], { type: output.mime }); self.postMessage({ type: 'complete', blob: outputBlob }); @@ -152,7 +152,9 @@ self.onmessage = async (e) => { } } try { - await ffmpeg.deleteFile(output.name); + if (output.name) { + await ffmpeg.deleteFile(output.name); + } } catch { self.postMessage({ type: 'log', message: `Failed to delete ${output.name} from FFmpeg FS.` }); } diff --git a/js/ffmpegFormats.ts b/js/ffmpegFormats.ts index 562cbd0..eeb27a1 100644 --- a/js/ffmpegFormats.ts +++ b/js/ffmpegFormats.ts @@ -194,15 +194,14 @@ export async function transcodeWithCustomFormat( signal: AbortSignal | null = null, extraFiles: any[] = [] ): Promise { - return ffmpeg( - audioBlob, - format.ffmpegArgs, - format.outputFilename, - format.outputMime, + return ffmpeg(audioBlob, { + args: format.ffmpegArgs, + outputName: format.outputFilename, + outputMime: format.outputMime, onProgress, signal, - extraFiles - ); + extraFiles, + }); } /** @@ -216,13 +215,12 @@ export async function transcodeWithContainerFormat( signal: AbortSignal | null = null, extraFiles: any[] = [] ): Promise { - return ffmpeg( - audioBlob, - format.ffmpegArgs, - format.outputFilename, - format.outputMime, + return ffmpeg(audioBlob, { + args: format.ffmpegArgs, + outputName: format.outputFilename, + outputMime: format.outputMime, onProgress, signal, - extraFiles - ); + extraFiles, + }); } diff --git a/js/mp3-encoder.js b/js/mp3-encoder.js deleted file mode 100644 index f45811a..0000000 --- a/js/mp3-encoder.js +++ /dev/null @@ -1,39 +0,0 @@ -import { ffmpeg } from './ffmpeg'; - -/** - * @typedef {import('./ffmpeg.types.ts').FfmpegProgress} FfmpegProgress - */ - -class MP3EncodingError extends Error { - constructor(message) { - super(message); - this.name = 'MP3EncodingError'; - this.code = 'MP3_ENCODING_FAILED'; - } -} - -/** - * - * @param {Blob} audioBlob - * @param {(progress: FfmpegProgress) => void} [onProgress=null] - * @param {AbortSignal|null} [signal=null] - * @returns {Promise} Encoded MP3 audio blob - */ -export async function encodeToMp3(audioBlob, onProgress = null, signal = null) { - try { - // Use Web Worker for non-blocking FFmpeg encoding - if (typeof Worker !== 'undefined') { - const args = ['-map_metadata', '-1', '-c:a', 'libmp3lame', '-b:a', '320k', '-ar', '44100']; - - return await ffmpeg(audioBlob, { args }, 'output.mp3', 'audio/mpeg', onProgress, signal); - } - - throw new MP3EncodingError('Web Workers are required for MP3 encoding'); - } catch (error) { - console.error('MP3 encoding failed:', error); - - throw new MP3EncodingError(error?.message ?? error); - } -} - -export { MP3EncodingError };