src/demux/mpegaudio.ts
/**
* MPEG parser helper
*/
import {
DemuxedAudioTrack
} from '../types/demuxer';
let chromeVersion: number | null = null;
const BitratesMap = [
32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448,
32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384,
32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256,
8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160
];
const SamplingRateMap = [44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000];
const SamplesCoefficients = [
// MPEG 2.5
[
0, // Reserved
72, // Layer3
144, // Layer2
12 // Layer1
],
// Reserved
[
0, // Reserved
0, // Layer3
0, // Layer2
0 // Layer1
],
// MPEG 2
[
0, // Reserved
72, // Layer3
144, // Layer2
12 // Layer1
],
// MPEG 1
[
0, // Reserved
144, // Layer3
144, // Layer2
12 // Layer1
]
];
const BytesInSlot = [
0, // Reserved
1, // Layer3
1, // Layer2
4 // Layer1
];
export function appendFrame (track: DemuxedAudioTrack, data: Uint8Array, offset: number, pts: number, frameIndex: number) {
// Using http://www.datavoyage.com/mpgscript/mpeghdr.htm as a reference
if (offset + 24 > data.length) {
return;
}
const header = parseHeader(data, offset);
if (header && offset + header.frameLength <= data.length) {
const frameDuration = header.samplesPerFrame * 90000 / header.sampleRate;
const stamp = pts + frameIndex * frameDuration;
const sample = { unit: data.subarray(offset, offset + header.frameLength), pts: stamp, dts: stamp };
track.config = [];
track.channelCount = header.channelCount;
track.samplerate = header.sampleRate;
track.samples.push(sample);
return { sample, length: header.frameLength };
}
}
export function parseHeader (data: Uint8Array, offset: number) {
const mpegVersion = (data[offset + 1] >> 3) & 3;
const mpegLayer = (data[offset + 1] >> 1) & 3;
const bitRateIndex = (data[offset + 2] >> 4) & 15;
const sampleRateIndex = (data[offset + 2] >> 2) & 3;
if (mpegVersion !== 1 && bitRateIndex !== 0 && bitRateIndex !== 15 && sampleRateIndex !== 3) {
const paddingBit = (data[offset + 2] >> 1) & 1;
const channelMode = data[offset + 3] >> 6;
const columnInBitrates = mpegVersion === 3 ? (3 - mpegLayer) : (mpegLayer === 3 ? 3 : 4);
const bitRate = BitratesMap[columnInBitrates * 14 + bitRateIndex - 1] * 1000;
const columnInSampleRates = mpegVersion === 3 ? 0 : mpegVersion === 2 ? 1 : 2;
const sampleRate = SamplingRateMap[columnInSampleRates * 3 + sampleRateIndex];
const channelCount = channelMode === 3 ? 1 : 2; // If bits of channel mode are `11` then it is a single channel (Mono)
const sampleCoefficient = SamplesCoefficients[mpegVersion][mpegLayer];
const bytesInSlot = BytesInSlot[mpegLayer];
const samplesPerFrame = sampleCoefficient * 8 * bytesInSlot;
const frameLength = Math.floor(sampleCoefficient * bitRate / sampleRate + paddingBit) * bytesInSlot;
if (chromeVersion === null) {
const userAgent = navigator.userAgent || '';
const result = userAgent.match(/Chrome\/(\d+)/i);
chromeVersion = result ? parseInt(result[1]) : 0;
}
const needChromeFix = !!chromeVersion && chromeVersion <= 87;
if (needChromeFix && mpegLayer === 2 && bitRate >= 224000 && channelMode === 0) {
// Work around bug in Chromium by setting channelMode to dual-channel (01) instead of stereo (00)
data[offset + 3] = data[offset + 3] | 0x80;
}
return { sampleRate, channelCount, frameLength, samplesPerFrame };
}
}
export function isHeaderPattern (data: Uint8Array, offset: number): boolean {
return data[offset] === 0xff && (data[offset + 1] & 0xe0) === 0xe0 && (data[offset + 1] & 0x06) !== 0x00;
}
export function isHeader (data: Uint8Array, offset: number): boolean {
// Look for MPEG header | 1111 1111 | 111X XYZX | where X can be either 0 or 1 and Y or Z should be 1
// Layer bits (position 14 and 15) in header should be always different from 0 (Layer I or Layer II or Layer III)
// More info http://www.mp3-tech.org/programmer/frame_header.html
return offset + 1 < data.length && isHeaderPattern(data, offset);
}
export function canParse (data: Uint8Array, offset: number): boolean {
const headerSize = 4;
return isHeaderPattern(data, offset) && data.length - offset >= headerSize;
}
export function probe (data: Uint8Array, offset: number): boolean {
// same as isHeader but we also check that MPEG frame follows last MPEG frame
// or end of data is reached
if (offset + 1 < data.length && isHeaderPattern(data, offset)) {
// MPEG header Length
const headerLength = 4;
// MPEG frame Length
const header = parseHeader(data, offset);
let frameLength = headerLength;
if (header?.frameLength) {
frameLength = header.frameLength;
}
const newOffset = offset + frameLength;
return newOffset === data.length || isHeader(data, newOffset);
}
return false;
}