From 37a74ad7555aadc5c416feb334d3655869a717b2 Mon Sep 17 00:00:00 2001 From: Daniel <790119+DanTheMan827@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:02:44 +0000 Subject: [PATCH] refactor(downloads/ffmpeg): refactor ffmpeg usage and add additional logging for ffmpeg --- index.html | 4 +++- js/download-utils.ts | 24 ++++++++++++++++-------- js/ffmpeg.js | 26 +++++++++++++++++++++++--- js/ffmpeg.worker.js | 12 +++++++++--- js/ffmpegFormats.ts | 12 ++---------- js/settings.js | 5 +++++ 6 files changed, 58 insertions(+), 25 deletions(-) diff --git a/index.html b/index.html index dfafb6d..4db6184 100644 --- a/index.html +++ b/index.html @@ -5139,7 +5139,9 @@ Lossless Container Container format for lossless downloads - +
diff --git a/js/download-utils.ts b/js/download-utils.ts index 763a78c..c5b17da 100644 --- a/js/download-utils.ts +++ b/js/download-utils.ts @@ -1,6 +1,6 @@ import { losslessContainerSettings } from './storage'; -import { rebuildFlacWithoutMetadata } from './metadata.flac'; import { getExtensionFromBlob } from './utils'; +import { rebuildFlacWithoutMetadata } from './metadata.flac.js'; import { type ProgressEvent, isCustomFormat, @@ -9,7 +9,7 @@ import { getContainerFormat, transcodeWithContainerFormat, } from './ffmpegFormats'; -import { ffmpeg } from './ffmpeg'; +import { ffmpegNewContainer } from './ffmpeg'; /** * Triggers a browser file download for the given blob. @@ -60,12 +60,20 @@ export async function applyAudioPostProcessing( if (quality.endsWith('LOSSLESS')) { try { const containerFmt = getContainerFormat(losslessContainerSettings.getContainer()); - if (containerFmt) { - if (await containerFmt.needsTranscode(blob)) { - blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal); - } else if ((await getExtensionFromBlob(blob)) === 'flac') { - blob = await rebuildFlacWithoutMetadata(blob); - } + const extension = await getExtensionFromBlob(blob); + + if (await containerFmt?.needsTranscode(blob)) { + blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal); + } else if (extension == 'flac') { + blob = await rebuildFlacWithoutMetadata(blob); + } else { + blob = await ffmpegNewContainer( + blob, + extension == 'm4a' ? 'mp4' : extension, + blob.type, + onProgress, + signal + ); } } catch (error) { if ((error as Error)?.name === 'AbortError') { diff --git a/js/ffmpeg.js b/js/ffmpeg.js index d62e713..28193fd 100644 --- a/js/ffmpeg.js +++ b/js/ffmpeg.js @@ -27,7 +27,7 @@ export function loadFfmpeg() { async function ffmpegWorker( audioBlob, - args = {}, + args = [], outputName = 'output', outputMime = 'application/octet-stream', onProgress = null, @@ -93,7 +93,7 @@ async function ffmpegWorker( { audioData, extraFiles, - ...args, + args, output: { name: outputName, mime: outputMime, @@ -108,7 +108,7 @@ async function ffmpegWorker( export async function ffmpeg( audioBlob, - args = {}, + args = [], outputName = 'output', outputMime = 'application/octet-stream', onProgress = null, @@ -128,4 +128,24 @@ export async function ffmpeg( } } +/** + * 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 {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, + onProgress, + signal + ); +} + export { FfmpegError }; diff --git a/js/ffmpeg.worker.js b/js/ffmpeg.worker.js index b90082d..e331ec6 100644 --- a/js/ffmpeg.worker.js +++ b/js/ffmpeg.worker.js @@ -141,15 +141,21 @@ self.onmessage = async (e) => { } finally { try { if (audioData) await ffmpeg.deleteFile('input'); - } catch {} + } catch { + self.postMessage({ type: 'log', message: 'Failed to delete input file from FFmpeg FS.' }); + } for (const file of extraFiles) { try { await ffmpeg.deleteFile(file.name); - } catch {} + } catch { + self.postMessage({ type: 'log', message: `Failed to delete ${file.name} from FFmpeg FS.` }); + } } try { await ffmpeg.deleteFile(output.name); - } catch {} + } catch { + self.postMessage({ type: 'log', message: `Failed to delete ${output.name} from FFmpeg FS.` }); + } } } catch (error) { self.postMessage({ type: 'error', message: error.message }); diff --git a/js/ffmpegFormats.ts b/js/ffmpegFormats.ts index 5fa25b5..ef69277 100644 --- a/js/ffmpegFormats.ts +++ b/js/ffmpegFormats.ts @@ -154,14 +154,6 @@ export const containerFormats: Record = { extension: 'm4a', needsTranscode: async () => true, }, - nochange: { - displayName: "Don't change", - ffmpegArgs: ['-c:a', 'copy', '-strict', '-2'], - outputFilename: 'output.mp4', - outputMime: 'audio/mp4', - extension: 'mp4', - needsTranscode: async (blob) => (await getExtensionFromBlob(blob)) == 'm4a', - }, }; /** Returns true if the quality string identifies a known custom ffmpeg-transcoded format */ @@ -192,7 +184,7 @@ export async function transcodeWithCustomFormat( ): Promise { return ffmpeg( audioBlob, - { args: format.ffmpegArgs }, + format.ffmpegArgs, format.outputFilename, format.outputMime, onProgress, @@ -214,7 +206,7 @@ export async function transcodeWithContainerFormat( ): Promise { return ffmpeg( audioBlob, - { args: format.ffmpegArgs }, + format.ffmpegArgs, format.outputFilename, format.outputMime, onProgress, diff --git a/js/settings.js b/js/settings.js index 8cbe240..191ea41 100644 --- a/js/settings.js +++ b/js/settings.js @@ -877,6 +877,9 @@ export function initializeSettings(scrobbler, player, api, ui) { } if (losslessContainerSetting) { + const noChangeOption = losslessContainerSetting.querySelector('option:last-child'); + noChangeOption.remove(); + for (const [internalName, { displayName }] of Object.entries(containerFormats)) { const option = document.createElement('option'); option.value = internalName; @@ -884,6 +887,8 @@ export function initializeSettings(scrobbler, player, api, ui) { losslessContainerSetting.appendChild(option); } + losslessContainerSetting.append(noChangeOption); + losslessContainerSetting.value = losslessContainerSettings.getContainer(); losslessContainerSetting.addEventListener('change', (e) => {