diff --git a/README.md b/README.md index 9ca8549d206..7d07cef58c1 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ HLS.js is written in [ECMAScript6] (`*.js`) and [TypeScript] (`*.ts`) (strongly - ITU-T Rec. H.264 and ISO/IEC 14496-10 Elementary Stream - ISO/IEC 13818-7 ADTS AAC Elementary Stream - ISO/IEC 11172-3 / ISO/IEC 13818-3 (MPEG-1/2 Audio Layer III) Elementary Stream + - ATSC A/52 / AC-3 / Dolby Digital Elementary Stream - Packetized metadata (ID3v2.3.0) Elementary Stream - AAC container (audio only streams) - MPEG Audio container (MPEG-1/2 Audio Layer III audio only streams) diff --git a/src/demux/transmuxer-interface.ts b/src/demux/transmuxer-interface.ts index a6af4619893..413c0e253ce 100644 --- a/src/demux/transmuxer-interface.ts +++ b/src/demux/transmuxer-interface.ts @@ -60,6 +60,7 @@ export default class TransmuxerInterface { mp4: MediaSource.isTypeSupported('video/mp4'), mpeg: MediaSource.isTypeSupported('audio/mpeg'), mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'), + ac3: MediaSource.isTypeSupported('audio/mp4; codecs="ac-3"'), }; // navigator.vendor is not always available in Web Worker // refer to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/navigator diff --git a/src/demux/tsdemuxer.ts b/src/demux/tsdemuxer.ts index e77590e8254..7ca080b07f4 100644 --- a/src/demux/tsdemuxer.ts +++ b/src/demux/tsdemuxer.ts @@ -54,6 +54,7 @@ export interface TypeSupported { mpeg: boolean; mp3: boolean; mp4: boolean; + ac3: boolean; } const PACKET_LENGTH = 188; @@ -284,6 +285,9 @@ class TSDemuxer implements Demuxer { case 'mp3': this.parseMPEGPES(audioTrack, pes); break; + case 'ac3': + this.parseAC3PES(pes); + break; } } audioData = { data: [], size: 0 }; @@ -443,6 +447,9 @@ class TSDemuxer implements Demuxer { case 'mp3': this.parseMPEGPES(audioTrack, pes); break; + case 'ac3': + this.parseAC3PES(pes); + break; } audioTrack.pesData = null; } else { @@ -958,6 +965,115 @@ class TSDemuxer implements Demuxer { } } + private parseAC3PES(pes) { + const data = pes.data; + const pts = pes.pts; + const length = data.length; + let frameIndex = 0; + let offset = 0; + let parsed; + + while ( + offset < length && + (parsed = this.parseAC3(data, offset, length, frameIndex++, pts)) > 0 + ) { + offset += parsed; + } + } + + private onAC3Frame(data, sampleRate, channelCount, config, frameIndex, pts) { + const frameDuration = (1536 / sampleRate) * 1000; + const stamp = pts + frameIndex * frameDuration; + const audioTrack = this._audioTrack as DemuxedAudioTrack; + + audioTrack.config = config; + audioTrack.channelCount = channelCount; + audioTrack.samplerate = sampleRate; + audioTrack.duration = this._duration; + audioTrack.samples.push({ unit: data, pts: stamp }); + } + + private parseAC3(data, start, end, frameIndex, pts) { + if (start + 8 > end) { + return -1; // not enough bytes left + } + + if (data[start] !== 0x0b || data[start + 1] !== 0x77) { + return -1; // invalid magic + } + + // get sample rate + const samplingRateCode = data[start + 4] >> 6; + if (samplingRateCode >= 3) { + return -1; // invalid sampling rate + } + + const samplingRateMap = [48000, 44100, 32000]; + const sampleRate = samplingRateMap[samplingRateCode]; + + // get frame size + const frameSizeCode = data[start + 4] & 0x3f; + const frameSizeMap = [ + 64, 69, 96, 64, 70, 96, 80, 87, 120, 80, 88, 120, 96, 104, 144, 96, 105, + 144, 112, 121, 168, 112, 122, 168, 128, 139, 192, 128, 140, 192, 160, 174, + 240, 160, 175, 240, 192, 208, 288, 192, 209, 288, 224, 243, 336, 224, 244, + 336, 256, 278, 384, 256, 279, 384, 320, 348, 480, 320, 349, 480, 384, 417, + 576, 384, 418, 576, 448, 487, 672, 448, 488, 672, 512, 557, 768, 512, 558, + 768, 640, 696, 960, 640, 697, 960, 768, 835, 1152, 768, 836, 1152, 896, + 975, 1344, 896, 976, 1344, 1024, 1114, 1536, 1024, 1115, 1536, 1152, 1253, + 1728, 1152, 1254, 1728, 1280, 1393, 1920, 1280, 1394, 1920, + ]; + + const frameLength = frameSizeMap[frameSizeCode * 3 + samplingRateCode] * 2; + if (start + frameLength > end) { + return -1; + } + + // get channel count + const channelMode = data[start + 6] >> 5; + let skipCount = 0; + if (channelMode === 2) { + skipCount += 2; + } else { + if (channelMode & 1 && channelMode !== 1) { + skipCount += 2; + } + if (channelMode & 4) { + skipCount += 2; + } + } + + const lfeon = + (((data[start + 6] << 8) | data[start + 7]) >> (12 - skipCount)) & 1; + + const channelsMap = [2, 1, 2, 3, 3, 4, 4, 5]; + const channelCount = channelsMap[channelMode] + lfeon; + + // build dac3 box (save it as a single int) + const bsid = data[start + 5] >> 3; + const bsmod = data[start + 5] & 7; + + const config = new Uint8Array([ + (samplingRateCode << 6) | (bsid << 1) | (bsmod >> 2), + ((bsmod & 3) << 6) | + (channelMode << 3) | + (lfeon << 2) | + (frameSizeCode >> 4), + (frameSizeCode << 4) & 0xe0, + ]); + + this.onAC3Frame( + data.subarray(start, start + frameLength), + sampleRate, + channelCount, + config, + frameIndex, + pts + ); + + return frameLength; + } + private parseID3PES(id3Track: DemuxedMetadataTrack, pes: PES) { if (pes.pts === undefined) { logger.warn('[tsdemuxer]: ID3 PES unknown PTS'); @@ -1061,6 +1177,15 @@ function parsePMT(data, offset, typeSupported, isSampleAes) { } break; + case 0x81: + if (typeSupported.ac3 !== true) { + logger.log('AC-3 audio found, not supported in this browser for now'); + } else if (result.audio === -1) { + result.audio = pid; + result.segmentCodec = 'ac3'; + } + break; + case 0x24: logger.warn('Unsupported HEVC stream type found'); break; diff --git a/src/remux/mp4-generator.ts b/src/remux/mp4-generator.ts index 4dfb4c6f395..908fc627826 100644 --- a/src/remux/mp4-generator.ts +++ b/src/remux/mp4-generator.ts @@ -41,6 +41,8 @@ class MP4 { moov: [], mp4a: [], '.mp3': [], + dac3: [], + 'ac-3': [], mvex: [], mvhd: [], pasp: [], @@ -775,79 +777,57 @@ class MP4 { ); // GASpecificConfig)); // length + audio config descriptor } - static mp4a(track) { + static audioStsd(track) { const samplerate = track.samplerate; + return new Uint8Array([ + 0x00, + 0x00, + 0x00, // reserved + 0x00, + 0x00, + 0x00, // reserved + 0x00, + 0x01, // data_reference_index + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, // reserved + 0x00, + track.channelCount, // channelcount + 0x00, + 0x10, // sampleSize:16bits + 0x00, + 0x00, + 0x00, + 0x00, // reserved2 + (samplerate >> 8) & 0xff, + samplerate & 0xff, // + 0x00, + 0x00, + ]); + } + + static mp4a(track) { return MP4.box( MP4.types.mp4a, - new Uint8Array([ - 0x00, - 0x00, - 0x00, // reserved - 0x00, - 0x00, - 0x00, // reserved - 0x00, - 0x01, // data_reference_index - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, // reserved - 0x00, - track.channelCount, // channelcount - 0x00, - 0x10, // sampleSize:16bits - 0x00, - 0x00, - 0x00, - 0x00, // reserved2 - (samplerate >> 8) & 0xff, - samplerate & 0xff, // - 0x00, - 0x00, - ]), + MP4.audioStsd(track), MP4.box(MP4.types.esds, MP4.esds(track)) ); } static mp3(track) { - const samplerate = track.samplerate; + return MP4.box(MP4.types['.mp3'], MP4.audioStsd(track)); + } + + static ac3(track) { return MP4.box( - MP4.types['.mp3'], - new Uint8Array([ - 0x00, - 0x00, - 0x00, // reserved - 0x00, - 0x00, - 0x00, // reserved - 0x00, - 0x01, // data_reference_index - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, - 0x00, // reserved - 0x00, - track.channelCount, // channelcount - 0x00, - 0x10, // sampleSize:16bits - 0x00, - 0x00, - 0x00, - 0x00, // reserved2 - (samplerate >> 8) & 0xff, - samplerate & 0xff, // - 0x00, - 0x00, - ]) - ); + MP4.types['ac-3'], + MP4.audioStsd(track), + MP4.box(MP4.types.dac3, track.config)); } static stsd(track) { @@ -855,7 +835,9 @@ class MP4 { if (track.segmentCodec === 'mp3' && track.codec === 'mp3') { return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp3(track)); } - + if (track.segmentCodec === 'ac3') { + return MP4.box(MP4.types.stsd, MP4.STSD, MP4.ac3(track)); + } return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp4a(track)); } else { return MP4.box(MP4.types.stsd, MP4.STSD, MP4.avc1(track)); diff --git a/src/remux/mp4-remuxer.ts b/src/remux/mp4-remuxer.ts index ff23306c187..330cbe56572 100644 --- a/src/remux/mp4-remuxer.ts +++ b/src/remux/mp4-remuxer.ts @@ -30,6 +30,7 @@ import type { HlsConfig } from '../config'; const MAX_SILENT_FRAME_DURATION = 10 * 1000; // 10 seconds const AAC_SAMPLES_PER_FRAME = 1024; const MPEG_AUDIO_SAMPLE_PER_FRAME = 1152; +const AC3_SAMPLES_PER_FRAME = 1536; let chromeVersion: number | null = null; let safariWebkitVersion: number | null = null; @@ -307,6 +308,10 @@ export default class MP4Remuxer implements Remuxer { audioTrack.codec = 'mp3'; } break; + + case 'ac3': + audioTrack.codec = 'ac-3'; + break; } tracks.audio = { id: 'audio', @@ -683,6 +688,17 @@ export default class MP4Remuxer implements Remuxer { return data; } + getSamplesPerFrame(track: DemuxedAudioTrack) { + switch (track.segmentCodec) { + case 'mp3': + return MPEG_AUDIO_SAMPLE_PER_FRAME; + case 'ac3': + return AC3_SAMPLES_PER_FRAME; + default: + return AAC_SAMPLES_PER_FRAME; + } + } + remuxAudio( track: DemuxedAudioTrack, timeOffset: number, @@ -695,10 +711,7 @@ export default class MP4Remuxer implements Remuxer { ? track.samplerate : inputTimeScale; const scaleFactor: number = inputTimeScale / mp4timeScale; - const mp4SampleDuration: number = - track.segmentCodec === 'aac' - ? AAC_SAMPLES_PER_FRAME - : MPEG_AUDIO_SAMPLE_PER_FRAME; + const mp4SampleDuration: number = this.getSamplesPerFrame(track); const inputSampleDuration: number = mp4SampleDuration * scaleFactor; const initPTS: number = this._initPTS; const rawMPEG: boolean =