kv-music/js/mp3-encoder.js
gpulch cde7080052 fix: address code review feedback
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
2026-02-23 10:49:05 +01:00

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