1045 lines
42 KiB
JavaScript
1045 lines
42 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
|
|
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); /**
|
|
* highly optimized TS demuxer:
|
|
* parse PAT, PMT
|
|
* extract PES packet from audio and video PIDs
|
|
* extract AVC/H264 NAL units and AAC/ADTS samples from PES packet
|
|
* trigger the remuxer upon parsing completion
|
|
* it also tries to workaround as best as it can audio codec switch (HE-AAC to AAC and vice versa), without having to restart the MediaSource.
|
|
* it also controls the remuxing process :
|
|
* upon discontinuity or level switch detection, it will also notifies the remuxer so that it can reset its state.
|
|
*/
|
|
|
|
// import Hex from '../utils/hex';
|
|
|
|
|
|
var _adts = require('./adts');
|
|
|
|
var _adts2 = _interopRequireDefault(_adts);
|
|
|
|
var _mpegaudio = require('./mpegaudio');
|
|
|
|
var _mpegaudio2 = _interopRequireDefault(_mpegaudio);
|
|
|
|
var _events = require('../events');
|
|
|
|
var _events2 = _interopRequireDefault(_events);
|
|
|
|
var _expGolomb = require('./exp-golomb');
|
|
|
|
var _expGolomb2 = _interopRequireDefault(_expGolomb);
|
|
|
|
var _sampleAes = require('./sample-aes');
|
|
|
|
var _sampleAes2 = _interopRequireDefault(_sampleAes);
|
|
|
|
var _logger = require('../utils/logger');
|
|
|
|
var _errors = require('../errors');
|
|
|
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
|
|
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
|
|
|
|
var TSDemuxer = function () {
|
|
function TSDemuxer(observer, remuxer, config, typeSupported) {
|
|
_classCallCheck(this, TSDemuxer);
|
|
|
|
this.observer = observer;
|
|
this.config = config;
|
|
this.typeSupported = typeSupported;
|
|
this.remuxer = remuxer;
|
|
this.sampleAes = null;
|
|
}
|
|
|
|
_createClass(TSDemuxer, [{
|
|
key: 'setDecryptData',
|
|
value: function setDecryptData(decryptdata) {
|
|
if (decryptdata != null && decryptdata.key != null && decryptdata.method === 'SAMPLE-AES') {
|
|
this.sampleAes = new _sampleAes2.default(this.observer, this.config, decryptdata, this.discardEPB);
|
|
} else {
|
|
this.sampleAes = null;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'resetInitSegment',
|
|
value: function resetInitSegment(initSegment, audioCodec, videoCodec, duration) {
|
|
this.pmtParsed = false;
|
|
this._pmtId = -1;
|
|
this._avcTrack = { container: 'video/mp2t', type: 'video', id: -1, inputTimeScale: 90000, sequenceNumber: 0, samples: [], len: 0, dropped: 0 };
|
|
this._audioTrack = { container: 'video/mp2t', type: 'audio', id: -1, inputTimeScale: 90000, duration: duration, sequenceNumber: 0, samples: [], len: 0, isAAC: true };
|
|
this._id3Track = { type: 'id3', id: -1, inputTimeScale: 90000, sequenceNumber: 0, samples: [], len: 0 };
|
|
this._txtTrack = { type: 'text', id: -1, inputTimeScale: 90000, sequenceNumber: 0, samples: [], len: 0 };
|
|
// flush any partial content
|
|
this.aacOverFlow = null;
|
|
this.aacLastPTS = null;
|
|
this.avcSample = null;
|
|
this.audioCodec = audioCodec;
|
|
this.videoCodec = videoCodec;
|
|
this._duration = duration;
|
|
}
|
|
}, {
|
|
key: 'resetTimeStamp',
|
|
value: function resetTimeStamp() {}
|
|
|
|
// feed incoming data to the front of the parsing pipeline
|
|
|
|
}, {
|
|
key: 'append',
|
|
value: function append(data, timeOffset, contiguous, accurateTimeOffset) {
|
|
var start,
|
|
len = data.length,
|
|
stt,
|
|
pid,
|
|
atf,
|
|
offset,
|
|
pes,
|
|
unknownPIDs = false;
|
|
this.contiguous = contiguous;
|
|
var pmtParsed = this.pmtParsed,
|
|
avcTrack = this._avcTrack,
|
|
audioTrack = this._audioTrack,
|
|
id3Track = this._id3Track,
|
|
avcId = avcTrack.id,
|
|
audioId = audioTrack.id,
|
|
id3Id = id3Track.id,
|
|
pmtId = this._pmtId,
|
|
avcData = avcTrack.pesData,
|
|
audioData = audioTrack.pesData,
|
|
id3Data = id3Track.pesData,
|
|
parsePAT = this._parsePAT,
|
|
parsePMT = this._parsePMT,
|
|
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
|
|
len -= len % 188;
|
|
// loop through TS packets
|
|
for (start = 0; start < len; start += 188) {
|
|
if (data[start] === 0x47) {
|
|
stt = !!(data[start + 1] & 0x40);
|
|
// pid is a 13-bit field starting at the last bit of TS[1]
|
|
pid = ((data[start + 1] & 0x1f) << 8) + data[start + 2];
|
|
atf = (data[start + 3] & 0x30) >> 4;
|
|
// if an adaption field is present, its length is specified by the fifth byte of the TS packet header.
|
|
if (atf > 1) {
|
|
offset = start + 5 + data[start + 4];
|
|
// continue if there is only adaptation field
|
|
if (offset === start + 188) {
|
|
continue;
|
|
}
|
|
} else {
|
|
offset = start + 4;
|
|
}
|
|
switch (pid) {
|
|
case avcId:
|
|
if (stt) {
|
|
if (avcData && (pes = parsePES(avcData))) {
|
|
parseAVCPES(pes, false);
|
|
}
|
|
avcData = { data: [], size: 0 };
|
|
}
|
|
if (avcData) {
|
|
avcData.data.push(data.subarray(offset, start + 188));
|
|
avcData.size += start + 188 - offset;
|
|
}
|
|
break;
|
|
case audioId:
|
|
if (stt) {
|
|
if (audioData && (pes = parsePES(audioData))) {
|
|
if (audioTrack.isAAC) {
|
|
parseAACPES(pes);
|
|
} else {
|
|
parseMPEGPES(pes);
|
|
}
|
|
}
|
|
audioData = { data: [], size: 0 };
|
|
}
|
|
if (audioData) {
|
|
audioData.data.push(data.subarray(offset, start + 188));
|
|
audioData.size += start + 188 - offset;
|
|
}
|
|
break;
|
|
case id3Id:
|
|
if (stt) {
|
|
if (id3Data && (pes = parsePES(id3Data))) {
|
|
parseID3PES(pes);
|
|
}
|
|
id3Data = { data: [], size: 0 };
|
|
}
|
|
if (id3Data) {
|
|
id3Data.data.push(data.subarray(offset, start + 188));
|
|
id3Data.size += start + 188 - offset;
|
|
}
|
|
break;
|
|
case 0:
|
|
if (stt) {
|
|
offset += data[offset] + 1;
|
|
}
|
|
pmtId = this._pmtId = parsePAT(data, offset);
|
|
break;
|
|
case pmtId:
|
|
if (stt) {
|
|
offset += data[offset] + 1;
|
|
}
|
|
var parsedPIDs = parsePMT(data, offset, this.typeSupported.mpeg === true || this.typeSupported.mp3 === true, this.sampleAes != null);
|
|
|
|
// only update track id if track PID found while parsing PMT
|
|
// this is to avoid resetting the PID to -1 in case
|
|
// track PID transiently disappears from the stream
|
|
// this could happen in case of transient missing audio samples for example
|
|
avcId = parsedPIDs.avc;
|
|
if (avcId > 0) {
|
|
avcTrack.id = avcId;
|
|
}
|
|
audioId = parsedPIDs.audio;
|
|
if (audioId > 0) {
|
|
audioTrack.id = audioId;
|
|
audioTrack.isAAC = parsedPIDs.isAAC;
|
|
}
|
|
id3Id = parsedPIDs.id3;
|
|
if (id3Id > 0) {
|
|
id3Track.id = id3Id;
|
|
}
|
|
if (unknownPIDs && !pmtParsed) {
|
|
_logger.logger.log('reparse from beginning');
|
|
unknownPIDs = false;
|
|
// we set it to -188, the += 188 in the for loop will reset start to 0
|
|
start = -188;
|
|
}
|
|
pmtParsed = this.pmtParsed = true;
|
|
break;
|
|
case 17:
|
|
case 0x1fff:
|
|
break;
|
|
default:
|
|
unknownPIDs = true;
|
|
break;
|
|
}
|
|
} else {
|
|
this.observer.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.FRAG_PARSING_ERROR, fatal: false, reason: 'TS packet did not start with 0x47' });
|
|
}
|
|
}
|
|
// try to parse last PES packets
|
|
if (avcData && (pes = parsePES(avcData))) {
|
|
parseAVCPES(pes, true);
|
|
avcTrack.pesData = null;
|
|
} else {
|
|
// either avcData null or PES truncated, keep it for next frag parsing
|
|
avcTrack.pesData = avcData;
|
|
}
|
|
|
|
if (audioData && (pes = parsePES(audioData))) {
|
|
if (audioTrack.isAAC) {
|
|
parseAACPES(pes);
|
|
} else {
|
|
parseMPEGPES(pes);
|
|
}
|
|
audioTrack.pesData = null;
|
|
} else {
|
|
if (audioData && audioData.size) {
|
|
_logger.logger.log('last AAC PES packet truncated,might overlap between fragments');
|
|
}
|
|
// either audioData null or PES truncated, keep it for next frag parsing
|
|
audioTrack.pesData = audioData;
|
|
}
|
|
|
|
if (id3Data && (pes = parsePES(id3Data))) {
|
|
parseID3PES(pes);
|
|
id3Track.pesData = null;
|
|
} else {
|
|
// either id3Data null or PES truncated, keep it for next frag parsing
|
|
id3Track.pesData = id3Data;
|
|
}
|
|
|
|
if (this.sampleAes == null) {
|
|
this.remuxer.remux(audioTrack, avcTrack, id3Track, this._txtTrack, timeOffset, contiguous, accurateTimeOffset);
|
|
} else {
|
|
this.decryptAndRemux(audioTrack, avcTrack, id3Track, this._txtTrack, timeOffset, contiguous, accurateTimeOffset);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'decryptAndRemux',
|
|
value: function decryptAndRemux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, contiguous, accurateTimeOffset) {
|
|
if (audioTrack.samples && audioTrack.isAAC) {
|
|
var localthis = this;
|
|
this.sampleAes.decryptAacSamples(audioTrack.samples, 0, function () {
|
|
localthis.decryptAndRemuxAvc(audioTrack, videoTrack, id3Track, textTrack, timeOffset, contiguous, accurateTimeOffset);
|
|
});
|
|
} else {
|
|
this.decryptAndRemuxAvc(audioTrack, videoTrack, id3Track, textTrack, timeOffset, contiguous, accurateTimeOffset);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'decryptAndRemuxAvc',
|
|
value: function decryptAndRemuxAvc(audioTrack, videoTrack, id3Track, textTrack, timeOffset, contiguous, accurateTimeOffset) {
|
|
if (videoTrack.samples) {
|
|
var localthis = this;
|
|
this.sampleAes.decryptAvcSamples(videoTrack.samples, 0, 0, function () {
|
|
localthis.remuxer.remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, contiguous, accurateTimeOffset);
|
|
});
|
|
} else {
|
|
this.remuxer.remux(audioTrack, videoTrack, id3Track, textTrack, timeOffset, contiguous, accurateTimeOffset);
|
|
}
|
|
}
|
|
}, {
|
|
key: 'destroy',
|
|
value: function destroy() {
|
|
this._initPTS = this._initDTS = undefined;
|
|
this._duration = 0;
|
|
}
|
|
}, {
|
|
key: '_parsePAT',
|
|
value: function _parsePAT(data, offset) {
|
|
// skip the PSI header and parse the first PMT entry
|
|
return (data[offset + 10] & 0x1F) << 8 | data[offset + 11];
|
|
//logger.log('PMT PID:' + this._pmtId);
|
|
}
|
|
}, {
|
|
key: '_parsePMT',
|
|
value: function _parsePMT(data, offset, mpegSupported, isSampleAes) {
|
|
var sectionLength,
|
|
tableEnd,
|
|
programInfoLength,
|
|
pid,
|
|
result = { audio: -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
|
|
// long the program info descriptors are
|
|
programInfoLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11];
|
|
// advance the offset to the first entry in the mapping table
|
|
offset += 12 + programInfoLength;
|
|
while (offset < tableEnd) {
|
|
pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2];
|
|
switch (data[offset]) {
|
|
case 0xcf:
|
|
// SAMPLE-AES AAC
|
|
if (!isSampleAes) {
|
|
_logger.logger.log('unkown stream type:' + data[offset]);
|
|
break;
|
|
}
|
|
/* falls through */
|
|
|
|
// ISO/IEC 13818-7 ADTS AAC (MPEG-2 lower bit-rate audio)
|
|
case 0x0f:
|
|
//logger.log('AAC PID:' + pid);
|
|
if (result.audio === -1) {
|
|
result.audio = pid;
|
|
}
|
|
break;
|
|
|
|
// Packetized metadata (ID3)
|
|
case 0x15:
|
|
//logger.log('ID3 PID:' + pid);
|
|
if (result.id3 === -1) {
|
|
result.id3 = pid;
|
|
}
|
|
break;
|
|
|
|
case 0xdb:
|
|
// SAMPLE-AES AVC
|
|
if (!isSampleAes) {
|
|
_logger.logger.log('unkown stream type:' + data[offset]);
|
|
break;
|
|
}
|
|
/* falls through */
|
|
|
|
// ITU-T Rec. H.264 and ISO/IEC 14496-10 (lower bit-rate video)
|
|
case 0x1b:
|
|
//logger.log('AVC PID:' + pid);
|
|
if (result.avc === -1) {
|
|
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 (!mpegSupported) {
|
|
_logger.logger.log('MPEG audio found, not supported in this browser for now');
|
|
} else if (result.audio === -1) {
|
|
result.audio = pid;
|
|
result.isAAC = false;
|
|
}
|
|
break;
|
|
|
|
case 0x24:
|
|
_logger.logger.warn('HEVC stream type found, not supported for now');
|
|
break;
|
|
|
|
default:
|
|
_logger.logger.log('unkown stream type:' + data[offset]);
|
|
break;
|
|
}
|
|
// move to the next table entry
|
|
// skip past the elementary stream descriptors, if present
|
|
offset += ((data[offset + 3] & 0x0F) << 8 | data[offset + 4]) + 5;
|
|
}
|
|
return result;
|
|
}
|
|
}, {
|
|
key: '_parsePES',
|
|
value: function _parsePES(stream) {
|
|
var i = 0,
|
|
frag,
|
|
pesFlags,
|
|
pesPrefix,
|
|
pesLen,
|
|
pesHdrLen,
|
|
pesData,
|
|
pesPts,
|
|
pesDts,
|
|
payloadStartOffset,
|
|
data = stream.data;
|
|
// safety check
|
|
if (!stream || stream.size === 0) {
|
|
return null;
|
|
}
|
|
|
|
// we might need up to 19 bytes to read PES header
|
|
// if first chunk of data is less than 19 bytes, let's merge it with following ones until we get 19 bytes
|
|
// usually only one merge is needed (and this is rare ...)
|
|
while (data[0].length < 19 && data.length > 1) {
|
|
var newData = new Uint8Array(data[0].length + data[1].length);
|
|
newData.set(data[0]);
|
|
newData.set(data[1], data[0].length);
|
|
data[0] = newData;
|
|
data.splice(1, 1);
|
|
}
|
|
//retrieve PTS/DTS from first fragment
|
|
frag = data[0];
|
|
pesPrefix = (frag[0] << 16) + (frag[1] << 8) + frag[2];
|
|
if (pesPrefix === 1) {
|
|
pesLen = (frag[4] << 8) + frag[5];
|
|
// if PES parsed length is not zero and greater than total received length, stop parsing. PES might be truncated
|
|
// minus 6 : PES header size
|
|
if (pesLen && pesLen > stream.size - 6) {
|
|
return null;
|
|
}
|
|
pesFlags = frag[7];
|
|
if (pesFlags & 0xC0) {
|
|
/* PES header described here : http://dvd.sourceforge.net/dvdinfo/pes-hdr.html
|
|
as PTS / DTS is 33 bit we cannot use bitwise operator in JS,
|
|
as Bitwise operators treat their operands as a sequence of 32 bits */
|
|
pesPts = (frag[9] & 0x0E) * 536870912 + // 1 << 29
|
|
(frag[10] & 0xFF) * 4194304 + // 1 << 22
|
|
(frag[11] & 0xFE) * 16384 + // 1 << 14
|
|
(frag[12] & 0xFF) * 128 + // 1 << 7
|
|
(frag[13] & 0xFE) / 2;
|
|
// check if greater than 2^32 -1
|
|
if (pesPts > 4294967295) {
|
|
// decrement 2^33
|
|
pesPts -= 8589934592;
|
|
}
|
|
if (pesFlags & 0x40) {
|
|
pesDts = (frag[14] & 0x0E) * 536870912 + // 1 << 29
|
|
(frag[15] & 0xFF) * 4194304 + // 1 << 22
|
|
(frag[16] & 0xFE) * 16384 + // 1 << 14
|
|
(frag[17] & 0xFF) * 128 + // 1 << 7
|
|
(frag[18] & 0xFE) / 2;
|
|
// check if greater than 2^32 -1
|
|
if (pesDts > 4294967295) {
|
|
// decrement 2^33
|
|
pesDts -= 8589934592;
|
|
}
|
|
if (pesPts - pesDts > 60 * 90000) {
|
|
_logger.logger.warn(Math.round((pesPts - pesDts) / 90000) + 's delta between PTS and DTS, align them');
|
|
pesPts = pesDts;
|
|
}
|
|
} else {
|
|
pesDts = pesPts;
|
|
}
|
|
}
|
|
pesHdrLen = frag[8];
|
|
// 9 bytes : 6 bytes for PES header + 3 bytes for PES extension
|
|
payloadStartOffset = pesHdrLen + 9;
|
|
|
|
stream.size -= payloadStartOffset;
|
|
//reassemble PES packet
|
|
pesData = new Uint8Array(stream.size);
|
|
for (var j = 0, dataLen = data.length; j < dataLen; j++) {
|
|
frag = data[j];
|
|
var len = frag.byteLength;
|
|
if (payloadStartOffset) {
|
|
if (payloadStartOffset > len) {
|
|
// trim full frag if PES header bigger than frag
|
|
payloadStartOffset -= len;
|
|
continue;
|
|
} else {
|
|
// trim partial frag if PES header smaller than frag
|
|
frag = frag.subarray(payloadStartOffset);
|
|
len -= payloadStartOffset;
|
|
payloadStartOffset = 0;
|
|
}
|
|
}
|
|
pesData.set(frag, i);
|
|
i += len;
|
|
}
|
|
if (pesLen) {
|
|
// payload size : remove PES header + PES extension
|
|
pesLen -= pesHdrLen + 3;
|
|
}
|
|
return { data: pesData, pts: pesPts, dts: pesDts, len: pesLen };
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
}, {
|
|
key: 'pushAccesUnit',
|
|
value: function pushAccesUnit(avcSample, avcTrack) {
|
|
if (avcSample.units.length && avcSample.frame) {
|
|
var samples = avcTrack.samples;
|
|
var nbSamples = samples.length;
|
|
// only push AVC sample if starting with a keyframe is not mandatory OR
|
|
// if keyframe already found in this fragment OR
|
|
// keyframe found in last fragment (track.sps) AND
|
|
// samples already appended (we already found a keyframe in this fragment) OR fragment is contiguous
|
|
if (!this.config.forceKeyFrameOnDiscontinuity || avcSample.key === true || avcTrack.sps && (nbSamples || this.contiguous)) {
|
|
avcSample.id = nbSamples;
|
|
samples.push(avcSample);
|
|
} else {
|
|
// dropped samples, track it
|
|
avcTrack.dropped++;
|
|
}
|
|
}
|
|
if (avcSample.debug.length) {
|
|
_logger.logger.log(avcSample.pts + '/' + avcSample.dts + ':' + avcSample.debug);
|
|
}
|
|
}
|
|
}, {
|
|
key: '_parseAVCPES',
|
|
value: function _parseAVCPES(pes, last) {
|
|
var _this = this;
|
|
|
|
//logger.log('parse new PES');
|
|
var track = this._avcTrack,
|
|
units = this._parseAVCNALu(pes.data),
|
|
debug = false,
|
|
expGolombDecoder,
|
|
avcSample = this.avcSample,
|
|
push,
|
|
spsfound = false,
|
|
i,
|
|
pushAccesUnit = this.pushAccesUnit.bind(this),
|
|
createAVCSample = function createAVCSample(key, pts, dts, debug) {
|
|
return { key: key, pts: pts, dts: dts, units: [], debug: debug };
|
|
};
|
|
//free pes.data to save up some memory
|
|
pes.data = null;
|
|
|
|
// if new NAL units found and last sample still there, let's push ...
|
|
// this helps parsing streams with missing AUD
|
|
if (avcSample && units.length) {
|
|
pushAccesUnit(avcSample, track);
|
|
avcSample = this.avcSample = createAVCSample(false, pes.pts, pes.dts, '');
|
|
}
|
|
|
|
units.forEach(function (unit) {
|
|
switch (unit.type) {
|
|
//NDR
|
|
case 1:
|
|
push = true;
|
|
if (debug && avcSample) {
|
|
avcSample.debug += 'NDR ';
|
|
}
|
|
avcSample.frame = true;
|
|
var data = unit.data;
|
|
// only check slice type to detect KF in case SPS found in same packet (any keyframe is preceded by SPS ...)
|
|
if (spsfound && data.length > 4) {
|
|
// retrieve slice type by parsing beginning of NAL unit (follow H264 spec, slice_header definition) to detect keyframe embedded in NDR
|
|
var sliceType = new _expGolomb2.default(data).readSliceType();
|
|
// 2 : I slice, 4 : SI slice, 7 : I slice, 9: SI slice
|
|
// SI slice : A slice that is coded using intra prediction only and using quantisation of the prediction samples.
|
|
// An SI slice can be coded such that its decoded samples can be constructed identically to an SP slice.
|
|
// I slice: A slice that is not an SI slice that is decoded using intra prediction only.
|
|
//if (sliceType === 2 || sliceType === 7) {
|
|
if (sliceType === 2 || sliceType === 4 || sliceType === 7 || sliceType === 9) {
|
|
avcSample.key = true;
|
|
}
|
|
}
|
|
break;
|
|
//IDR
|
|
case 5:
|
|
push = true;
|
|
// handle PES not starting with AUD
|
|
if (!avcSample) {
|
|
avcSample = _this.avcSample = createAVCSample(true, pes.pts, pes.dts, '');
|
|
}
|
|
if (debug) {
|
|
avcSample.debug += 'IDR ';
|
|
}
|
|
avcSample.key = true;
|
|
avcSample.frame = true;
|
|
break;
|
|
//SEI
|
|
case 6:
|
|
push = true;
|
|
if (debug && avcSample) {
|
|
avcSample.debug += 'SEI ';
|
|
}
|
|
expGolombDecoder = new _expGolomb2.default(_this.discardEPB(unit.data));
|
|
|
|
// skip frameType
|
|
expGolombDecoder.readUByte();
|
|
|
|
var payloadType = 0;
|
|
var payloadSize = 0;
|
|
var endOfCaptions = false;
|
|
var b = 0;
|
|
|
|
while (!endOfCaptions && expGolombDecoder.bytesAvailable > 1) {
|
|
payloadType = 0;
|
|
do {
|
|
b = expGolombDecoder.readUByte();
|
|
payloadType += b;
|
|
} while (b === 0xFF);
|
|
|
|
// Parse payload size.
|
|
payloadSize = 0;
|
|
do {
|
|
b = expGolombDecoder.readUByte();
|
|
payloadSize += b;
|
|
} while (b === 0xFF);
|
|
|
|
// TODO: there can be more than one payload in an SEI packet...
|
|
// TODO: need to read type and size in a while loop to get them all
|
|
if (payloadType === 4 && expGolombDecoder.bytesAvailable !== 0) {
|
|
|
|
endOfCaptions = true;
|
|
|
|
var countryCode = expGolombDecoder.readUByte();
|
|
|
|
if (countryCode === 181) {
|
|
var providerCode = expGolombDecoder.readUShort();
|
|
|
|
if (providerCode === 49) {
|
|
var userStructure = expGolombDecoder.readUInt();
|
|
|
|
if (userStructure === 0x47413934) {
|
|
var userDataType = expGolombDecoder.readUByte();
|
|
|
|
// Raw CEA-608 bytes wrapped in CEA-708 packet
|
|
if (userDataType === 3) {
|
|
var firstByte = expGolombDecoder.readUByte();
|
|
var secondByte = expGolombDecoder.readUByte();
|
|
|
|
var totalCCs = 31 & firstByte;
|
|
var byteArray = [firstByte, secondByte];
|
|
|
|
for (i = 0; i < totalCCs; i++) {
|
|
// 3 bytes per CC
|
|
byteArray.push(expGolombDecoder.readUByte());
|
|
byteArray.push(expGolombDecoder.readUByte());
|
|
byteArray.push(expGolombDecoder.readUByte());
|
|
}
|
|
|
|
_this._insertSampleInOrder(_this._txtTrack.samples, { type: 3, pts: pes.pts, bytes: byteArray });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if (payloadSize < expGolombDecoder.bytesAvailable) {
|
|
for (i = 0; i < payloadSize; i++) {
|
|
expGolombDecoder.readUByte();
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
//SPS
|
|
case 7:
|
|
push = true;
|
|
spsfound = true;
|
|
if (debug && avcSample) {
|
|
avcSample.debug += 'SPS ';
|
|
}
|
|
if (!track.sps) {
|
|
expGolombDecoder = new _expGolomb2.default(unit.data);
|
|
var config = expGolombDecoder.readSPS();
|
|
track.width = config.width;
|
|
track.height = config.height;
|
|
track.pixelRatio = config.pixelRatio;
|
|
track.sps = [unit.data];
|
|
track.duration = _this._duration;
|
|
var codecarray = unit.data.subarray(1, 4);
|
|
var codecstring = 'avc1.';
|
|
for (i = 0; i < 3; i++) {
|
|
var h = codecarray[i].toString(16);
|
|
if (h.length < 2) {
|
|
h = '0' + h;
|
|
}
|
|
codecstring += h;
|
|
}
|
|
track.codec = codecstring;
|
|
}
|
|
break;
|
|
//PPS
|
|
case 8:
|
|
push = true;
|
|
if (debug && avcSample) {
|
|
avcSample.debug += 'PPS ';
|
|
}
|
|
if (!track.pps) {
|
|
track.pps = [unit.data];
|
|
}
|
|
break;
|
|
// AUD
|
|
case 9:
|
|
push = false;
|
|
if (avcSample) {
|
|
pushAccesUnit(avcSample, track);
|
|
}
|
|
avcSample = _this.avcSample = createAVCSample(false, pes.pts, pes.dts, debug ? 'AUD ' : '');
|
|
break;
|
|
// Filler Data
|
|
case 12:
|
|
push = false;
|
|
break;
|
|
default:
|
|
push = false;
|
|
if (avcSample) {
|
|
avcSample.debug += 'unknown NAL ' + unit.type + ' ';
|
|
}
|
|
break;
|
|
}
|
|
if (avcSample && push) {
|
|
var _units = avcSample.units;
|
|
_units.push(unit);
|
|
}
|
|
});
|
|
// if last PES packet, push samples
|
|
if (last && avcSample) {
|
|
pushAccesUnit(avcSample, track);
|
|
this.avcSample = null;
|
|
}
|
|
}
|
|
}, {
|
|
key: '_insertSampleInOrder',
|
|
value: function _insertSampleInOrder(arr, data) {
|
|
var len = arr.length;
|
|
if (len > 0) {
|
|
if (data.pts >= arr[len - 1].pts) {
|
|
arr.push(data);
|
|
} else {
|
|
for (var pos = len - 1; pos >= 0; pos--) {
|
|
if (data.pts < arr[pos].pts) {
|
|
arr.splice(pos, 0, data);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
arr.push(data);
|
|
}
|
|
}
|
|
}, {
|
|
key: '_getLastNalUnit',
|
|
value: function _getLastNalUnit() {
|
|
var avcSample = this.avcSample,
|
|
lastUnit = void 0;
|
|
// try to fallback to previous sample if current one is empty
|
|
if (!avcSample || avcSample.units.length === 0) {
|
|
var track = this._avcTrack,
|
|
samples = track.samples;
|
|
avcSample = samples[samples.length - 1];
|
|
}
|
|
if (avcSample) {
|
|
var units = avcSample.units;
|
|
lastUnit = units[units.length - 1];
|
|
}
|
|
return lastUnit;
|
|
}
|
|
}, {
|
|
key: '_parseAVCNALu',
|
|
value: function _parseAVCNALu(array) {
|
|
var i = 0,
|
|
len = array.byteLength,
|
|
value,
|
|
overflow,
|
|
track = this._avcTrack,
|
|
state = track.naluState || 0,
|
|
lastState = state;
|
|
var units = [],
|
|
unit,
|
|
unitType,
|
|
lastUnitStart = -1,
|
|
lastUnitType;
|
|
//logger.log('PES:' + Hex.hexDump(array));
|
|
|
|
if (state === -1) {
|
|
// special use case where we found 3 or 4-byte start codes exactly at the end of previous PES packet
|
|
lastUnitStart = 0;
|
|
// NALu type is value read from offset 0
|
|
lastUnitType = array[0] & 0x1f;
|
|
state = 0;
|
|
i = 1;
|
|
}
|
|
|
|
while (i < len) {
|
|
value = array[i++];
|
|
// optimization. state 0 and 1 are the predominant case. let's handle them outside of the switch/case
|
|
if (!state) {
|
|
state = value ? 0 : 1;
|
|
continue;
|
|
}
|
|
if (state === 1) {
|
|
state = value ? 0 : 2;
|
|
continue;
|
|
}
|
|
// here we have state either equal to 2 or 3
|
|
if (!value) {
|
|
state = 3;
|
|
} else if (value === 1) {
|
|
if (lastUnitStart >= 0) {
|
|
unit = { data: array.subarray(lastUnitStart, i - state - 1), type: lastUnitType };
|
|
//logger.log('pushing NALU, type/size:' + unit.type + '/' + unit.data.byteLength);
|
|
units.push(unit);
|
|
} else {
|
|
// lastUnitStart is undefined => this is the first start code found in this PES packet
|
|
// first check if start code delimiter is overlapping between 2 PES packets,
|
|
// ie it started in last packet (lastState not zero)
|
|
// and ended at the beginning of this PES packet (i <= 4 - lastState)
|
|
var lastUnit = this._getLastNalUnit();
|
|
if (lastUnit) {
|
|
if (lastState && i <= 4 - lastState) {
|
|
// start delimiter overlapping between PES packets
|
|
// strip start delimiter bytes from the end of last NAL unit
|
|
// check if lastUnit had a state different from zero
|
|
if (lastUnit.state) {
|
|
// strip last bytes
|
|
lastUnit.data = lastUnit.data.subarray(0, lastUnit.data.byteLength - lastState);
|
|
}
|
|
}
|
|
// If NAL units are not starting right at the beginning of the PES packet, push preceding data into previous NAL unit.
|
|
overflow = i - state - 1;
|
|
if (overflow > 0) {
|
|
//logger.log('first NALU found with overflow:' + overflow);
|
|
var tmp = new Uint8Array(lastUnit.data.byteLength + overflow);
|
|
tmp.set(lastUnit.data, 0);
|
|
tmp.set(array.subarray(0, overflow), lastUnit.data.byteLength);
|
|
lastUnit.data = tmp;
|
|
}
|
|
}
|
|
}
|
|
// check if we can read unit type
|
|
if (i < len) {
|
|
unitType = array[i] & 0x1f;
|
|
//logger.log('find NALU @ offset:' + i + ',type:' + unitType);
|
|
lastUnitStart = i;
|
|
lastUnitType = unitType;
|
|
state = 0;
|
|
} else {
|
|
// not enough byte to read unit type. let's read it on next PES parsing
|
|
state = -1;
|
|
}
|
|
} else {
|
|
state = 0;
|
|
}
|
|
}
|
|
if (lastUnitStart >= 0 && state >= 0) {
|
|
unit = { data: array.subarray(lastUnitStart, len), type: lastUnitType, state: state };
|
|
units.push(unit);
|
|
//logger.log('pushing NALU, type/size/state:' + unit.type + '/' + unit.data.byteLength + '/' + state);
|
|
}
|
|
// no NALu found
|
|
if (units.length === 0) {
|
|
// append pes.data to previous NAL unit
|
|
var _lastUnit = this._getLastNalUnit();
|
|
if (_lastUnit) {
|
|
var _tmp = new Uint8Array(_lastUnit.data.byteLength + array.byteLength);
|
|
_tmp.set(_lastUnit.data, 0);
|
|
_tmp.set(array, _lastUnit.data.byteLength);
|
|
_lastUnit.data = _tmp;
|
|
}
|
|
}
|
|
track.naluState = state;
|
|
return units;
|
|
}
|
|
|
|
/**
|
|
* remove Emulation Prevention bytes from a RBSP
|
|
*/
|
|
|
|
}, {
|
|
key: 'discardEPB',
|
|
value: function discardEPB(data) {
|
|
var length = data.byteLength,
|
|
EPBPositions = [],
|
|
i = 1,
|
|
newLength,
|
|
newData;
|
|
|
|
// Find all `Emulation Prevention Bytes`
|
|
while (i < length - 2) {
|
|
if (data[i] === 0 && data[i + 1] === 0 && data[i + 2] === 0x03) {
|
|
EPBPositions.push(i + 2);
|
|
i += 2;
|
|
} else {
|
|
i++;
|
|
}
|
|
}
|
|
|
|
// If no Emulation Prevention Bytes were found just return the original
|
|
// array
|
|
if (EPBPositions.length === 0) {
|
|
return data;
|
|
}
|
|
|
|
// Create a new array to hold the NAL unit data
|
|
newLength = length - EPBPositions.length;
|
|
newData = new Uint8Array(newLength);
|
|
var sourceIndex = 0;
|
|
|
|
for (i = 0; i < newLength; sourceIndex++, i++) {
|
|
if (sourceIndex === EPBPositions[0]) {
|
|
// Skip this byte
|
|
sourceIndex++;
|
|
// Remove this position index
|
|
EPBPositions.shift();
|
|
}
|
|
newData[i] = data[sourceIndex];
|
|
}
|
|
return newData;
|
|
}
|
|
}, {
|
|
key: '_parseAACPES',
|
|
value: function _parseAACPES(pes) {
|
|
var track = this._audioTrack,
|
|
data = pes.data,
|
|
pts = pes.pts,
|
|
startOffset = 0,
|
|
aacOverFlow = this.aacOverFlow,
|
|
aacLastPTS = this.aacLastPTS,
|
|
frameDuration,
|
|
frameIndex,
|
|
offset,
|
|
stamp,
|
|
len;
|
|
if (aacOverFlow) {
|
|
var tmp = new Uint8Array(aacOverFlow.byteLength + data.byteLength);
|
|
tmp.set(aacOverFlow, 0);
|
|
tmp.set(data, aacOverFlow.byteLength);
|
|
//logger.log(`AAC: append overflowing ${aacOverFlow.byteLength} bytes to beginning of new PES`);
|
|
data = tmp;
|
|
}
|
|
// look for ADTS header (0xFFFx)
|
|
for (offset = startOffset, len = data.length; offset < len - 1; offset++) {
|
|
if (_adts2.default.isHeader(data, offset)) {
|
|
break;
|
|
}
|
|
}
|
|
// if ADTS header does not start straight from the beginning of the PES payload, raise an error
|
|
if (offset) {
|
|
var reason, fatal;
|
|
if (offset < len - 1) {
|
|
reason = 'AAC PES did not start with ADTS header,offset:' + offset;
|
|
fatal = false;
|
|
} else {
|
|
reason = 'no ADTS header found in AAC PES';
|
|
fatal = true;
|
|
}
|
|
_logger.logger.warn('parsing error:' + reason);
|
|
this.observer.trigger(_events2.default.ERROR, { type: _errors.ErrorTypes.MEDIA_ERROR, details: _errors.ErrorDetails.FRAG_PARSING_ERROR, fatal: fatal, reason: reason });
|
|
if (fatal) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
_adts2.default.initTrackConfig(track, this.observer, data, offset, this.audioCodec);
|
|
frameIndex = 0;
|
|
frameDuration = _adts2.default.getFrameDuration(track.samplerate);
|
|
|
|
// if last AAC frame is overflowing, we should ensure timestamps are contiguous:
|
|
// first sample PTS should be equal to last sample PTS + frameDuration
|
|
if (aacOverFlow && aacLastPTS) {
|
|
var newPTS = aacLastPTS + frameDuration;
|
|
if (Math.abs(newPTS - pts) > 1) {
|
|
_logger.logger.log('AAC: align PTS for overlapping frames by ' + Math.round((newPTS - pts) / 90));
|
|
pts = newPTS;
|
|
}
|
|
}
|
|
|
|
//scan for aac samples
|
|
while (offset < len) {
|
|
if (_adts2.default.isHeader(data, offset) && offset + 5 < len) {
|
|
var frame = _adts2.default.appendFrame(track, data, offset, pts, frameIndex);
|
|
if (frame) {
|
|
//logger.log(`${Math.round(frame.sample.pts)} : AAC`);
|
|
offset += frame.length;
|
|
stamp = frame.sample.pts;
|
|
frameIndex++;
|
|
} else {
|
|
//logger.log('Unable to parse AAC frame');
|
|
break;
|
|
}
|
|
} else {
|
|
//nothing found, keep looking
|
|
offset++;
|
|
}
|
|
}
|
|
|
|
if (offset < len) {
|
|
aacOverFlow = data.subarray(offset, len);
|
|
//logger.log(`AAC: overflow detected:${len-offset}`);
|
|
} else {
|
|
aacOverFlow = null;
|
|
}
|
|
this.aacOverFlow = aacOverFlow;
|
|
this.aacLastPTS = stamp;
|
|
}
|
|
}, {
|
|
key: '_parseMPEGPES',
|
|
value: function _parseMPEGPES(pes) {
|
|
var data = pes.data;
|
|
var length = data.length;
|
|
var frameIndex = 0;
|
|
var offset = 0;
|
|
var pts = pes.pts;
|
|
|
|
while (offset < length) {
|
|
if (_mpegaudio2.default.isHeader(data, offset)) {
|
|
var frame = _mpegaudio2.default.appendFrame(this._audioTrack, data, offset, pts, frameIndex);
|
|
if (frame) {
|
|
offset += frame.length;
|
|
frameIndex++;
|
|
} else {
|
|
//logger.log('Unable to parse Mpeg audio frame');
|
|
break;
|
|
}
|
|
} else {
|
|
//nothing found, keep looking
|
|
offset++;
|
|
}
|
|
}
|
|
}
|
|
}, {
|
|
key: '_parseID3PES',
|
|
value: function _parseID3PES(pes) {
|
|
this._id3Track.samples.push(pes);
|
|
}
|
|
}], [{
|
|
key: 'probe',
|
|
value: function probe(data) {
|
|
// a TS fragment should contain at least 3 TS packets, a PAT, a PMT, and one PID, each starting with 0x47
|
|
if (data.length >= 3 * 188 && data[0] === 0x47 && data[188] === 0x47 && data[2 * 188] === 0x47) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
}]);
|
|
|
|
return TSDemuxer;
|
|
}();
|
|
|
|
exports.default = TSDemuxer; |