| ; | |
| /*! | |
| * Copyright (c) 2026-present, Vanilagy and contributors | |
| * | |
| * This Source Code Form is subject to the terms of the Mozilla Public | |
| * License, v. 2.0. If a copy of the MPL was not distributed with this | |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. | |
| */ | |
| var __importDefault = (this && this.__importDefault) || function (mod) { | |
| return (mod && mod.__esModule) ? mod : { "default": mod }; | |
| }; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| exports.registerMp3Encoder = void 0; | |
| const mediabunny_1 = require("mediabunny"); | |
| const mp3_misc_1 = require("../../../shared/mp3-misc"); | |
| // @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand | |
| const encode_worker_1 = __importDefault(require("./encode.worker")); | |
| const MP3_ENCODER_LOADED_SYMBOL = Symbol.for('@mediabunny/mp3-encoder loaded'); | |
| if (globalThis[MP3_ENCODER_LOADED_SYMBOL]) { | |
| mediabunny_1.Logging._error('[WARNING]\n@mediabunny/mp3-encoder was loaded twice.' | |
| + ' This will likely cause the encoder not to work correctly.' | |
| + ' Check if multiple dependencies are importing different versions of @mediabunny/mp3-encoder,' | |
| + ' or if something is being bundled incorrectly.'); | |
| } | |
| globalThis[MP3_ENCODER_LOADED_SYMBOL] = true; | |
| class Mp3Encoder extends mediabunny_1.CustomAudioEncoder { | |
| constructor() { | |
| super(...arguments); | |
| this.worker = null; | |
| this.workerError = null; | |
| this.nextMessageId = 0; | |
| this.pendingMessages = new Map(); | |
| this.buffer = new Uint8Array(2 ** 16); | |
| this.currentBufferOffset = 0; | |
| this.currentTimestamp = null; | |
| this.chunkMetadata = {}; | |
| } | |
| static supports(codec, config) { | |
| return codec === 'mp3' | |
| && (config.numberOfChannels === 1 || config.numberOfChannels === 2) | |
| && Object.values(mp3_misc_1.SAMPLING_RATES).some(x => x === config.sampleRate || (x / 2) === config.sampleRate || (x / 4) === config.sampleRate); | |
| } | |
| async init() { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call | |
| this.worker = (await (0, encode_worker_1.default)()); // The actual encoding takes place in this worker | |
| const onMessage = (data) => { | |
| const pending = this.pendingMessages.get(data.id); | |
| assert(pending !== undefined); | |
| this.pendingMessages.delete(data.id); | |
| if (data.success) { | |
| pending.resolve(data.data); | |
| } | |
| else { | |
| pending.reject(data.error); | |
| } | |
| }; | |
| // Treats any worker error (like CSP errors) as fatal, rejecting pending and future commands | |
| const onError = (error) => { | |
| this.workerError = error; | |
| for (const pending of this.pendingMessages.values()) { | |
| pending.reject(error); | |
| } | |
| this.pendingMessages.clear(); | |
| this.worker?.terminate(); | |
| this.worker = null; | |
| }; | |
| if (this.worker.addEventListener) { | |
| this.worker.addEventListener('message', event => onMessage(event.data)); | |
| this.worker.addEventListener('error', (event) => { | |
| onError(new Error(event.message || 'MP3 encoder worker failed to load or crashed.')); | |
| }); | |
| } | |
| else { | |
| const nodeWorker = this.worker; | |
| nodeWorker.on('message', onMessage); | |
| nodeWorker.on('error', onError); | |
| } | |
| assert(this.config.bitrate); | |
| await this.sendCommand({ | |
| type: 'init', | |
| data: { | |
| numberOfChannels: this.config.numberOfChannels, | |
| sampleRate: this.config.sampleRate, | |
| bitrate: this.config.bitrate, | |
| }, | |
| }); | |
| this.resetInternalState(); | |
| } | |
| resetInternalState() { | |
| this.currentBufferOffset = 0; | |
| this.currentTimestamp = null; | |
| this.chunkMetadata = { | |
| decoderConfig: { | |
| codec: 'mp3', | |
| numberOfChannels: this.config.numberOfChannels, | |
| sampleRate: this.config.sampleRate, | |
| }, | |
| }; | |
| } | |
| async encode(audioSample) { | |
| if (this.currentTimestamp === null) { | |
| // LAME adds 576 samples of encoder delay on top of inherent delay | |
| this.currentTimestamp = audioSample.timestamp - (576 + mp3_misc_1.MP3_DECODER_DELAY) / this.config.sampleRate; | |
| } | |
| const sizePerChannel = audioSample.allocationSize({ | |
| format: 's16-planar', | |
| planeIndex: 0, | |
| }); | |
| const requiredBytes = audioSample.numberOfChannels * sizePerChannel; | |
| const audioData = new ArrayBuffer(requiredBytes); | |
| const audioBytes = new Uint8Array(audioData); | |
| for (let i = 0; i < audioSample.numberOfChannels; i++) { | |
| audioSample.copyTo(audioBytes.subarray(i * sizePerChannel), { | |
| format: 's16-planar', // LAME wants it in this format | |
| planeIndex: i, | |
| }); | |
| } | |
| const result = await this.sendCommand({ | |
| type: 'encode', | |
| data: { | |
| audioData, | |
| numberOfFrames: audioSample.numberOfFrames, | |
| }, | |
| }, [audioData]); | |
| this.digestOutput(new Uint8Array(result.encodedData)); | |
| } | |
| async flush() { | |
| const result = await this.sendCommand({ type: 'flush' }); | |
| this.digestOutput(new Uint8Array(result.flushedData)); | |
| this.resetInternalState(); | |
| } | |
| close() { | |
| this.worker?.terminate(); | |
| } | |
| /** | |
| * LAME returns data in chunks, but a chunk doesn't need to contain a full MP3 frame. Therefore, we must accumulate | |
| * these chunks and extract the MP3 frames only when they're complete. | |
| */ | |
| digestOutput(bytes) { | |
| assert(this.currentTimestamp !== null); | |
| const requiredBufferSize = this.currentBufferOffset + bytes.length; | |
| if (requiredBufferSize > this.buffer.length) { | |
| // Grow the buffer to the required size | |
| const newSize = 1 << Math.ceil(Math.log2(requiredBufferSize)); | |
| const newBuffer = new Uint8Array(newSize); | |
| newBuffer.set(this.buffer); | |
| this.buffer = newBuffer; | |
| } | |
| this.buffer.set(bytes, this.currentBufferOffset); | |
| this.currentBufferOffset = requiredBufferSize; | |
| let pos = 0; | |
| while (pos <= this.currentBufferOffset - mp3_misc_1.MP3_FRAME_HEADER_SIZE) { | |
| const word = new DataView(this.buffer.buffer).getUint32(pos, false); | |
| const header = (0, mp3_misc_1.readMp3FrameHeader)(word, null).header; | |
| if (!header) { | |
| break; | |
| } | |
| const fits = header.totalSize <= this.currentBufferOffset - pos; | |
| if (!fits) { | |
| // The frame isn't complete yet | |
| break; | |
| } | |
| const data = this.buffer.slice(pos, pos + header.totalSize); | |
| const duration = header.audioSamplesInFrame / header.sampleRate; | |
| this.onPacket(new mediabunny_1.EncodedPacket(data, 'key', this.currentTimestamp, duration), this.chunkMetadata); | |
| this.chunkMetadata = {}; // Mimic WebCodecs-like behavior | |
| this.currentTimestamp += duration; | |
| pos += header.totalSize; | |
| } | |
| if (pos > 0) { | |
| // Shift the data | |
| this.buffer.set(this.buffer.subarray(pos, this.currentBufferOffset), 0); | |
| this.currentBufferOffset -= pos; | |
| } | |
| } | |
| sendCommand(command, transferables) { | |
| return new Promise((resolve, reject) => { | |
| if (this.workerError) { | |
| reject(this.workerError); | |
| return; | |
| } | |
| const id = this.nextMessageId++; | |
| this.pendingMessages.set(id, { | |
| resolve: resolve, | |
| reject, | |
| }); | |
| assert(this.worker); | |
| if (transferables) { | |
| this.worker.postMessage({ id, command }, transferables); | |
| } | |
| else { | |
| this.worker.postMessage({ id, command }); | |
| } | |
| }); | |
| } | |
| } | |
| let registered = false; | |
| /** | |
| * Registers the LAME MP3 encoder, which Mediabunny will then use automatically when applicable. Make sure to call this | |
| * function before starting any encoding task. | |
| * | |
| * Preferably, wrap the call in a condition to avoid overriding any native MP3 encoder: | |
| * | |
| * ```ts | |
| * import { canEncodeAudio } from 'mediabunny'; | |
| * import { registerMp3Encoder } from '@mediabunny/mp3-encoder'; | |
| * | |
| * if (!(await canEncodeAudio('mp3'))) { | |
| * registerMp3Encoder(); | |
| * } | |
| * ``` | |
| * | |
| * @group \@mediabunny/mp3-encoder | |
| * @public | |
| */ | |
| const registerMp3Encoder = () => { | |
| if (registered) { | |
| return; | |
| } | |
| registered = true; | |
| (0, mediabunny_1.registerEncoder)(Mp3Encoder); | |
| }; | |
| exports.registerMp3Encoder = registerMp3Encoder; | |
| function assert(x) { | |
| if (!x) { | |
| throw new Error('Assertion failed.'); | |
| } | |
| } | |
Xet Storage Details
- Size:
- 9.01 kB
- Xet hash:
- 717326796a38458b3bc2ff17b8f5f2c4d2efce85adcbb2e5116859df03f4b7f3
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.