Skip to content

Commit

Permalink
Merge pull request video-dev#1113 from nochev/mpeg-audio-only-support
Browse files Browse the repository at this point in the history
Mpeg audio only support
  • Loading branch information
mangui authored May 3, 2017
2 parents 17fcc09 + 0cc17d2 commit 450c5a7
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 103 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ All HLS resources must be delivered with [CORS headers](https://developer.mozill
- ISO/IEC 11172-3 / ISO/IEC 13818-3 (MPEG-1/2 Audio Layer III) Elementary Stream
- Packetized metadata (ID3) Elementary Stream
- AAC container (audio only streams)
- MPEG Audio container (MPEG-1/2 Audio Layer III audio only streams)
- Timed Metadata for HTTP Live Streaming (in ID3 format, carried in MPEG-2 TS)
- AES-128 decryption
- SAMPLE-AES decryption
Expand All @@ -171,7 +172,6 @@ All HLS resources must be delivered with [CORS headers](https://developer.mozill
- [Redundant/Failover Playlists](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StreamingMediaGuide/UsingHTTPLiveStreaming/UsingHTTPLiveStreaming.html#//apple_ref/doc/uid/TP40008332-CH102-SW22)

## Not Supported (Yet)
- MPEG Audio container (MPEG-1/2 Audio Layer III audio only streams)
- MP3 Elementary Stream in Edge for Windows 10+

### Supported M3U8 tags
Expand Down
34 changes: 18 additions & 16 deletions src/demux/aacdemuxer.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,21 @@ import ID3 from '../demux/id3';
}

resetInitSegment(initSegment,audioCodec,videoCodec, duration) {
this._aacTrack = {container : 'audio/adts', type: 'audio', id :-1, sequenceNumber: 0, isAAC : true , samples : [], len : 0, manifestCodec : audioCodec, duration : duration, inputTimeScale : 90000};
this._audioTrack = {container : 'audio/adts', type: 'audio', id :-1, sequenceNumber: 0, isAAC : true , samples : [], len : 0, manifestCodec : audioCodec, duration : duration, inputTimeScale : 90000};
}

resetTimeStamp() {
}

static probe(data) {
// check if data contains ID3 timestamp and ADTS sync worc
var id3 = new ID3(data), offset,len;
// check if data contains ID3 timestamp and ADTS sync word
var id3 = new ID3(data), offset, length;
if(id3.hasTimeStamp) {
// look for ADTS header (0xFFFx)
for (offset = id3.length, len = data.length; offset < len - 1; offset++) {
if ((data[offset] === 0xff) && (data[offset+1] & 0xf0) === 0xf0) {
// 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
for (offset = id3.length, length = Math.min(data.length - 1, offset + 100); offset < length; offset++) {
if ((data[offset] === 0xff) && (data[offset+1] & 0xf6) === 0xf0) {
//logger.log('ADTS sync word found !');
return true;
}
Expand All @@ -41,13 +43,13 @@ import ID3 from '../demux/id3';
var track,
id3 = new ID3(data),
pts = 90*id3.timeStamp,
config, frameLength, frameDuration, frameIndex, offset, headerLength, stamp, len, aacSample;
config, frameLength, frameDuration, frameIndex, offset, headerLength, stamp, length, aacSample;

track = this._aacTrack;
track = this._audioTrack;

// look for ADTS header (0xFFFx)
for (offset = id3.length, len = data.length; offset < len - 1; offset++) {
if ((data[offset] === 0xff) && (data[offset+1] & 0xf0) === 0xf0) {
// Look for ADTS header
for (offset = id3.length, length = data.length; offset < length - 1; offset++) {
if ((data[offset] === 0xff) && (data[offset+1] & 0xf6) === 0xf0) {
break;
}
}
Expand All @@ -62,7 +64,7 @@ import ID3 from '../demux/id3';
}
frameIndex = 0;
frameDuration = 1024 * 90000 / track.samplerate;
while ((offset + 5) < len) {
while ((offset + 5) < length) {
// The protection skip bit tells us if we have 2 bytes of CRC data at the end of the ADTS header
headerLength = (!!(data[offset + 1] & 0x01) ? 7 : 9);
// retrieve frame size
Expand All @@ -72,7 +74,7 @@ import ID3 from '../demux/id3';
frameLength -= headerLength;
//stamp = pes.pts;

if ((frameLength > 0) && ((offset + headerLength + frameLength) <= len)) {
if ((frameLength > 0) && ((offset + headerLength + frameLength) <= length)) {
stamp = pts + frameIndex * frameDuration;
//logger.log(`AAC frame, offset/length/total/pts:${offset+headerLength}/${frameLength}/${data.byteLength}/${(stamp/90).toFixed(0)}`);
aacSample = {unit: data.subarray(offset + headerLength, offset + headerLength + frameLength), pts: stamp, dts: stamp};
Expand All @@ -81,8 +83,8 @@ import ID3 from '../demux/id3';
offset += frameLength + headerLength;
frameIndex++;
// look for ADTS header (0xFFFx)
for ( ; offset < (len - 1); offset++) {
if ((data[offset] === 0xff) && ((data[offset + 1] & 0xf0) === 0xf0)) {
for ( ; offset < (length - 1); offset++) {
if ((data[offset] === 0xff) && ((data[offset + 1] & 0xf6) === 0xf0)) {
break;
}
}
Expand All @@ -92,7 +94,7 @@ import ID3 from '../demux/id3';
}
this.remuxer.remux(track,
{samples : []},
{samples : [ { pts: pts, dts : pts, unit : id3.payload}], inputTimeScale : 90000},
{samples : [ { pts: pts, dts : pts, data : id3.payload}], inputTimeScale : 90000},
{samples : []},
timeOffset,
contiguous,
Expand Down
2 changes: 2 additions & 0 deletions src/demux/demuxer-inline.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Decrypter from '../crypt/decrypter';
import AACDemuxer from '../demux/aacdemuxer';
import MP4Demuxer from '../demux/mp4demuxer';
import TSDemuxer from '../demux/tsdemuxer';
import MP3Demuxer from '../demux/mp3demuxer';
import MP4Remuxer from '../remux/mp4-remuxer';
import PassThroughRemuxer from '../remux/passthrough-remuxer';

Expand Down Expand Up @@ -66,6 +67,7 @@ class DemuxerInline {
const typeSupported = this.typeSupported;
const config = this.config;
const muxConfig = [ {demux : TSDemuxer, remux : MP4Remuxer},
{demux : MP3Demuxer, remux : MP4Remuxer},
{demux : AACDemuxer, remux : MP4Remuxer},
{demux : MP4Demuxer, remux : PassThroughRemuxer}];

Expand Down
1 change: 1 addition & 0 deletions src/demux/id3.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {logger} from '../utils/logger';

constructor(data) {
this._hasTimeStamp = false;
this._length = 0;
var offset = 0, byte1,byte2,byte3,byte4,tagSize,endPos,header,len;
do {
header = this.readUTF(data,offset,3);
Expand Down
69 changes: 69 additions & 0 deletions src/demux/mp3demuxer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* MP3 demuxer
*/
import ID3 from '../demux/id3';
import MpegAudio from './mpegaudio';

class MP3Demuxer {

constructor(observer, remuxer, config) {
this.observer = observer;
this.config = config;
this.remuxer = remuxer;
}

resetInitSegment(initSegment,audioCodec,videoCodec, duration) {
this._audioTrack = {container : 'audio/mpeg', type: 'audio', id :-1, sequenceNumber: 0, isAAC : false , samples : [], len : 0, manifestCodec : audioCodec, duration : duration, inputTimeScale : 90000};
}

resetTimeStamp() {
}

static probe(data) {
// check if data contains ID3 timestamp and MPEG sync word
var id3 = new ID3(data), offset, length;
if (id3.hasTimeStamp) {
// 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
for (offset = id3.length, length = Math.min(data.length - 1, offset + 100); offset < length; offset++) {
if ((data[offset] === 0xff) && (data[offset+1] & 0xe0) === 0xe0 && (data[offset+1] & 0x06) !== 0x00) {
//logger.log('MPEG sync word found !');
return true;
}
}
}
return false;
}


// feed incoming data to the front of the parsing pipeline
append(data, timeOffset,contiguous,accurateTimeOffset) {
var id3 = new ID3(data);
var pts = 90*id3.timeStamp;
var afterID3 = id3.length;
var offset, length;

// Look for MPEG header
for (offset = afterID3, length = data.length; offset < length - 1; offset++) {
if ((data[offset] === 0xff) && (data[offset+1] & 0xe0) === 0xe0 && (data[offset+1] & 0x06) !== 0x00) {
break;
}
}

MpegAudio.parse(this._audioTrack, data, id3.length, pts);

this.remuxer.remux(this._audioTrack,
{samples : []},
{samples : [ { pts: pts, dts : pts, data : id3.payload}], inputTimeScale : 90000},
{samples : []},
timeOffset,
contiguous,
accurateTimeOffset);
}

destroy() {
}
}

export default MP3Demuxer;
90 changes: 90 additions & 0 deletions src/demux/mpegaudio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* MPEG parser helper
*/
import {logger} from '../utils/logger';

const MpegAudio = {

onFrame: function(track, data, bitRate, sampleRate, channelCount, frameIndex, pts) {
var frameDuration = 1152 * 90000 / sampleRate;
var stamp = pts + frameIndex * frameDuration;

track.config = [];
track.channelCount = channelCount;
track.samplerate = sampleRate;
track.samples.push({unit: data, pts: stamp, dts: stamp});
track.len += data.length;
},

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

parseFrames: function(track, 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;
}

this.onFrame(track, 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
this.onNoise(data.subarray(start, offset - 1));

return offset - start - 1;
}
offset++;
}
return -1;
},

parse: function(track, data, offset, pts) {
var length = data.length;
var frameIndex = 0;
var parsed;

while (offset < length &&
(parsed = this.parseFrames(track, data, offset, length, frameIndex++, pts)) > 0) {
offset += parsed;
}
}
};

module.exports = MpegAudio;
Loading

0 comments on commit 450c5a7

Please sign in to comment.