refactor(downloads/ffmpeg): refactor ffmpeg usage and add additional logging for ffmpeg

This commit is contained in:
Daniel 2026-03-12 16:02:44 +00:00 committed by GitHub
parent c865b21bf5
commit 37a74ad755
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 58 additions and 25 deletions

View file

@ -5139,7 +5139,9 @@
<span class="label">Lossless Container</span>
<span class="description">Container format for lossless downloads</span>
</div>
<select id="lossless-container-setting"></select>
<select id="lossless-container-setting">
<option value="nochange">Don't change</option>
</select>
</div>
<div class="setting-item">
<div class="info">

View file

@ -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') {

View file

@ -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<Blob>} 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 };

View file

@ -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 });

View file

@ -154,14 +154,6 @@ export const containerFormats: Record<string, ContainerFormat> = {
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<Blob> {
return ffmpeg(
audioBlob,
{ args: format.ffmpegArgs },
format.ffmpegArgs,
format.outputFilename,
format.outputMime,
onProgress,
@ -214,7 +206,7 @@ export async function transcodeWithContainerFormat(
): Promise<Blob> {
return ffmpeg(
audioBlob,
{ args: format.ffmpegArgs },
format.ffmpegArgs,
format.outputFilename,
format.outputMime,
onProgress,

View file

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