Skip to content

Commit

Permalink
Playing MPEG audio. Muxing mp3 audio in mp4 container.
Browse files Browse the repository at this point in the history
  • Loading branch information
nochev committed Nov 2, 2016
1 parent 2a059a6 commit 8b8e09b
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 16 deletions.
4 changes: 2 additions & 2 deletions src/demux/demuxer-inline.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ class DemuxerInline {
// probe for content type
if (TSDemuxer.probe(data)) {
if (this.typeSupported.mp2t === true) {
demuxer = new TSDemuxer(hls, id, PassThroughRemuxer, this.config);
demuxer = new TSDemuxer(hls, id, PassThroughRemuxer, this.config, this.typeSupported);
} else {
demuxer = new TSDemuxer(hls, id, MP4Remuxer, this.config);
demuxer = new TSDemuxer(hls, id, MP4Remuxer, this.config, this.typeSupported);
}
} else if(AACDemuxer.probe(data)) {
demuxer = new AACDemuxer(hls, id, MP4Remuxer, this.config);
Expand Down
7 changes: 6 additions & 1 deletion src/demux/demuxer.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ class Demuxer {
this.id = id;
var typeSupported = {
mp4 : MediaSource.isTypeSupported('video/mp4'),
mp2t : hls.config.enableMP2TPassThrough && MediaSource.isTypeSupported('video/mp2t')
mp2t : hls.config.enableMP2TPassThrough && MediaSource.isTypeSupported('video/mp2t'),
mpeg: MediaSource.isTypeSupported('audio/mpeg'),
mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'),
mp4a4034: MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.40.34"'),
mp4a69: MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.69"'),
mp4a6B: MediaSource.isTypeSupported('audio/mp4; codecs="mp4a.6B"'),
};
if (hls.config.enableWorker && (typeof(Worker) !== 'undefined')) {
logger.log('demuxing in webworker');
Expand Down
189 changes: 183 additions & 6 deletions src/demux/tsdemuxer.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@

class TSDemuxer {

constructor(observer, id, remuxerClass, config) {
constructor(observer, id, remuxerClass, config, typeSupported) {
this.observer = observer;
this.id = id;
this.remuxerClass = remuxerClass;
this.config = config;
this.typeSupported = typeSupported;
this.lastCC = 0;
this.remuxer = new this.remuxerClass(observer, id, config);
this.remuxer = new this.remuxerClass(observer, id, config, typeSupported);
}

static probe(data) {
Expand All @@ -40,7 +41,7 @@
this.pmtParsed = false;
this._pmtId = -1;
this._avcTrack = {container : 'video/mp2t', type: 'video', id :-1, sequenceNumber: 0, samples : [], len : 0, dropped : 0};
this._aacTrack = {container : 'video/mp2t', type: 'audio', id :-1, sequenceNumber: 0, samples : [], len : 0};
this._aacTrack = {container : 'video/mp2t', type: 'audio', id :-1, sequenceNumber: 0, samples : [], len : 0, isAAC: true};
this._id3Track = {type: 'id3', id :-1, sequenceNumber: 0, samples : [], len : 0};
this._txtTrack = {type: 'text', id: -1, sequenceNumber: 0, samples : [], len : 0};
// flush any partial content
Expand Down Expand Up @@ -96,6 +97,7 @@
parsePES = this._parsePES,
parseAVCPES = this._parseAVCPES.bind(this),
parseAACPES = this._parseAACPES.bind(this),
parseMPEGPES = this._parseMPEGPES.bind(this),
parseID3PES = this._parseID3PES.bind(this);

// don't parse last TS packet if incomplete
Expand Down Expand Up @@ -142,7 +144,12 @@
case aacId:
if (stt) {
if (aacData && (pes = parsePES(aacData))) {
parseAACPES(pes);
if (aacTrack.isAAC) {
parseAACPES(pes);
}
else {
parseMPEGPES(pes);
}
if (codecsOnly) {
// here we now that we have audio codec info
// if video PID is undefined OR if we have video codec info,
Expand Down Expand Up @@ -186,6 +193,7 @@
avcId = avcTrack.id = parsedPIDs.avc;
aacId = aacTrack.id = parsedPIDs.aac;
id3Id = id3Track.id = parsedPIDs.id3;
aacTrack.isAAC = parsedPIDs.isAAC;
if (unknownPIDs && !pmtParsed) {
logger.log('reparse from beginning');
unknownPIDs = false;
Expand Down Expand Up @@ -215,7 +223,12 @@
}

if (aacData && (pes = parsePES(aacData))) {
parseAACPES(pes);
if (aacTrack.isAAC) {
parseAACPES(pes);
}
else {
parseMPEGPES(pes);
}
aacTrack.pesData = null;
} else {
if (aacData && aacData.size) {
Expand Down Expand Up @@ -269,7 +282,7 @@
}

_parsePMT(data, offset) {
var sectionLength, tableEnd, programInfoLength, pid, result = { aac : -1, avc : -1, id3 : -1};
var sectionLength, tableEnd, programInfoLength, pid, result = { aac : -1, avc : -1, id3 : -1, isAAC : true};
sectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2];
tableEnd = offset + 3 + sectionLength - 4;
// to determine where the table is, we have to figure out how
Expand Down Expand Up @@ -301,6 +314,16 @@
result.avc = pid;
}
break;
// ISO/IEC 11172-3 (MPEG-1 audio)
// or ISO/IEC 13818-3 (MPEG-2 halved sample rate audio)
case 0x03:
case 0x04:
logger.log('MPEG PID:' + pid);
if (result.aac === -1) {
result.aac = pid;
result.isAAC = false;
}
break;
case 0x24:
logger.warn('HEVC stream type found, not supported for now');
break;
Expand Down Expand Up @@ -885,6 +908,160 @@
this.aacLastPTS = stamp;
}

_parseMPEGPES(pes) {
var data = pes.data;
var pts = pes.pts;
var length;

if (typeof this.mpegBuffer === 'undefined') {
this.mpegBuffer = null;
this.mpegBufferSize = 0;
}

if (this.mpegBufferSize > 0) {
var needBuffer = data.length + this.mpegBufferSize;
if (!this.mpegBuffer || this.mpegBuffer.length < needBuffer) {
var newBuffer = new Uint8Array(needBuffer);
if (this.mpegBufferSize > 0) {
newBuffer.set(this.mpegBuffer.subarray(0, this.mpegBufferSize));
}
this.mpegBuffer = newBuffer;
}
this.mpegBuffer.set(data, this.mpegBufferSize);
this.mpegBufferSize = needBuffer;
data = this.mpegBuffer;
length = needBuffer;
}
else {
length = data.length;
}

var frameIndex = 0;
var offset = 0;
var parsed;
while (offset < length &&
(parsed = this._parseMpeg(data, offset, length, frameIndex++, pts)) > 0) {
offset += parsed;
}
var tail = length - offset;
if (tail > 0) {
if (!this.mpegBuffer || this.mpegBuffer.length < tail) {
this.mpegBuffer = new Uint8Array(data.subarray(offset, length));
}
else {
this.mpegBuffer.set(data.subarray(offset, length));
}
}
this.mpegBufferSize = tail;
}

_onMpegFrame(data, bitRate, sampleRate, channelCount, frameIndex, pts) {
var frameDuration = (1152 / sampleRate) * 1000;

var stamp = pts + frameIndex * frameDuration;

var track = this._aacTrack;

// Build audio config for mp4a.40.34
if (this.typeSupported.mp4a4034 === true) {
var audioConfigSampleingRates = {
96000: 0,
88200: 1,
64000: 2,
48000: 3,
44100: 4,
32000: 5,
24000: 6,
22050: 7,
16000: 8,
12000: 9,
11025: 10,
8000: 11,
7350: 12,
};
var audioSamplingIndex = audioConfigSampleingRates[sampleRate];

track.config = new Array(4);
// For mp4a.40.34 we need 11 bits. More information: https://wiki.multimedia.cx/index.php?title=MPEG-4_Audio
track.config[0] = (31 << 3);
track.config[1] |= 0x01 << 6;
track.config[1] |= (audioSamplingIndex & 0x0F) << 1;
// channelConfiguration
track.config[1] |= (channelCount & 0x0F) >> 3;
track.config[2] |= (channelCount & 0x0F) << 5;
track.config[3] = 0;
}
else {
track.config = [];
}

track.channelCount = channelCount;
track.audiosamplerate = sampleRate;
track.duration = this._duration;
track.samples.push({unit: data, pts: stamp, dts: stamp});
track.len += data.length;
}

_onMpegNoise(data) {
logger.warn('mpeg audio has noise: ' + data.length + ' bytes');
}

_parseMpeg(data, start, end, frameIndex, pts) {
var 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];
var SamplingRateMap = [44100, 48000, 32000, 22050, 24000, 16000, 11025, 12000, 8000];

if (start + 2 > end) {
return -1; // we need at least 2 bytes to detect sync pattern
}
if (data[start] === 0xFF || (data[start + 1] & 0xE0) === 0xE0) {
// Using http://www.datavoyage.com/mpgscript/mpeghdr.htm as a reference
if (start + 24 > end) {
return -1;
}
var headerB = (data[start + 1] >> 3) & 3;
var headerC = (data[start + 1] >> 1) & 3;
var headerE = (data[start + 2] >> 4) & 15;
var headerF = (data[start + 2] >> 2) & 3;
var headerG = !!(data[start + 2] & 2);
if (headerB !== 1 && headerE !== 0 && headerE !== 15 && headerF !== 3) {
var columnInBitrates = headerB === 3 ? (3 - headerC) : (headerC === 3 ? 3 : 4);
var bitRate = BitratesMap[columnInBitrates * 14 + headerE - 1] * 1000;
var columnInSampleRates = headerB === 3 ? 0 : headerB === 2 ? 1 : 2;
var sampleRate = SamplingRateMap[columnInSampleRates * 3 + headerF];
var padding = headerG ? 1 : 0;
var channelCount = data[start + 3] >> 6 === 3 ? 1 : 2; // If bits of channel mode are `11` then it is a single channel (Mono)
var frameLength = headerC === 3 ?
((headerB === 3 ? 12 : 6) * bitRate / sampleRate + padding) << 2 :
((headerB === 3 ? 144 : 72) * bitRate / sampleRate + padding) | 0;
if (start + frameLength > end) {
return -1;
}
if (this._onMpegFrame) {
this._onMpegFrame(data.subarray(start, start + frameLength), bitRate, sampleRate, channelCount, frameIndex, pts);
}
return frameLength;
}
}
// noise or ID3, trying to skip
var offset = start + 2;
while (offset < end) {
if (data[offset - 1] === 0xFF && (data[offset] & 0xE0) === 0xE0) {
// sync pattern is found
if (this._onMpegNoise) {
this._onMpegNoise(data.subarray(start, offset - 1));
}
return offset - start - 1;
}
offset++;
}
return -1;
}

_parseID3PES(pes) {
this._id3Track.samples.push(pes);
}
Expand Down
31 changes: 30 additions & 1 deletion src/remux/mp4-generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class MP4 {
moof: [],
moov: [],
mp4a: [],
'.mp3': [],
mvex: [],
mvhd: [],
sdtp: [],
Expand Down Expand Up @@ -378,6 +379,13 @@ class MP4 {

static esds(track) {
var configlen = track.config.length;
var mpeg4Audio = 0x40;
if (track.codec === 'mp4a.69') {
mpeg4Audio = 0x69;
}
else if (track.codec === 'mp4a.6B') {
mpeg4Audio = 0x6B;
}
return new Uint8Array([
0x00, // version 0
0x00, 0x00, 0x00, // flags
Expand All @@ -389,7 +397,7 @@ class MP4 {

0x04, // descriptor_type
0x0f+configlen, // length
0x40, //codec : mpeg4_audio
mpeg4Audio, //codec : mpeg4_audio
0x15, // stream_type
0x00, 0x00, 0x00, // buffer_size
0x00, 0x00, 0x00, 0x00, // maxBitrate
Expand All @@ -416,8 +424,29 @@ class MP4 {
MP4.box(MP4.types.esds, MP4.esds(track)));
}

static mp3(track) {
var audiosamplerate = track.audiosamplerate;
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
(audiosamplerate >> 8) & 0xFF,
audiosamplerate & 0xff, //
0x00, 0x00]));
}

static stsd(track) {
if (track.type === 'audio') {
if (!track.isAAC) {
if (track.codec === 'mp3') {
return MP4.box(MP4.types.stsd, MP4.STSD, MP4.mp3(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
Loading

0 comments on commit 8b8e09b

Please sign in to comment.