hls.js
Version:
JavaScript HLS client using MediaSourceExtension
250 lines (232 loc) • 7.67 kB
text/typescript
/**
* ADTS parser helper
* @link https://wiki.multimedia.cx/index.php?title=ADTS
*/
import { ErrorDetails, ErrorTypes } from '../../errors';
import { Events } from '../../events';
import { logger } from '../../utils/logger';
import type { HlsEventEmitter } from '../../events';
import type {
AudioFrame,
AudioSample,
DemuxedAudioTrack,
} from '../../types/demuxer';
type AudioConfig = {
config: [number, number];
samplerate: number;
channelCount: number;
codec: string;
parsedCodec: string;
manifestCodec: string | undefined;
};
type FrameHeader = {
headerLength: number;
frameLength: number;
};
export function getAudioConfig(
observer: HlsEventEmitter,
data: Uint8Array,
offset: number,
manifestCodec: string | undefined,
): AudioConfig | void {
const adtsSamplingRates = [
96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025,
8000, 7350,
];
const byte2 = data[offset + 2];
const adtsSamplingIndex = (byte2 >> 2) & 0xf;
if (adtsSamplingIndex > 12) {
const error = new Error(`invalid ADTS sampling index:${adtsSamplingIndex}`);
observer.emit(Events.ERROR, Events.ERROR, {
type: ErrorTypes.MEDIA_ERROR,
details: ErrorDetails.FRAG_PARSING_ERROR,
fatal: true,
error,
reason: error.message,
});
return;
}
// MPEG-4 Audio Object Type (profile_ObjectType+1)
const adtsObjectType = ((byte2 >> 6) & 0x3) + 1;
const channelCount = ((data[offset + 3] >> 6) & 0x3) | ((byte2 & 1) << 2);
const codec = 'mp4a.40.' + adtsObjectType;
/* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config
ISO/IEC 14496-3 - Table 1.13 — Syntax of AudioSpecificConfig()
Audio Profile / Audio Object Type
0: Null
1: AAC Main
2: AAC LC (Low Complexity)
3: AAC SSR (Scalable Sample Rate)
4: AAC LTP (Long Term Prediction)
5: SBR (Spectral Band Replication)
6: AAC Scalable
sampling freq
0: 96000 Hz
1: 88200 Hz
2: 64000 Hz
3: 48000 Hz
4: 44100 Hz
5: 32000 Hz
6: 24000 Hz
7: 22050 Hz
8: 16000 Hz
9: 12000 Hz
10: 11025 Hz
11: 8000 Hz
12: 7350 Hz
13: Reserved
14: Reserved
15: frequency is written explictly
Channel Configurations
These are the channel configurations:
0: Defined in AOT Specifc Config
1: 1 channel: front-center
2: 2 channels: front-left, front-right
*/
// audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1
const samplerate = adtsSamplingRates[adtsSamplingIndex];
let aacSampleIndex = adtsSamplingIndex;
if (adtsObjectType === 5 || adtsObjectType === 29) {
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
// there is a factor 2 between frame sample rate and output sample rate
// multiply frequency by 2 (see table above, equivalent to substract 3)
aacSampleIndex -= 3;
}
const config: [number, number] = [
(adtsObjectType << 3) | ((aacSampleIndex & 0x0e) >> 1),
((aacSampleIndex & 0x01) << 7) | (channelCount << 3),
];
logger.log(
`manifest codec:${manifestCodec}, parsed codec:${codec}, channels:${channelCount}, rate:${samplerate} (ADTS object type:${adtsObjectType} sampling index:${adtsSamplingIndex})`,
);
return {
config,
samplerate,
channelCount,
codec,
parsedCodec: codec,
manifestCodec,
};
}
export function isHeaderPattern(data: Uint8Array, offset: number): boolean {
return data[offset] === 0xff && (data[offset + 1] & 0xf6) === 0xf0;
}
export function getHeaderLength(data: Uint8Array, offset: number): number {
return data[offset + 1] & 0x01 ? 7 : 9;
}
export function getFullFrameLength(data: Uint8Array, offset: number): number {
return (
((data[offset + 3] & 0x03) << 11) |
(data[offset + 4] << 3) |
((data[offset + 5] & 0xe0) >>> 5)
);
}
export function canGetFrameLength(data: Uint8Array, offset: number): boolean {
return offset + 5 < data.length;
}
export function isHeader(data: Uint8Array, offset: number): boolean {
// Look for ADTS header | 1111 1111 | 1111 X00X | where X can be either 0 or 1
// Layer bits (position 14 and 15) in header should be always 0 for ADTS
// More info https://wiki.multimedia.cx/index.php?title=ADTS
return offset + 1 < data.length && isHeaderPattern(data, offset);
}
export function canParse(data: Uint8Array, offset: number): boolean {
return (
canGetFrameLength(data, offset) &&
isHeaderPattern(data, offset) &&
getFullFrameLength(data, offset) <= data.length - offset
);
}
export function probe(data: Uint8Array, offset: number): boolean {
// same as isHeader but we also check that ADTS frame follows last ADTS frame
// or end of data is reached
if (isHeader(data, offset)) {
// ADTS header Length
const headerLength = getHeaderLength(data, offset);
if (offset + headerLength >= data.length) {
return false;
}
// ADTS frame Length
const frameLength = getFullFrameLength(data, offset);
if (frameLength <= headerLength) {
return false;
}
const newOffset = offset + frameLength;
return newOffset === data.length || isHeader(data, newOffset);
}
return false;
}
export function initTrackConfig(
track: DemuxedAudioTrack,
observer: HlsEventEmitter,
data: Uint8Array,
offset: number,
audioCodec: string | undefined,
) {
if (!track.samplerate) {
const config = getAudioConfig(observer, data, offset, audioCodec);
if (!config) {
return;
}
Object.assign(track, config);
}
}
export function getFrameDuration(samplerate: number): number {
return (1024 * 90000) / samplerate;
}
export function parseFrameHeader(
data: Uint8Array,
offset: number,
): FrameHeader | void {
// The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header
const headerLength = getHeaderLength(data, offset);
if (offset + headerLength <= data.length) {
// retrieve frame size
const frameLength = getFullFrameLength(data, offset) - headerLength;
if (frameLength > 0) {
// logger.log(`AAC frame, offset/length/total/pts:${offset+headerLength}/${frameLength}/${data.byteLength}`);
return { headerLength, frameLength };
}
}
}
export function appendFrame(
track: DemuxedAudioTrack,
data: Uint8Array,
offset: number,
pts: number,
frameIndex: number,
): AudioFrame {
const frameDuration = getFrameDuration(track.samplerate as number);
const stamp = pts + frameIndex * frameDuration;
const header = parseFrameHeader(data, offset);
let unit: Uint8Array;
if (header) {
const { frameLength, headerLength } = header;
const length = headerLength + frameLength;
const missing = Math.max(0, offset + length - data.length);
// logger.log(`AAC frame ${frameIndex}, pts:${stamp} length@offset/total: ${frameLength}@${offset+headerLength}/${data.byteLength} missing: ${missing}`);
if (missing) {
unit = new Uint8Array(length - headerLength);
unit.set(data.subarray(offset + headerLength, data.length), 0);
} else {
unit = data.subarray(offset + headerLength, offset + length);
}
const sample: AudioSample = {
unit,
pts: stamp,
};
if (!missing) {
track.samples.push(sample as AudioSample);
}
return { sample, length, missing };
}
// overflow incomplete header
const length = data.length - offset;
unit = new Uint8Array(length);
unit.set(data.subarray(offset, data.length), 0);
const sample: AudioSample = {
unit,
pts: stamp,
};
return { sample, length, missing: -1 };
}