Skip to content

Commit

Permalink
Merge pull request video-dev#5167 from erankor/support-ac-3
Browse files Browse the repository at this point in the history
Add support for AC-3 codec in MPEG-TS
  • Loading branch information
robwalch authored Jun 1, 2023
2 parents 890fae3 + 77c41c4 commit fe6f333
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 68 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,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)
Expand Down
1 change: 1 addition & 0 deletions src/demux/transmuxer-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,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
Expand Down
125 changes: 125 additions & 0 deletions src/demux/tsdemuxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface TypeSupported {
mpeg: boolean;
mp3: boolean;
mp4: boolean;
ac3: boolean;
}

const PACKET_LENGTH = 188;
Expand Down Expand Up @@ -313,6 +314,9 @@ class TSDemuxer implements Demuxer {
case 'mp3':
this.parseMPEGPES(audioTrack, pes);
break;
case 'ac3':
this.parseAC3PES(pes);
break;
}
}
audioData = { data: [], size: 0 };
Expand Down Expand Up @@ -479,6 +483,9 @@ class TSDemuxer implements Demuxer {
case 'mp3':
this.parseMPEGPES(audioTrack, pes);
break;
case 'ac3':
this.parseAC3PES(pes);
break;
}
audioTrack.pesData = null;
} else {
Expand Down Expand Up @@ -994,6 +1001,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
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');
Expand Down Expand Up @@ -1106,6 +1222,15 @@ function parsePMT(
}
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;
Expand Down
111 changes: 47 additions & 64 deletions src/remux/mp4-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class MP4 {
moov: [],
mp4a: [],
'.mp3': [],
dac3: [],
'ac-3': [],
mvex: [],
mvhd: [],
pasp: [],
Expand Down Expand Up @@ -772,78 +774,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)
);
}

Expand All @@ -852,7 +833,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));
Expand Down
21 changes: 17 additions & 4 deletions src/remux/mp4-remuxer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,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;
Expand Down Expand Up @@ -327,6 +328,10 @@ export default class MP4Remuxer implements Remuxer {
audioTrack.codec = 'mp3';
}
break;

case 'ac3':
audioTrack.codec = 'ac-3';
break;
}
tracks.audio = {
id: 'audio',
Expand Down Expand Up @@ -712,6 +717,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,
Expand All @@ -724,10 +740,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 = this._initPTS as RationalTimestamp;
const rawMPEG: boolean =
Expand Down

0 comments on commit fe6f333

Please sign in to comment.