refactor(downloads/ffmpeg): refactor ffmpeg usage and add additional logging for ffmpeg
This commit is contained in:
parent
c865b21bf5
commit
37a74ad755
6 changed files with 58 additions and 25 deletions
|
|
@ -5139,7 +5139,9 @@
|
||||||
<span class="label">Lossless Container</span>
|
<span class="label">Lossless Container</span>
|
||||||
<span class="description">Container format for lossless downloads</span>
|
<span class="description">Container format for lossless downloads</span>
|
||||||
</div>
|
</div>
|
||||||
<select id="lossless-container-setting"></select>
|
<select id="lossless-container-setting">
|
||||||
|
<option value="nochange">Don't change</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { losslessContainerSettings } from './storage';
|
import { losslessContainerSettings } from './storage';
|
||||||
import { rebuildFlacWithoutMetadata } from './metadata.flac';
|
|
||||||
import { getExtensionFromBlob } from './utils';
|
import { getExtensionFromBlob } from './utils';
|
||||||
|
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
||||||
import {
|
import {
|
||||||
type ProgressEvent,
|
type ProgressEvent,
|
||||||
isCustomFormat,
|
isCustomFormat,
|
||||||
|
|
@ -9,7 +9,7 @@ import {
|
||||||
getContainerFormat,
|
getContainerFormat,
|
||||||
transcodeWithContainerFormat,
|
transcodeWithContainerFormat,
|
||||||
} from './ffmpegFormats';
|
} from './ffmpegFormats';
|
||||||
import { ffmpeg } from './ffmpeg';
|
import { ffmpegNewContainer } from './ffmpeg';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers a browser file download for the given blob.
|
* Triggers a browser file download for the given blob.
|
||||||
|
|
@ -60,12 +60,20 @@ export async function applyAudioPostProcessing(
|
||||||
if (quality.endsWith('LOSSLESS')) {
|
if (quality.endsWith('LOSSLESS')) {
|
||||||
try {
|
try {
|
||||||
const containerFmt = getContainerFormat(losslessContainerSettings.getContainer());
|
const containerFmt = getContainerFormat(losslessContainerSettings.getContainer());
|
||||||
if (containerFmt) {
|
const extension = await getExtensionFromBlob(blob);
|
||||||
if (await containerFmt.needsTranscode(blob)) {
|
|
||||||
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
|
if (await containerFmt?.needsTranscode(blob)) {
|
||||||
} else if ((await getExtensionFromBlob(blob)) === 'flac') {
|
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
|
||||||
blob = await rebuildFlacWithoutMetadata(blob);
|
} else if (extension == 'flac') {
|
||||||
}
|
blob = await rebuildFlacWithoutMetadata(blob);
|
||||||
|
} else {
|
||||||
|
blob = await ffmpegNewContainer(
|
||||||
|
blob,
|
||||||
|
extension == 'm4a' ? 'mp4' : extension,
|
||||||
|
blob.type,
|
||||||
|
onProgress,
|
||||||
|
signal
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as Error)?.name === 'AbortError') {
|
if ((error as Error)?.name === 'AbortError') {
|
||||||
|
|
|
||||||
26
js/ffmpeg.js
26
js/ffmpeg.js
|
|
@ -27,7 +27,7 @@ export function loadFfmpeg() {
|
||||||
|
|
||||||
async function ffmpegWorker(
|
async function ffmpegWorker(
|
||||||
audioBlob,
|
audioBlob,
|
||||||
args = {},
|
args = [],
|
||||||
outputName = 'output',
|
outputName = 'output',
|
||||||
outputMime = 'application/octet-stream',
|
outputMime = 'application/octet-stream',
|
||||||
onProgress = null,
|
onProgress = null,
|
||||||
|
|
@ -93,7 +93,7 @@ async function ffmpegWorker(
|
||||||
{
|
{
|
||||||
audioData,
|
audioData,
|
||||||
extraFiles,
|
extraFiles,
|
||||||
...args,
|
args,
|
||||||
output: {
|
output: {
|
||||||
name: outputName,
|
name: outputName,
|
||||||
mime: outputMime,
|
mime: outputMime,
|
||||||
|
|
@ -108,7 +108,7 @@ async function ffmpegWorker(
|
||||||
|
|
||||||
export async function ffmpeg(
|
export async function ffmpeg(
|
||||||
audioBlob,
|
audioBlob,
|
||||||
args = {},
|
args = [],
|
||||||
outputName = 'output',
|
outputName = 'output',
|
||||||
outputMime = 'application/octet-stream',
|
outputMime = 'application/octet-stream',
|
||||||
onProgress = null,
|
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 };
|
export { FfmpegError };
|
||||||
|
|
|
||||||
|
|
@ -141,15 +141,21 @@ self.onmessage = async (e) => {
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
if (audioData) await ffmpeg.deleteFile('input');
|
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) {
|
for (const file of extraFiles) {
|
||||||
try {
|
try {
|
||||||
await ffmpeg.deleteFile(file.name);
|
await ffmpeg.deleteFile(file.name);
|
||||||
} catch {}
|
} catch {
|
||||||
|
self.postMessage({ type: 'log', message: `Failed to delete ${file.name} from FFmpeg FS.` });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await ffmpeg.deleteFile(output.name);
|
await ffmpeg.deleteFile(output.name);
|
||||||
} catch {}
|
} catch {
|
||||||
|
self.postMessage({ type: 'log', message: `Failed to delete ${output.name} from FFmpeg FS.` });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
self.postMessage({ type: 'error', message: error.message });
|
self.postMessage({ type: 'error', message: error.message });
|
||||||
|
|
|
||||||
|
|
@ -154,14 +154,6 @@ export const containerFormats: Record<string, ContainerFormat> = {
|
||||||
extension: 'm4a',
|
extension: 'm4a',
|
||||||
needsTranscode: async () => true,
|
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 */
|
/** Returns true if the quality string identifies a known custom ffmpeg-transcoded format */
|
||||||
|
|
@ -192,7 +184,7 @@ export async function transcodeWithCustomFormat(
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
return ffmpeg(
|
return ffmpeg(
|
||||||
audioBlob,
|
audioBlob,
|
||||||
{ args: format.ffmpegArgs },
|
format.ffmpegArgs,
|
||||||
format.outputFilename,
|
format.outputFilename,
|
||||||
format.outputMime,
|
format.outputMime,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
|
@ -214,7 +206,7 @@ export async function transcodeWithContainerFormat(
|
||||||
): Promise<Blob> {
|
): Promise<Blob> {
|
||||||
return ffmpeg(
|
return ffmpeg(
|
||||||
audioBlob,
|
audioBlob,
|
||||||
{ args: format.ffmpegArgs },
|
format.ffmpegArgs,
|
||||||
format.outputFilename,
|
format.outputFilename,
|
||||||
format.outputMime,
|
format.outputMime,
|
||||||
onProgress,
|
onProgress,
|
||||||
|
|
|
||||||
|
|
@ -877,6 +877,9 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (losslessContainerSetting) {
|
if (losslessContainerSetting) {
|
||||||
|
const noChangeOption = losslessContainerSetting.querySelector('option:last-child');
|
||||||
|
noChangeOption.remove();
|
||||||
|
|
||||||
for (const [internalName, { displayName }] of Object.entries(containerFormats)) {
|
for (const [internalName, { displayName }] of Object.entries(containerFormats)) {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = internalName;
|
option.value = internalName;
|
||||||
|
|
@ -884,6 +887,8 @@ export function initializeSettings(scrobbler, player, api, ui) {
|
||||||
losslessContainerSetting.appendChild(option);
|
losslessContainerSetting.appendChild(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
losslessContainerSetting.append(noChangeOption);
|
||||||
|
|
||||||
losslessContainerSetting.value = losslessContainerSettings.getContainer();
|
losslessContainerSetting.value = losslessContainerSettings.getContainer();
|
||||||
|
|
||||||
losslessContainerSetting.addEventListener('change', (e) => {
|
losslessContainerSetting.addEventListener('change', (e) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue