fix(downloading): hi-res M4A's having no embedded covers (FUCK YOU TAGLIB)
This commit is contained in:
parent
0ed82f586c
commit
30b2e7d445
8 changed files with 280 additions and 45 deletions
108
js/api.js
108
js/api.js
|
|
@ -5,6 +5,9 @@ import {
|
|||
delay,
|
||||
isTrackUnavailable,
|
||||
getExtensionFromBlob,
|
||||
getTrackTitle,
|
||||
getFullArtistString,
|
||||
getMimeType,
|
||||
} from './utils.js';
|
||||
import { trackDateSettings, losslessContainerSettings } from './storage.js';
|
||||
import { APICache } from './cache.js';
|
||||
|
|
@ -12,7 +15,7 @@ import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
|
|||
import { DashDownloader } from './dash-downloader.js';
|
||||
import { HlsDownloader } from './hls-downloader.js';
|
||||
import { MP3EncodingError } from './mp3-encoder.js';
|
||||
import { loadFfmpeg, FfmpegError } from './ffmpeg.js';
|
||||
import { ffmpeg, loadFfmpeg, FfmpegError } from './ffmpeg.js';
|
||||
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
||||
import {
|
||||
isCustomFormat,
|
||||
|
|
@ -1423,12 +1426,65 @@ export class LosslessAPI {
|
|||
}
|
||||
|
||||
if (!isVideo) {
|
||||
const coverBlobToEmbed = await prefetchPromises.coverFetch;
|
||||
const extraFiles = [];
|
||||
const ffmpegMetadataArgs = [];
|
||||
|
||||
if (coverBlobToEmbed) {
|
||||
const coverBuffer = await coverBlobToEmbed.arrayBuffer();
|
||||
const coverExt = getMimeType(new Uint8Array(coverBuffer)) === 'image/png' ? 'png' : 'jpg';
|
||||
const coverName = `cover.${coverExt}`;
|
||||
extraFiles.push({
|
||||
name: coverName,
|
||||
data: coverBuffer
|
||||
});
|
||||
ffmpegMetadataArgs.push('-i', coverName);
|
||||
}
|
||||
|
||||
if (track) {
|
||||
ffmpegMetadataArgs.push(
|
||||
'-metadata', `title=${getTrackTitle(track)}`,
|
||||
'-metadata', `artist=${getFullArtistString(track)}`,
|
||||
'-metadata', `album=${track.album?.title || ''}`,
|
||||
'-metadata', `album_artist=${track.album?.artist?.name || track.artist?.name || ''}`
|
||||
);
|
||||
|
||||
const trackNum = track.trackNumber;
|
||||
if (trackNum) {
|
||||
const totalTracks = track.album?.numberOfTracks;
|
||||
ffmpegMetadataArgs.push('-metadata', `track=${trackNum}${totalTracks ? `/${totalTracks}` : ''}`);
|
||||
}
|
||||
|
||||
const discNum = track.volumeNumber || track.discNumber;
|
||||
if (discNum) {
|
||||
ffmpegMetadataArgs.push('-metadata', `disc=${discNum}`);
|
||||
}
|
||||
|
||||
const releaseDate = track.album?.releaseDate || track?.streamStartDate;
|
||||
if (releaseDate) {
|
||||
ffmpegMetadataArgs.push('-metadata', `date=${releaseDate.split('-')[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Transcode to custom format if requested
|
||||
if (isCustomFormat(quality)) {
|
||||
const format = getCustomFormat(quality);
|
||||
if (format) {
|
||||
try {
|
||||
blob = await transcodeWithCustomFormat(blob, format, onProgress, options.signal);
|
||||
const args = [...ffmpegMetadataArgs, ...format.ffmpegArgs];
|
||||
if (coverBlobToEmbed) {
|
||||
args.push('-map', '0:a', '-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic');
|
||||
}
|
||||
|
||||
blob = await ffmpeg(
|
||||
blob,
|
||||
{ args },
|
||||
format.outputFilename,
|
||||
format.outputMime,
|
||||
onProgress,
|
||||
options.signal,
|
||||
extraFiles
|
||||
);
|
||||
} catch (encodingError) {
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
|
|
@ -1443,18 +1499,56 @@ export class LosslessAPI {
|
|||
|
||||
if (quality.endsWith('LOSSLESS')) {
|
||||
try {
|
||||
const containerFmt = getContainerFormat(losslessContainerSettings.getContainer());
|
||||
if (containerFmt) {
|
||||
const containerType = losslessContainerSettings.getContainer();
|
||||
const containerFmt = getContainerFormat(containerType);
|
||||
|
||||
if (containerFmt && containerType !== 'nochange') {
|
||||
if (await containerFmt.needsTranscode(blob)) {
|
||||
blob = await transcodeWithContainerFormat(
|
||||
const args = [...ffmpegMetadataArgs, ...containerFmt.ffmpegArgs];
|
||||
if (coverBlobToEmbed) {
|
||||
args.push('-map', '0:a', '-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic');
|
||||
}
|
||||
|
||||
blob = await ffmpeg(
|
||||
blob,
|
||||
containerFmt,
|
||||
{ args },
|
||||
containerFmt.outputFilename,
|
||||
containerFmt.outputMime,
|
||||
onProgress,
|
||||
options.signal
|
||||
options.signal,
|
||||
extraFiles
|
||||
);
|
||||
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
|
||||
blob = await rebuildFlacWithoutMetadata(blob);
|
||||
}
|
||||
} else {
|
||||
const actualExtension = await getExtensionFromBlob(blob);
|
||||
if (actualExtension === 'm4a' || actualExtension === 'mp4') {
|
||||
try {
|
||||
const ffmpegArgs = [...ffmpegMetadataArgs];
|
||||
|
||||
ffmpegArgs.push('-map', '0:a');
|
||||
if (coverBlobToEmbed) {
|
||||
ffmpegArgs.push('-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic');
|
||||
}
|
||||
ffmpegArgs.push('-c:a', 'copy', '-movflags', '+faststart', '-strict', '-2');
|
||||
|
||||
const remuxedBlob = await ffmpeg(
|
||||
blob,
|
||||
{ args: ffmpegArgs },
|
||||
'output.mp4',
|
||||
'audio/mp4',
|
||||
onProgress,
|
||||
options.signal,
|
||||
extraFiles
|
||||
);
|
||||
if (remuxedBlob) {
|
||||
blob = remuxedBlob;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to remux hi-res M4A, proceeding with original:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
|
|
|
|||
108
js/downloads.js
108
js/downloads.js
|
|
@ -11,13 +11,15 @@ import {
|
|||
getExtensionFromBlob,
|
||||
escapeHtml,
|
||||
getTrackDiscNumber,
|
||||
getFullArtistString,
|
||||
getMimeType,
|
||||
} from './utils.js';
|
||||
import { lyricsSettings, bulkDownloadSettings, losslessContainerSettings, playlistSettings } from './storage.js';
|
||||
import { addMetadataToAudio, prefetchMetadataObjects } from './metadata.js';
|
||||
import { rebuildFlacWithoutMetadata } from './metadata.flac.js';
|
||||
import { DashDownloader } from './dash-downloader.js';
|
||||
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
|
||||
import { loadFfmpeg } from './ffmpeg.js';
|
||||
import { ffmpeg, loadFfmpeg } from './ffmpeg.js';
|
||||
import {
|
||||
isCustomFormat,
|
||||
getCustomFormat,
|
||||
|
|
@ -418,23 +420,119 @@ async function downloadTrackBlob(
|
|||
blob = await response.blob();
|
||||
}
|
||||
|
||||
const coverBlobToEmbed = await prefetchPromises.coverFetch;
|
||||
const extraFiles = [];
|
||||
const ffmpegMetadataArgs = [];
|
||||
|
||||
if (coverBlobToEmbed) {
|
||||
const coverBuffer = await coverBlobToEmbed.arrayBuffer();
|
||||
const coverExt = getMimeType(new Uint8Array(coverBuffer)) === 'image/png' ? 'png' : 'jpg';
|
||||
const coverName = `cover.${coverExt}`;
|
||||
extraFiles.push({
|
||||
name: coverName,
|
||||
data: coverBuffer
|
||||
});
|
||||
ffmpegMetadataArgs.push('-i', coverName);
|
||||
}
|
||||
|
||||
if (enrichedTrack) {
|
||||
ffmpegMetadataArgs.push(
|
||||
'-metadata', `title=${getTrackTitle(enrichedTrack)}`,
|
||||
'-metadata', `artist=${getFullArtistString(enrichedTrack)}`,
|
||||
'-metadata', `album=${enrichedTrack.album?.title || ''}`,
|
||||
'-metadata', `album_artist=${enrichedTrack.album?.artist?.name || enrichedTrack.artist?.name || ''}`
|
||||
);
|
||||
|
||||
const trackNum = enrichedTrack.trackNumber;
|
||||
if (trackNum) {
|
||||
const totalTracks = enrichedTrack.album?.numberOfTracks;
|
||||
ffmpegMetadataArgs.push('-metadata', `track=${trackNum}${totalTracks ? `/${totalTracks}` : ''}`);
|
||||
}
|
||||
|
||||
const discNum = enrichedTrack.volumeNumber || enrichedTrack.discNumber;
|
||||
if (discNum) {
|
||||
ffmpegMetadataArgs.push('-metadata', `disc=${discNum}`);
|
||||
}
|
||||
|
||||
const releaseDate = enrichedTrack.album?.releaseDate || enrichedTrack?.streamStartDate;
|
||||
if (releaseDate) {
|
||||
ffmpegMetadataArgs.push('-metadata', `date=${releaseDate.split('-')[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Transcode to custom format if requested
|
||||
if (isCustomFormat(quality)) {
|
||||
const format = getCustomFormat(quality);
|
||||
if (format) {
|
||||
blob = await transcodeWithCustomFormat(blob, format, onProgress || (() => undefined), signal);
|
||||
const args = [...ffmpegMetadataArgs, ...format.ffmpegArgs];
|
||||
if (coverBlobToEmbed) {
|
||||
args.push('-map', '0:a', '-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic');
|
||||
}
|
||||
|
||||
blob = await ffmpeg(
|
||||
blob,
|
||||
{ args },
|
||||
format.outputFilename,
|
||||
format.outputMime,
|
||||
onProgress,
|
||||
signal,
|
||||
extraFiles
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (quality.endsWith('LOSSLESS')) {
|
||||
try {
|
||||
const containerFmt = getContainerFormat(losslessContainerSettings.getContainer());
|
||||
if (containerFmt) {
|
||||
const containerType = losslessContainerSettings.getContainer();
|
||||
const containerFmt = getContainerFormat(containerType);
|
||||
|
||||
if (containerFmt && containerType !== 'nochange') {
|
||||
if (await containerFmt.needsTranscode(blob)) {
|
||||
blob = await transcodeWithContainerFormat(blob, containerFmt, onProgress, signal);
|
||||
const args = [...ffmpegMetadataArgs, ...containerFmt.ffmpegArgs];
|
||||
if (coverBlobToEmbed) {
|
||||
args.push('-map', '0:a', '-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic');
|
||||
}
|
||||
|
||||
blob = await ffmpeg(
|
||||
blob,
|
||||
{ args },
|
||||
containerFmt.outputFilename,
|
||||
containerFmt.outputMime,
|
||||
onProgress,
|
||||
signal,
|
||||
extraFiles
|
||||
);
|
||||
} else if ((await getExtensionFromBlob(blob)) == 'flac') {
|
||||
blob = await rebuildFlacWithoutMetadata(blob);
|
||||
}
|
||||
} else {
|
||||
const actualExtension = await getExtensionFromBlob(blob);
|
||||
if (actualExtension === 'm4a' || actualExtension === 'mp4') {
|
||||
try {
|
||||
const ffmpegArgs = [...ffmpegMetadataArgs];
|
||||
|
||||
ffmpegArgs.push('-map', '0:a');
|
||||
if (coverBlobToEmbed) {
|
||||
ffmpegArgs.push('-map', '1:v', '-c:v', 'copy', '-disposition:v:0', 'attached_pic');
|
||||
}
|
||||
ffmpegArgs.push('-c:a', 'copy', '-movflags', '+faststart', '-strict', '-2');
|
||||
|
||||
const remuxedBlob = await ffmpeg(
|
||||
blob,
|
||||
{ args: ffmpegArgs },
|
||||
'output.mp4',
|
||||
'audio/mp4',
|
||||
onProgress,
|
||||
signal,
|
||||
extraFiles
|
||||
);
|
||||
if (remuxedBlob) {
|
||||
blob = remuxedBlob;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to remux hi-res M4A, proceeding with original:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error?.name === 'AbortError') {
|
||||
|
|
|
|||
23
js/ffmpeg.js
23
js/ffmpeg.js
|
|
@ -32,9 +32,10 @@ async function ffmpegWorker(
|
|||
outputName = 'output',
|
||||
outputMime = 'application/octet-stream',
|
||||
onProgress = null,
|
||||
signal = null
|
||||
signal = null,
|
||||
extraFiles = []
|
||||
) {
|
||||
const audioData = await audioBlob.arrayBuffer();
|
||||
const audioData = audioBlob ? await audioBlob.arrayBuffer() : null;
|
||||
const assets = loadFfmpeg();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
@ -79,9 +80,20 @@ async function ffmpegWorker(
|
|||
};
|
||||
|
||||
(async () => {
|
||||
const transferables = [];
|
||||
if (audioData) transferables.push(audioData);
|
||||
for (const f of extraFiles) {
|
||||
if (f.data instanceof ArrayBuffer) {
|
||||
transferables.push(f.data);
|
||||
} else if (f.data.buffer instanceof ArrayBuffer) {
|
||||
transferables.push(f.data.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
worker.postMessage(
|
||||
{
|
||||
audioData,
|
||||
extraFiles,
|
||||
...args,
|
||||
output: {
|
||||
name: outputName,
|
||||
|
|
@ -89,7 +101,7 @@ async function ffmpegWorker(
|
|||
},
|
||||
loadOptions: await assets,
|
||||
},
|
||||
[audioData]
|
||||
transferables
|
||||
);
|
||||
})();
|
||||
});
|
||||
|
|
@ -101,12 +113,13 @@ export async function ffmpeg(
|
|||
outputName = 'output',
|
||||
outputMime = 'application/octet-stream',
|
||||
onProgress = null,
|
||||
signal = null
|
||||
signal = null,
|
||||
extraFiles = []
|
||||
) {
|
||||
try {
|
||||
// Use Web Worker for non-blocking FFmpeg encoding
|
||||
if (typeof Worker !== 'undefined') {
|
||||
return await ffmpegWorker(audioBlob, args, outputName, outputMime, onProgress, signal);
|
||||
return await ffmpegWorker(audioBlob, args, outputName, outputMime, onProgress, signal, extraFiles);
|
||||
}
|
||||
|
||||
throw new FfmpegError('Web Workers are required for FFMPEG');
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ async function loadFFmpeg(loadOptions = {}) {
|
|||
self.onmessage = async (e) => {
|
||||
const {
|
||||
audioData,
|
||||
extraFiles = [],
|
||||
args = [],
|
||||
output = {
|
||||
name: 'output',
|
||||
|
|
@ -109,42 +110,40 @@ self.onmessage = async (e) => {
|
|||
} = e.data;
|
||||
|
||||
try {
|
||||
console.log(loadOptions);
|
||||
await loadFFmpeg(loadOptions);
|
||||
|
||||
self.postMessage({ type: 'progress', stage: 'encoding', message: encodeStartMessage, progress: 0.0 });
|
||||
|
||||
try {
|
||||
// Write input file to FFmpeg virtual filesystem
|
||||
await ffmpeg.writeFile('input', new Uint8Array(audioData));
|
||||
if (audioData) {
|
||||
await ffmpeg.writeFile('input', new Uint8Array(audioData));
|
||||
}
|
||||
|
||||
for (const file of extraFiles) {
|
||||
await ffmpeg.writeFile(file.name, new Uint8Array(file.data));
|
||||
}
|
||||
|
||||
const ffmpegArgs = ['-i', 'input', ...args, output.name];
|
||||
self.postMessage({ type: 'log', message: `FFmpeg command: ffmpeg ${ffmpegArgs.join(' ')}` });
|
||||
|
||||
// Log the exact FFmpeg command being run for debugging.
|
||||
self.postMessage({ type: 'log', message: `Running with args: ${ffmpegArgs.join(' ')}` });
|
||||
const exitCode = await ffmpeg.exec(ffmpegArgs);
|
||||
|
||||
// Run FFMPEG with the provided arguments.
|
||||
await ffmpeg.exec(ffmpegArgs);
|
||||
if (exitCode !== 0) {
|
||||
throw new Error(`FFmpeg failed with exit code ${exitCode}.`);
|
||||
}
|
||||
|
||||
self.postMessage({ type: 'progress', stage: 'finalizing', message: encodeEndMessage, progress: 100.0 });
|
||||
|
||||
// Read output file - use Uint8Array directly to avoid extra bytes from ArrayBuffer
|
||||
const data = await ffmpeg.readFile(output.name);
|
||||
const outputBlob = new Blob([data], { type: output.mime });
|
||||
|
||||
self.postMessage({ type: 'complete', blob: outputBlob });
|
||||
} finally {
|
||||
// Always cleanup virtual filesystem files
|
||||
try {
|
||||
await ffmpeg.deleteFile('input');
|
||||
} catch {
|
||||
// File may not exist if writeFile failed
|
||||
}
|
||||
try {
|
||||
await ffmpeg.deleteFile(output.name);
|
||||
} catch {
|
||||
// File may not exist if exec failed
|
||||
try { if (audioData) await ffmpeg.deleteFile('input'); } catch {}
|
||||
for (const file of extraFiles) {
|
||||
try { await ffmpeg.deleteFile(file.name); } catch {}
|
||||
}
|
||||
try { await ffmpeg.deleteFile(output.name); } catch {}
|
||||
}
|
||||
} catch (error) {
|
||||
self.postMessage({ type: 'error', message: error.message });
|
||||
|
|
|
|||
|
|
@ -210,9 +210,18 @@ export async function transcodeWithCustomFormat(
|
|||
audioBlob: Blob,
|
||||
format: CustomFormat,
|
||||
onProgress: ((progress: ProgressEvent) => void) | null = null,
|
||||
signal: AbortSignal | null = null
|
||||
signal: AbortSignal | null = null,
|
||||
extraFiles: any[] = []
|
||||
): Promise<Blob> {
|
||||
return ffmpeg(audioBlob, { args: format.ffmpegArgs }, format.outputFilename, format.outputMime, onProgress, signal);
|
||||
return ffmpeg(
|
||||
audioBlob,
|
||||
{ args: format.ffmpegArgs },
|
||||
format.outputFilename,
|
||||
format.outputMime,
|
||||
onProgress,
|
||||
signal,
|
||||
extraFiles
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -223,7 +232,16 @@ export async function transcodeWithContainerFormat(
|
|||
audioBlob: Blob,
|
||||
format: ContainerFormat,
|
||||
onProgress: ((progress: ProgressEvent) => void) | null = null,
|
||||
signal: AbortSignal | null = null
|
||||
signal: AbortSignal | null = null,
|
||||
extraFiles: any[] = []
|
||||
): Promise<Blob> {
|
||||
return ffmpeg(audioBlob, { args: format.ffmpegArgs }, format.outputFilename, format.outputMime, onProgress, signal);
|
||||
return ffmpeg(
|
||||
audioBlob,
|
||||
{ args: format.ffmpegArgs },
|
||||
format.outputFilename,
|
||||
format.outputMime,
|
||||
onProgress,
|
||||
signal,
|
||||
extraFiles
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
getMimeType,
|
||||
getTrackCoverId,
|
||||
getTrackDiscNumber,
|
||||
getExtensionFromBlob,
|
||||
} from './utils.js';
|
||||
import { fetchTagLib, addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts';
|
||||
import { doTimed, doTimedAsync } from './doTimed.ts';
|
||||
|
|
@ -42,10 +43,18 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
|
|||
const { coverFetch, lyricsFetch } = prefetchPromises;
|
||||
|
||||
/**
|
||||
* @type {import("./taglib.types.ts").TagLibMetadata}
|
||||
* @type {import("./taglib.worker.ts").TagLibMetadata}
|
||||
*/
|
||||
const data = {};
|
||||
|
||||
const detectedExt = await getExtensionFromBlob(audioBlob);
|
||||
const isM4A = detectedExt === 'm4a' || detectedExt === 'mp4';
|
||||
|
||||
if (isM4A) {
|
||||
console.log('Skipping TagLib for M4A (handled by FFmpeg)');
|
||||
return audioBlob;
|
||||
}
|
||||
|
||||
const audioBuffer = await doTimedAsync('Get audio array buffer', () => audioBlob.arrayBuffer());
|
||||
|
||||
try {
|
||||
|
|
@ -55,8 +64,7 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
|
|||
data.albumArtist = track.album?.artist?.name || track.artist?.name;
|
||||
data.trackNumber = track.trackNumber;
|
||||
data.discNumber = track.volumeNumber ?? track.discNumber;
|
||||
data.totalTracks = track.album.numberOfTracksOnDisc ?? track.album.numberOfTracks;
|
||||
data.totalDiscs = track.album.totalDiscs;
|
||||
data.totalTracks = track.album.numberOfTracks;
|
||||
data.copyright = track.copyright;
|
||||
data.isrc = track.isrc;
|
||||
data.explicit = Boolean(track.explicit);
|
||||
|
|
@ -96,9 +104,9 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
|
|||
try {
|
||||
if (track.album?.cover) {
|
||||
const coverBlob = await coverFetch;
|
||||
const coverBuffer = new Uint8Array(await coverBlob.arrayBuffer());
|
||||
|
||||
if (coverBlob) {
|
||||
const coverBuffer = new Uint8Array(await coverBlob.arrayBuffer());
|
||||
data.cover = {
|
||||
data: coverBuffer,
|
||||
type: getMimeType(coverBuffer),
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import { getButterchurnPresets } from './visualizers/butterchurn.js';
|
|||
import { db } from './db.js';
|
||||
import { authManager } from './accounts/auth.js';
|
||||
import { syncManager } from './accounts/pocketbase.js';
|
||||
import { saveFirebaseConfig, clearFirebaseConfig } from './accounts/config.js';
|
||||
import { containerFormats, customFormats } from './ffmpegFormats.ts';
|
||||
|
||||
export function initializeSettings(scrobbler, player, api, ui) {
|
||||
|
|
|
|||
|
|
@ -47,7 +47,13 @@ export async function addMetadataWithTagLib(
|
|||
};
|
||||
worker.onerror = reject;
|
||||
worker.onmessageerror = reject;
|
||||
worker.postMessage({ ...data, type: 'Add', wasmUrl, audioData }, [audioData.buffer]);
|
||||
|
||||
const transferables: Transferable[] = [audioData.buffer];
|
||||
if ((data as any).cover?.data?.buffer instanceof ArrayBuffer) {
|
||||
transferables.push((data as any).cover.data.buffer);
|
||||
}
|
||||
|
||||
worker.postMessage({ ...data, type: 'Add', wasmUrl, audioData }, transferables);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue