Inline fixes: - Remove TDRC frame from ID3 writer (ID3v2.3 uses TYER only, not TDRC) - Add try/finally cleanup in worker to prevent VFS leaks on errors - Fix Blob creation to use Uint8Array directly (avoid extra bytes) - Replace loadFFmpeg race guard with promise singleton pattern - Add -map_metadata -1 to strip source metadata (prevent duplicate ID3) Error handling improvements: - Create MP3EncodingError class with code property for reliable detection - Update api.js to use instanceof check instead of string matching - Pass AbortSignal to encodeToMp3 for proper cancellation support - Remove error re-wrapping in mp3-encoder.js (preserve original errors) Technical details: - Promise singleton ensures FFmpeg loads once even with concurrent calls - AbortSignal listener properly cleaned up on success/error/abort - Virtual FS cleanup in finally block prevents file leaks - MP3EncodingError.code = 'MP3_ENCODING_FAILED' for robust detection
74 lines
2.4 KiB
JavaScript
74 lines
2.4 KiB
JavaScript
class MP3EncodingError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = 'MP3EncodingError';
|
|
this.code = 'MP3_ENCODING_FAILED';
|
|
}
|
|
}
|
|
|
|
async function encodeToMp3Worker(audioBlob, onProgress = null, signal = null) {
|
|
const audioData = await audioBlob.arrayBuffer();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const worker = new Worker(new URL('./mp3-encoder.worker.js', import.meta.url), { type: 'module' });
|
|
|
|
// Handle abort signal
|
|
const abortHandler = () => {
|
|
worker.terminate();
|
|
reject(new MP3EncodingError('MP3 encoding aborted'));
|
|
};
|
|
|
|
if (signal) {
|
|
if (signal.aborted) {
|
|
abortHandler();
|
|
return;
|
|
}
|
|
signal.addEventListener('abort', abortHandler);
|
|
}
|
|
|
|
worker.onmessage = (e) => {
|
|
const { type, blob, message, stage, progress } = e.data;
|
|
|
|
if (type === 'complete') {
|
|
if (signal) signal.removeEventListener('abort', abortHandler);
|
|
worker.terminate();
|
|
resolve(blob);
|
|
} else if (type === 'error') {
|
|
if (signal) signal.removeEventListener('abort', abortHandler);
|
|
worker.terminate();
|
|
reject(new MP3EncodingError(message));
|
|
} else if (type === 'progress' && onProgress) {
|
|
onProgress({ stage, message, progress });
|
|
} else if (type === 'log') {
|
|
console.log('[FFmpeg]', message);
|
|
}
|
|
};
|
|
|
|
worker.onerror = (error) => {
|
|
if (signal) signal.removeEventListener('abort', abortHandler);
|
|
worker.terminate();
|
|
reject(new MP3EncodingError('Worker failed: ' + error.message));
|
|
};
|
|
|
|
// Transfer audio data to worker
|
|
worker.postMessage({
|
|
audioData
|
|
}, [audioData]);
|
|
});
|
|
}
|
|
|
|
export async function encodeToMp3(audioBlob, onProgress = null, signal = null) {
|
|
try {
|
|
// Use Web Worker for non-blocking FFmpeg encoding
|
|
if (typeof Worker !== 'undefined') {
|
|
return await encodeToMp3Worker(audioBlob, onProgress, signal);
|
|
}
|
|
|
|
throw new MP3EncodingError('Web Workers are required for MP3 encoding');
|
|
} catch (error) {
|
|
console.error('MP3 encoding failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export { MP3EncodingError };
|