| ; | |
| /*! | |
| * 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.registerAacEncoder = void 0; | |
| const aac_misc_1 = require("../../../shared/aac-misc"); | |
| const mediabunny_1 = require("mediabunny"); | |
| // @ts-expect-error An esbuild plugin handles this, TypeScript doesn't need to understand | |
| const encode_worker_1 = __importDefault(require("./encode.worker")); | |
| const AAC_SAMPLE_RATES = [ | |
| 96000, 88200, 64000, 48000, 44100, 32000, | |
| 24000, 22050, 16000, 12000, 11025, 8000, 7350, | |
| ]; | |
| class AacEncoder extends mediabunny_1.CustomAudioEncoder { | |
| constructor() { | |
| super(...arguments); | |
| this.worker = null; | |
| this.workerError = null; | |
| this.nextMessageId = 0; | |
| this.pendingMessages = new Map(); | |
| this.ctx = 0; | |
| this.encoderFrameSize = 0; | |
| this.sampleRate = 0; | |
| this.numberOfChannels = 0; | |
| this.chunkMetadata = {}; | |
| this.useAdts = false; | |
| this.adtsHeaderTemplate = null; | |
| this.description = null; | |
| // Accumulate interleaved f32 samples until we have a full frame | |
| this.pendingBuffer = new Float32Array(2 ** 16); | |
| this.pendingFrames = 0; | |
| this.nextSampleTimestampInSamples = null; | |
| } | |
| static supports(codec, config) { | |
| return codec === 'aac' | |
| && config.numberOfChannels >= 1 | |
| && config.numberOfChannels <= 8 | |
| && AAC_SAMPLE_RATES.includes(config.sampleRate) | |
| && config.bitrate !== undefined; | |
| } | |
| async init() { | |
| // eslint-disable-next-line @typescript-eslint/no-unsafe-call | |
| this.worker = (await (0, encode_worker_1.default)()); | |
| 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 || 'AAC encoder worker failed to load or crashed.')); | |
| }); | |
| } | |
| else { | |
| const nodeWorker = this.worker; | |
| nodeWorker.on('message', onMessage); | |
| nodeWorker.on('error', onError); | |
| } | |
| assert(this.config.bitrate !== undefined); | |
| this.sampleRate = this.config.sampleRate; | |
| this.numberOfChannels = this.config.numberOfChannels; | |
| const result = await this.sendCommand({ | |
| type: 'init', | |
| data: { | |
| numberOfChannels: this.config.numberOfChannels, | |
| sampleRate: this.config.sampleRate, | |
| bitrate: this.config.bitrate, | |
| }, | |
| }); | |
| this.ctx = result.ctx; | |
| this.encoderFrameSize = result.frameSize; | |
| // The ffmpeg encoder provides an AudioSpecificConfig as extradata after init | |
| const description = new Uint8Array(result.extradata); | |
| const aacConfig = this.config.aac; | |
| this.useAdts = aacConfig?.format === 'adts'; | |
| if (this.useAdts) { | |
| const audioSpecificConfig = (0, aac_misc_1.parseAacAudioSpecificConfig)(description); | |
| this.adtsHeaderTemplate = (0, aac_misc_1.buildAdtsHeaderTemplate)(audioSpecificConfig); | |
| } | |
| this.description = this.useAdts ? null : description; | |
| this.resetInternalState(); | |
| } | |
| resetInternalState() { | |
| this.pendingFrames = 0; | |
| this.nextSampleTimestampInSamples = null; | |
| this.chunkMetadata = { | |
| decoderConfig: { | |
| codec: 'mp4a.40.2', | |
| numberOfChannels: this.config.numberOfChannels, | |
| sampleRate: this.config.sampleRate, | |
| ...(this.description ? { description: this.description } : {}), | |
| }, | |
| }; | |
| } | |
| async encode(audioSample) { | |
| if (this.nextSampleTimestampInSamples === null) { | |
| this.nextSampleTimestampInSamples = Math.round(audioSample.timestamp * this.sampleRate); | |
| } | |
| const channels = this.numberOfChannels; | |
| const incomingFrames = audioSample.numberOfFrames; | |
| // Extract interleaved f32 data | |
| const totalBytes = audioSample.allocationSize({ format: 'f32', planeIndex: 0 }); | |
| const audioBytes = new Uint8Array(totalBytes); | |
| audioSample.copyTo(audioBytes, { format: 'f32', planeIndex: 0 }); | |
| const incomingData = new Float32Array(audioBytes.buffer); | |
| const requiredSamples = (this.pendingFrames + incomingFrames) * channels; | |
| if (requiredSamples > this.pendingBuffer.length) { | |
| let newSize = this.pendingBuffer.length; | |
| while (newSize < requiredSamples) { | |
| newSize *= 2; | |
| } | |
| const newBuffer = new Float32Array(newSize); | |
| newBuffer.set(this.pendingBuffer.subarray(0, this.pendingFrames * channels)); | |
| this.pendingBuffer = newBuffer; | |
| } | |
| this.pendingBuffer.set(incomingData, this.pendingFrames * channels); | |
| this.pendingFrames += incomingFrames; | |
| while (this.pendingFrames >= this.encoderFrameSize) { | |
| await this.encodeOneFrame(); | |
| } | |
| } | |
| async flush() { | |
| // Pad remaining samples with silence to fill a full frame | |
| if (this.pendingFrames > 0) { | |
| const channels = this.numberOfChannels; | |
| const frameSize = this.encoderFrameSize; | |
| const usedSamples = this.pendingFrames * channels; | |
| const frameSamples = frameSize * channels; | |
| this.pendingBuffer.fill(0, usedSamples, frameSamples); | |
| this.pendingFrames = frameSize; | |
| await this.encodeOneFrame(); | |
| } | |
| const result = await this.sendCommand({ type: 'flush', data: { ctx: this.ctx } }); | |
| this.emitPackets(result.packets); | |
| this.resetInternalState(); | |
| } | |
| close() { | |
| this.worker?.terminate(); | |
| } | |
| async encodeOneFrame() { | |
| assert(this.nextSampleTimestampInSamples !== null); | |
| const channels = this.numberOfChannels; | |
| const frameSize = this.encoderFrameSize; | |
| const frameSamples = frameSize * channels; | |
| const frameData = this.pendingBuffer.slice(0, frameSamples); | |
| // Shift remaining using copyWithin | |
| this.pendingFrames -= frameSize; | |
| if (this.pendingFrames > 0) { | |
| this.pendingBuffer.copyWithin(0, frameSamples, frameSamples + this.pendingFrames * channels); | |
| } | |
| const audioData = frameData.buffer; | |
| const result = await this.sendCommand({ | |
| type: 'encode', | |
| data: { | |
| ctx: this.ctx, | |
| audioData, | |
| timestamp: this.nextSampleTimestampInSamples, | |
| }, | |
| }, [audioData]); | |
| this.nextSampleTimestampInSamples += frameSize; | |
| this.emitPackets(result.packets); | |
| } | |
| emitPackets(packets) { | |
| for (const p of packets) { | |
| let data = new Uint8Array(p.encodedData); | |
| if (this.useAdts) { | |
| assert(this.adtsHeaderTemplate !== null); | |
| const { header, bitstream } = this.adtsHeaderTemplate; | |
| const frameLength = header.byteLength + data.byteLength; | |
| (0, aac_misc_1.writeAdtsFrameLength)(bitstream, frameLength); | |
| const adtsFrame = new Uint8Array(frameLength); | |
| adtsFrame.set(header, 0); | |
| adtsFrame.set(data, header.byteLength); | |
| data = adtsFrame; | |
| } | |
| const packet = new mediabunny_1.EncodedPacket(data, 'key', p.pts / this.sampleRate, p.duration / this.sampleRate); | |
| this.onPacket(packet, this.chunkMetadata); | |
| this.chunkMetadata = {}; | |
| } | |
| } | |
| 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 AAC 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 AAC encoder: | |
| * | |
| * ```ts | |
| * import { canEncodeAudio } from 'mediabunny'; | |
| * import { registerAacEncoder } from '@mediabunny/aac-encoder'; | |
| * | |
| * if (!(await canEncodeAudio('aac'))) { | |
| * registerAacEncoder(); | |
| * } | |
| * ``` | |
| * | |
| * @group \@mediabunny/aac-encoder | |
| * @public | |
| */ | |
| const registerAacEncoder = () => { | |
| if (registered) { | |
| return; | |
| } | |
| registered = true; | |
| (0, mediabunny_1.registerEncoder)(AacEncoder); | |
| }; | |
| exports.registerAacEncoder = registerAacEncoder; | |
| function assert(x) { | |
| if (!x) { | |
| throw new Error('Assertion failed.'); | |
| } | |
| } | |
Xet Storage Details
- Size:
- 10.3 kB
- Xet hash:
- 2609acf30be7e3f0d057f0f46c12100645b0498463b0b8558a9038e6eea8c511
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.