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) => {