diff --git a/src/codec.ts b/src/codec.ts index 2e90cfd..a396e70 100644 --- a/src/codec.ts +++ b/src/codec.ts @@ -621,6 +621,54 @@ export const parseAacAudioSpecificConfig = (bytes: Uint8Array | null): AacAudioS }; }; +export const buildAacAudioSpecificConfig = (config: { + objectType: number; + sampleRate: number; + numberOfChannels: number; +}) => { + let frequencyIndex = aacFrequencyTable.indexOf(config.sampleRate); + let customSampleRate: number | null = null; + + if (frequencyIndex === -1) { + frequencyIndex = 15; + customSampleRate = config.sampleRate; + } + + const channelConfiguration = aacChannelMap.indexOf(config.numberOfChannels); + if (channelConfiguration === -1) { + throw new TypeError(`Unsupported number of channels: ${config.numberOfChannels}`); + } + + let bitCount = 5 + 4 + 4; + if (config.objectType >= 32) { + bitCount += 6; + } + if (frequencyIndex === 15) { + bitCount += 24; + } + + const byteCount = Math.ceil(bitCount / 8); + const bytes = new Uint8Array(byteCount); + const bitstream = new Bitstream(bytes); + + if (config.objectType < 32) { + bitstream.writeBits(5, config.objectType); + } else { + bitstream.writeBits(5, 31); + bitstream.writeBits(6, config.objectType - 32); + } + + bitstream.writeBits(4, frequencyIndex); + + if (frequencyIndex === 15) { + bitstream.writeBits(24, customSampleRate!); + } + + bitstream.writeBits(4, channelConfiguration); + + return bytes; +}; + export const OPUS_SAMPLE_RATE = 48_000; const PCM_CODEC_REGEX = /^pcm-([usf])(\d+)+(be)?$/; diff --git a/src/media-source.ts b/src/media-source.ts index c89b24a..86989a2 100644 --- a/src/media-source.ts +++ b/src/media-source.ts @@ -9,6 +9,8 @@ import { AUDIO_CODECS, AudioCodec, + buildAacAudioSpecificConfig, + parseAacAudioSpecificConfig, parsePcmCodec, PCM_AUDIO_CODECS, PcmAudioCodec, @@ -24,9 +26,11 @@ import { CallSerializer, clamp, isFirefox, + last, promiseWithResolvers, setInt24, setUint24, + toUint8Array, } from './misc'; import { Muxer } from './muxer'; import { SubtitleParser } from './subtitles'; @@ -1515,6 +1519,32 @@ class AudioEncoderWrapper { this.encoder = new AudioEncoder({ output: (chunk, meta) => { + // WebKit emits an invalid description for AAC (https://bugs.webkit.org/show_bug.cgi?id=302253), + // which we try to detect here. If detected, we'll provide our own description instead, derived + // from the codec string and audio parameters. + if (this.encodingConfig.codec === 'aac' && meta?.decoderConfig) { + let needsDescriptionOverwrite = false; + if (!meta.decoderConfig.description || meta.decoderConfig.description.byteLength < 2) { + needsDescriptionOverwrite = true; + } else { + const audioSpecificConfig = parseAacAudioSpecificConfig( + toUint8Array(meta.decoderConfig.description), + ); + + needsDescriptionOverwrite = audioSpecificConfig.objectType === 0; + } + + if (needsDescriptionOverwrite) { + const objectType = Number(last(encoderConfig.codec.split('.'))); + + meta.decoderConfig.description = buildAacAudioSpecificConfig({ + objectType, + numberOfChannels: meta.decoderConfig.numberOfChannels, + sampleRate: meta.decoderConfig.sampleRate, + }); + } + } + const packet = EncodedPacket.fromEncodedChunk(chunk); this.encodingConfig.onEncodedPacket?.(packet, meta);