mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
update components
This commit is contained in:
parent
2c580f4f73
commit
3578670ef6
37 changed files with 1463 additions and 1145 deletions
|
@ -15,11 +15,12 @@ const State = {
|
|||
IDLE : 0,
|
||||
KEY_LOADING : 1,
|
||||
FRAG_LOADING : 2,
|
||||
WAITING_LEVEL : 3,
|
||||
PARSING : 4,
|
||||
PARSED : 5,
|
||||
APPENDING : 6,
|
||||
BUFFER_FLUSHING : 7
|
||||
FRAG_LOADING_WAITING_RETRY : 3,
|
||||
WAITING_LEVEL : 4,
|
||||
PARSING : 5,
|
||||
PARSED : 6,
|
||||
APPENDING : 7,
|
||||
BUFFER_FLUSHING : 8
|
||||
};
|
||||
|
||||
class MSEMediaController {
|
||||
|
@ -28,6 +29,7 @@ class MSEMediaController {
|
|||
this.config = hls.config;
|
||||
this.audioCodecSwap = false;
|
||||
this.hls = hls;
|
||||
this.ticks = 0;
|
||||
// Source Buffer listeners
|
||||
this.onsbue = this.onSBUpdateEnd.bind(this);
|
||||
this.onsbe = this.onSBUpdateError.bind(this);
|
||||
|
@ -84,6 +86,7 @@ class MSEMediaController {
|
|||
this.demuxer = new Demuxer(hls);
|
||||
this.timer = setInterval(this.ontick, 100);
|
||||
this.level = -1;
|
||||
this.fragLoadError = 0;
|
||||
hls.on(Event.FRAG_LOADED, this.onfl);
|
||||
hls.on(Event.FRAG_PARSING_INIT_SEGMENT, this.onis);
|
||||
hls.on(Event.FRAG_PARSING_DATA, this.onfpg);
|
||||
|
@ -136,6 +139,17 @@ class MSEMediaController {
|
|||
}
|
||||
|
||||
tick() {
|
||||
this.ticks++;
|
||||
if (this.ticks === 1) {
|
||||
this.doTick();
|
||||
if (this.ticks > 1) {
|
||||
setTimeout(this.tick, 1);
|
||||
}
|
||||
this.ticks = 0;
|
||||
}
|
||||
}
|
||||
|
||||
doTick() {
|
||||
var pos, level, levelDetails, hls = this.hls;
|
||||
switch(this.state) {
|
||||
case State.ERROR:
|
||||
|
@ -367,6 +381,17 @@ class MSEMediaController {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case State.FRAG_LOADING_WAITING_RETRY:
|
||||
var now = performance.now();
|
||||
var retryDate = this.retryDate;
|
||||
var media = this.media;
|
||||
var isSeeking = media && media.seeking;
|
||||
// if current time is gt than retryDate, or if media seeking let's switch to IDLE state to retry loading
|
||||
if(!retryDate || (now >= retryDate) || isSeeking) {
|
||||
logger.log(`mediaController: retryDate reached, switch back to IDLE state`);
|
||||
this.state = State.IDLE;
|
||||
}
|
||||
break;
|
||||
case State.PARSING:
|
||||
// nothing to do, wait for fragment being parsed
|
||||
break;
|
||||
|
@ -454,10 +479,10 @@ class MSEMediaController {
|
|||
default:
|
||||
break;
|
||||
}
|
||||
// check/update current fragment
|
||||
this._checkFragmentChanged();
|
||||
// check buffer
|
||||
this._checkBuffer();
|
||||
// check/update current fragment
|
||||
this._checkFragmentChanged();
|
||||
}
|
||||
|
||||
|
||||
|
@ -876,8 +901,12 @@ class MSEMediaController {
|
|||
}
|
||||
|
||||
onMediaMetadata() {
|
||||
if (this.media.currentTime !== this.startPosition) {
|
||||
this.media.currentTime = this.startPosition;
|
||||
var media = this.media,
|
||||
currentTime = media.currentTime;
|
||||
// only adjust currentTime if not equal to 0
|
||||
if (!currentTime && currentTime !== this.startPosition) {
|
||||
logger.log('onMediaMetadata: adjust currentTime to startPosition');
|
||||
media.currentTime = this.startPosition;
|
||||
}
|
||||
this.loadedmetadata = true;
|
||||
this.tick();
|
||||
|
@ -992,8 +1021,11 @@ class MSEMediaController {
|
|||
level = fragCurrent.level,
|
||||
sn = fragCurrent.sn,
|
||||
audioCodec = currentLevel.audioCodec;
|
||||
if(audioCodec && this.audioCodecSwap) {
|
||||
if(this.audioCodecSwap) {
|
||||
logger.log('swapping playlist audio codec');
|
||||
if(audioCodec === undefined) {
|
||||
audioCodec = this.lastAudioCodec;
|
||||
}
|
||||
if(audioCodec.indexOf('mp4a.40.5') !==-1) {
|
||||
audioCodec = 'mp4a.40.2';
|
||||
} else {
|
||||
|
@ -1012,6 +1044,7 @@ class MSEMediaController {
|
|||
// check if codecs have been explicitely defined in the master playlist for this level;
|
||||
// if yes use these ones instead of the ones parsed from the demux
|
||||
var audioCodec = this.levels[this.level].audioCodec, videoCodec = this.levels[this.level].videoCodec, sb;
|
||||
this.lastAudioCodec = data.audioCodec;
|
||||
if(audioCodec && this.audioCodecSwap) {
|
||||
logger.log('swapping playlist audio codec');
|
||||
if(audioCodec.indexOf('mp4a.40.5') !==-1) {
|
||||
|
@ -1070,7 +1103,7 @@ class MSEMediaController {
|
|||
this.tparse2 = Date.now();
|
||||
var level = this.levels[this.level],
|
||||
frag = this.fragCurrent;
|
||||
logger.log(`parsed data, type/startPTS/endPTS/startDTS/endDTS/nb:${data.type}/${data.startPTS.toFixed(3)}/${data.endPTS.toFixed(3)}/${data.startDTS.toFixed(3)}/${data.endDTS.toFixed(3)}/${data.nb}`);
|
||||
logger.log(`parsed ${data.type},PTS:[${data.startPTS.toFixed(3)},${data.endPTS.toFixed(3)}],DTS:[${data.startDTS.toFixed(3)}/${data.endDTS.toFixed(3)}],nb:${data.nb}`);
|
||||
var drift = LevelHelper.updateFragPTS(level.details,frag.sn,data.startPTS,data.endPTS);
|
||||
this.hls.trigger(Event.LEVEL_PTS_UPDATED, {details: level.details, level: this.level, drift: drift});
|
||||
|
||||
|
@ -1097,25 +1130,32 @@ class MSEMediaController {
|
|||
|
||||
onError(event, data) {
|
||||
switch(data.details) {
|
||||
// abort fragment loading on errors
|
||||
case ErrorDetails.FRAG_LOAD_ERROR:
|
||||
case ErrorDetails.FRAG_LOAD_TIMEOUT:
|
||||
var loadError = this.fragLoadError;
|
||||
if(loadError) {
|
||||
loadError++;
|
||||
} else {
|
||||
loadError=1;
|
||||
}
|
||||
if (loadError <= this.config.fragLoadingMaxRetry) {
|
||||
this.fragLoadError = loadError;
|
||||
// retry loading
|
||||
this.state = State.IDLE;
|
||||
} else {
|
||||
logger.error(`mediaController: ${data.details} reaches max retry, redispatch as fatal ...`);
|
||||
// redispatch same error but with fatal set to true
|
||||
data.fatal = true;
|
||||
this.hls.trigger(event, data);
|
||||
this.state = State.ERROR;
|
||||
if(!data.fatal) {
|
||||
var loadError = this.fragLoadError;
|
||||
if(loadError) {
|
||||
loadError++;
|
||||
} else {
|
||||
loadError=1;
|
||||
}
|
||||
if (loadError <= this.config.fragLoadingMaxRetry) {
|
||||
this.fragLoadError = loadError;
|
||||
// reset load counter to avoid frag loop loading error
|
||||
data.frag.loadCounter = 0;
|
||||
// exponential backoff capped to 64s
|
||||
var delay = Math.min(Math.pow(2,loadError-1)*this.config.fragLoadingRetryDelay,64000);
|
||||
logger.warn(`mediaController: frag loading failed, retry in ${delay} ms`);
|
||||
this.retryDate = performance.now() + delay;
|
||||
// retry loading state
|
||||
this.state = State.FRAG_LOADING_WAITING_RETRY;
|
||||
} else {
|
||||
logger.error(`mediaController: ${data.details} reaches max retry, redispatch as fatal ...`);
|
||||
// redispatch same error but with fatal set to true
|
||||
data.fatal = true;
|
||||
this.hls.trigger(event, data);
|
||||
this.state = State.ERROR;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ErrorDetails.FRAG_LOOP_LOADING_ERROR:
|
||||
|
@ -1162,24 +1202,35 @@ _checkBuffer() {
|
|||
media.currentTime = seekAfterBuffered;
|
||||
this.seekAfterBuffered = undefined;
|
||||
}
|
||||
} else if(readyState < 3 ) {
|
||||
// readyState = 1 or 2
|
||||
// HAVE_METADATA (numeric value 1) Enough of the resource has been obtained that the duration of the resource is available.
|
||||
// The API will no longer throw an exception when seeking.
|
||||
// HAVE_CURRENT_DATA (numeric value 2) Data for the immediate current playback position is available,
|
||||
// but either not enough data is available that the user agent could
|
||||
// successfully advance the current playback position
|
||||
var currentTime = media.currentTime;
|
||||
var bufferInfo = this.bufferInfo(currentTime,0);
|
||||
// check if current time is buffered or not
|
||||
if(bufferInfo.len === 0) {
|
||||
// no buffer available @ currentTime, check if next buffer is close (in a 300 ms range)
|
||||
var nextBufferStart = bufferInfo.nextStart;
|
||||
if(nextBufferStart && (nextBufferStart - currentTime < 0.3)) {
|
||||
// next buffer is close ! adjust currentTime to nextBufferStart
|
||||
// this will ensure effective video decoding
|
||||
logger.log(`adjust currentTime from ${currentTime} to ${nextBufferStart}`);
|
||||
media.currentTime = nextBufferStart;
|
||||
} else {
|
||||
var currentTime = media.currentTime,
|
||||
bufferInfo = this.bufferInfo(currentTime,0),
|
||||
isPlaying = !(media.paused || media.ended || media.seeking || readyState < 3),
|
||||
jumpThreshold = 0.2;
|
||||
|
||||
// check buffer upfront
|
||||
// if less than 200ms is buffered, and media is playing but playhead is not moving,
|
||||
// and we have a new buffer range available upfront, let's seek to that one
|
||||
if(bufferInfo.len <= jumpThreshold) {
|
||||
if(currentTime > media.playbackRate*this.lastCurrentTime || !isPlaying) {
|
||||
// playhead moving or media not playing
|
||||
jumpThreshold = 0;
|
||||
} else {
|
||||
logger.trace('playback seems stuck');
|
||||
}
|
||||
// if we are below threshold, try to jump if next buffer range is close
|
||||
if(bufferInfo.len <= jumpThreshold) {
|
||||
// no buffer available @ currentTime, check if next buffer is close (more than 5ms diff but within a 300 ms range)
|
||||
var nextBufferStart = bufferInfo.nextStart, delta = nextBufferStart-currentTime;
|
||||
if(nextBufferStart &&
|
||||
(delta < 0.3) &&
|
||||
(delta > 0.005) &&
|
||||
!media.seeking) {
|
||||
// next buffer is close ! adjust currentTime to nextBufferStart
|
||||
// this will ensure effective video decoding
|
||||
logger.log(`adjust currentTime from ${currentTime} to ${nextBufferStart}`);
|
||||
media.currentTime = nextBufferStart;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,7 +130,7 @@ class AES128Decrypter {
|
|||
return decrypted;
|
||||
}
|
||||
|
||||
localDecript(encrypted, key, initVector, decrypted) {
|
||||
localDecrypt(encrypted, key, initVector, decrypted) {
|
||||
var bytes = this.doDecrypt(encrypted,
|
||||
key,
|
||||
initVector);
|
||||
|
@ -148,7 +148,7 @@ class AES128Decrypter {
|
|||
// split up the encryption job and do the individual chunks asynchronously
|
||||
var key = this.key;
|
||||
var initVector = this.iv;
|
||||
this.localDecript(encrypted32.subarray(i, i + step), key, initVector, decrypted);
|
||||
this.localDecrypt(encrypted32.subarray(i, i + step), key, initVector, decrypted);
|
||||
|
||||
for (i = step; i < encrypted32.length; i += step) {
|
||||
initVector = new Uint32Array([
|
||||
|
@ -157,7 +157,7 @@ class AES128Decrypter {
|
|||
this.ntoh(encrypted32[i - 2]),
|
||||
this.ntoh(encrypted32[i - 1])
|
||||
]);
|
||||
this.localDecript(encrypted32.subarray(i, i + step), key, initVector, decrypted);
|
||||
this.localDecrypt(encrypted32.subarray(i, i + step), key, initVector, decrypted);
|
||||
}
|
||||
|
||||
return decrypted;
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/**
|
||||
* AAC demuxer
|
||||
*/
|
||||
import ADTS from './adts';
|
||||
import {logger} from '../utils/logger';
|
||||
import ID3 from '../demux/id3';
|
||||
import {ErrorTypes, ErrorDetails} from '../errors';
|
||||
|
||||
class AACDemuxer {
|
||||
|
||||
|
@ -32,7 +32,10 @@ import {ErrorTypes, ErrorDetails} from '../errors';
|
|||
|
||||
// feed incoming data to the front of the parsing pipeline
|
||||
push(data, audioCodec, videoCodec, timeOffset, cc, level, sn, duration) {
|
||||
var id3 = new ID3(data), adtsStartOffset,len, track = this._aacTrack, pts = id3.timeStamp, config, nbSamples,adtsFrameSize,adtsHeaderLen,stamp,aacSample;
|
||||
var track = this._aacTrack,
|
||||
id3 = new ID3(data),
|
||||
pts = 90*id3.timeStamp,
|
||||
config, adtsFrameSize, adtsStartOffset, adtsHeaderLen, stamp, nbSamples, len, aacSample;
|
||||
// look for ADTS header (0xFFFx)
|
||||
for (adtsStartOffset = id3.length, len = data.length; adtsStartOffset < len - 1; adtsStartOffset++) {
|
||||
if ((data[adtsStartOffset] === 0xff) && (data[adtsStartOffset+1] & 0xf0) === 0xf0) {
|
||||
|
@ -41,7 +44,7 @@ import {ErrorTypes, ErrorDetails} from '../errors';
|
|||
}
|
||||
|
||||
if (!track.audiosamplerate) {
|
||||
config = this._ADTStoAudioConfig(data, adtsStartOffset, audioCodec);
|
||||
config = ADTS.getAudioConfig(this.observer,data, adtsStartOffset, audioCodec);
|
||||
track.config = config.config;
|
||||
track.audiosamplerate = config.samplerate;
|
||||
track.channelCount = config.channelCount;
|
||||
|
@ -60,7 +63,7 @@ import {ErrorTypes, ErrorDetails} from '../errors';
|
|||
adtsFrameSize |= ((data[adtsStartOffset + 5] & 0xE0) >>> 5);
|
||||
adtsHeaderLen = (!!(data[adtsStartOffset + 1] & 0x01) ? 7 : 9);
|
||||
adtsFrameSize -= adtsHeaderLen;
|
||||
stamp = Math.round(90*pts + nbSamples * 1024 * 90000 / track.audiosamplerate);
|
||||
stamp = Math.round(pts + nbSamples * 1024 * 90000 / track.audiosamplerate);
|
||||
//stamp = pes.pts;
|
||||
//console.log('AAC frame, offset/length/pts:' + (adtsStartOffset+7) + '/' + adtsFrameSize + '/' + stamp.toFixed(0));
|
||||
if ((adtsFrameSize > 0) && ((adtsStartOffset + adtsHeaderLen + adtsFrameSize) <= len)) {
|
||||
|
@ -82,124 +85,6 @@ import {ErrorTypes, ErrorDetails} from '../errors';
|
|||
this.remuxer.remux(this._aacTrack,{samples : []}, {samples : [ { pts: pts, dts : pts, unit : id3.payload} ]}, timeOffset);
|
||||
}
|
||||
|
||||
_ADTStoAudioConfig(data, offset, audioCodec) {
|
||||
var adtsObjectType, // :int
|
||||
adtsSampleingIndex, // :int
|
||||
adtsExtensionSampleingIndex, // :int
|
||||
adtsChanelConfig, // :int
|
||||
config,
|
||||
userAgent = navigator.userAgent.toLowerCase(),
|
||||
adtsSampleingRates = [
|
||||
96000, 88200,
|
||||
64000, 48000,
|
||||
44100, 32000,
|
||||
24000, 22050,
|
||||
16000, 12000,
|
||||
11025, 8000,
|
||||
7350];
|
||||
// byte 2
|
||||
adtsObjectType = ((data[offset + 2] & 0xC0) >>> 6) + 1;
|
||||
adtsSampleingIndex = ((data[offset + 2] & 0x3C) >>> 2);
|
||||
if(adtsSampleingIndex > adtsSampleingRates.length-1) {
|
||||
this.observer.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.FRAG_PARSING_ERROR, fatal: true, reason: `invalid ADTS sampling index:${adtsSampleingIndex}`});
|
||||
return;
|
||||
}
|
||||
adtsChanelConfig = ((data[offset + 2] & 0x01) << 2);
|
||||
// byte 3
|
||||
adtsChanelConfig |= ((data[offset + 3] & 0xC0) >>> 6);
|
||||
logger.log(`manifest codec:${audioCodec},ADTS data:type:${adtsObjectType},sampleingIndex:${adtsSampleingIndex}[${adtsSampleingRates[adtsSampleingIndex]}Hz],channelConfig:${adtsChanelConfig}`);
|
||||
// firefox: freq less than 24kHz = AAC SBR (HE-AAC)
|
||||
if (userAgent.indexOf('firefox') !== -1) {
|
||||
if (adtsSampleingIndex >= 6) {
|
||||
adtsObjectType = 5;
|
||||
config = new Array(4);
|
||||
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
||||
// there is a factor 2 between frame sample rate and output sample rate
|
||||
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex - 3;
|
||||
} else {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
||||
}
|
||||
// Android : always use AAC
|
||||
} else if (userAgent.indexOf('android') !== -1) {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
||||
} else {
|
||||
/* for other browsers (chrome ...)
|
||||
always force audio type to be HE-AAC SBR, as some browsers do not support audio codec switch properly (like Chrome ...)
|
||||
*/
|
||||
adtsObjectType = 5;
|
||||
config = new Array(4);
|
||||
// if (manifest codec is HE-AAC) OR (manifest codec not specified AND frequency less than 24kHz)
|
||||
if ((audioCodec && audioCodec.indexOf('mp4a.40.5') !== -1) || (!audioCodec && adtsSampleingIndex >= 6)) {
|
||||
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
||||
// there is a factor 2 between frame sample rate and output sample rate
|
||||
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex - 3;
|
||||
} else {
|
||||
// if (manifest codec is AAC) AND (frequency less than 24kHz OR nb channel is 1)
|
||||
if (audioCodec && audioCodec.indexOf('mp4a.40.2') !== -1 && (adtsSampleingIndex >= 6 || adtsChanelConfig === 1)) {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
}
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
||||
}
|
||||
}
|
||||
/* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config
|
||||
ISO 14496-3 (AAC).pdf - Table 1.13 — Syntax of AudioSpecificConfig()
|
||||
Audio Profile / Audio Object Type
|
||||
0: Null
|
||||
1: AAC Main
|
||||
2: AAC LC (Low Complexity)
|
||||
3: AAC SSR (Scalable Sample Rate)
|
||||
4: AAC LTP (Long Term Prediction)
|
||||
5: SBR (Spectral Band Replication)
|
||||
6: AAC Scalable
|
||||
sampling freq
|
||||
0: 96000 Hz
|
||||
1: 88200 Hz
|
||||
2: 64000 Hz
|
||||
3: 48000 Hz
|
||||
4: 44100 Hz
|
||||
5: 32000 Hz
|
||||
6: 24000 Hz
|
||||
7: 22050 Hz
|
||||
8: 16000 Hz
|
||||
9: 12000 Hz
|
||||
10: 11025 Hz
|
||||
11: 8000 Hz
|
||||
12: 7350 Hz
|
||||
13: Reserved
|
||||
14: Reserved
|
||||
15: frequency is written explictly
|
||||
Channel Configurations
|
||||
These are the channel configurations:
|
||||
0: Defined in AOT Specifc Config
|
||||
1: 1 channel: front-center
|
||||
2: 2 channels: front-left, front-right
|
||||
*/
|
||||
// audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1
|
||||
config[0] = adtsObjectType << 3;
|
||||
// samplingFrequencyIndex
|
||||
config[0] |= (adtsSampleingIndex & 0x0E) >> 1;
|
||||
config[1] |= (adtsSampleingIndex & 0x01) << 7;
|
||||
// channelConfiguration
|
||||
config[1] |= adtsChanelConfig << 3;
|
||||
if (adtsObjectType === 5) {
|
||||
// adtsExtensionSampleingIndex
|
||||
config[1] |= (adtsExtensionSampleingIndex & 0x0E) >> 1;
|
||||
config[2] = (adtsExtensionSampleingIndex & 0x01) << 7;
|
||||
// adtsObjectType (force to 2, chrome is checking that object type is less than 5 ???
|
||||
// https://chromium.googlesource.com/chromium/src.git/+/master/media/formats/mp4/aac.cc
|
||||
config[2] |= 2 << 2;
|
||||
config[3] = 0;
|
||||
}
|
||||
return {config: config, samplerate: adtsSampleingRates[adtsSampleingIndex], channelCount: adtsChanelConfig, codec: ('mp4a.40.' + adtsObjectType)};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
}
|
||||
|
||||
|
|
132
dashboard-ui/bower_components/hls.js/src/demux/adts.js
vendored
Normal file
132
dashboard-ui/bower_components/hls.js/src/demux/adts.js
vendored
Normal file
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* ADTS parser helper
|
||||
*/
|
||||
import {logger} from '../utils/logger';
|
||||
import {ErrorTypes, ErrorDetails} from '../errors';
|
||||
|
||||
class ADTS {
|
||||
|
||||
static getAudioConfig(observer, data, offset, audioCodec) {
|
||||
var adtsObjectType, // :int
|
||||
adtsSampleingIndex, // :int
|
||||
adtsExtensionSampleingIndex, // :int
|
||||
adtsChanelConfig, // :int
|
||||
config,
|
||||
userAgent = navigator.userAgent.toLowerCase(),
|
||||
adtsSampleingRates = [
|
||||
96000, 88200,
|
||||
64000, 48000,
|
||||
44100, 32000,
|
||||
24000, 22050,
|
||||
16000, 12000,
|
||||
11025, 8000,
|
||||
7350];
|
||||
// byte 2
|
||||
adtsObjectType = ((data[offset + 2] & 0xC0) >>> 6) + 1;
|
||||
adtsSampleingIndex = ((data[offset + 2] & 0x3C) >>> 2);
|
||||
if(adtsSampleingIndex > adtsSampleingRates.length-1) {
|
||||
observer.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.FRAG_PARSING_ERROR, fatal: true, reason: `invalid ADTS sampling index:${adtsSampleingIndex}`});
|
||||
return;
|
||||
}
|
||||
adtsChanelConfig = ((data[offset + 2] & 0x01) << 2);
|
||||
// byte 3
|
||||
adtsChanelConfig |= ((data[offset + 3] & 0xC0) >>> 6);
|
||||
logger.log(`manifest codec:${audioCodec},ADTS data:type:${adtsObjectType},sampleingIndex:${adtsSampleingIndex}[${adtsSampleingRates[adtsSampleingIndex]}Hz],channelConfig:${adtsChanelConfig}`);
|
||||
// firefox: freq less than 24kHz = AAC SBR (HE-AAC)
|
||||
if (userAgent.indexOf('firefox') !== -1) {
|
||||
if (adtsSampleingIndex >= 6) {
|
||||
adtsObjectType = 5;
|
||||
config = new Array(4);
|
||||
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
||||
// there is a factor 2 between frame sample rate and output sample rate
|
||||
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex - 3;
|
||||
} else {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
||||
}
|
||||
// Android : always use AAC
|
||||
} else if (userAgent.indexOf('android') !== -1) {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
||||
} else {
|
||||
/* for other browsers (chrome ...)
|
||||
always force audio type to be HE-AAC SBR, as some browsers do not support audio codec switch properly (like Chrome ...)
|
||||
*/
|
||||
adtsObjectType = 5;
|
||||
config = new Array(4);
|
||||
// if (manifest codec is HE-AAC or HE-AACv2) OR (manifest codec not specified AND frequency less than 24kHz)
|
||||
if ((audioCodec && ((audioCodec.indexOf('mp4a.40.29') !== -1) ||
|
||||
(audioCodec.indexOf('mp4a.40.5') !== -1))) ||
|
||||
(!audioCodec && adtsSampleingIndex >= 6)) {
|
||||
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
||||
// there is a factor 2 between frame sample rate and output sample rate
|
||||
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex - 3;
|
||||
} else {
|
||||
// if (manifest codec is AAC) AND (frequency less than 24kHz OR nb channel is 1) OR (manifest codec not specified and mono audio)
|
||||
// Chrome fails to play back with AAC LC mono when initialized with HE-AAC. This is not a problem with stereo.
|
||||
if (audioCodec && audioCodec.indexOf('mp4a.40.2') !== -1 && (adtsSampleingIndex >= 6 || adtsChanelConfig === 1) ||
|
||||
(!audioCodec && adtsChanelConfig === 1)) {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
}
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
||||
}
|
||||
}
|
||||
/* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config
|
||||
ISO 14496-3 (AAC).pdf - Table 1.13 — Syntax of AudioSpecificConfig()
|
||||
Audio Profile / Audio Object Type
|
||||
0: Null
|
||||
1: AAC Main
|
||||
2: AAC LC (Low Complexity)
|
||||
3: AAC SSR (Scalable Sample Rate)
|
||||
4: AAC LTP (Long Term Prediction)
|
||||
5: SBR (Spectral Band Replication)
|
||||
6: AAC Scalable
|
||||
sampling freq
|
||||
0: 96000 Hz
|
||||
1: 88200 Hz
|
||||
2: 64000 Hz
|
||||
3: 48000 Hz
|
||||
4: 44100 Hz
|
||||
5: 32000 Hz
|
||||
6: 24000 Hz
|
||||
7: 22050 Hz
|
||||
8: 16000 Hz
|
||||
9: 12000 Hz
|
||||
10: 11025 Hz
|
||||
11: 8000 Hz
|
||||
12: 7350 Hz
|
||||
13: Reserved
|
||||
14: Reserved
|
||||
15: frequency is written explictly
|
||||
Channel Configurations
|
||||
These are the channel configurations:
|
||||
0: Defined in AOT Specifc Config
|
||||
1: 1 channel: front-center
|
||||
2: 2 channels: front-left, front-right
|
||||
*/
|
||||
// audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1
|
||||
config[0] = adtsObjectType << 3;
|
||||
// samplingFrequencyIndex
|
||||
config[0] |= (adtsSampleingIndex & 0x0E) >> 1;
|
||||
config[1] |= (adtsSampleingIndex & 0x01) << 7;
|
||||
// channelConfiguration
|
||||
config[1] |= adtsChanelConfig << 3;
|
||||
if (adtsObjectType === 5) {
|
||||
// adtsExtensionSampleingIndex
|
||||
config[1] |= (adtsExtensionSampleingIndex & 0x0E) >> 1;
|
||||
config[2] = (adtsExtensionSampleingIndex & 0x01) << 7;
|
||||
// adtsObjectType (force to 2, chrome is checking that object type is less than 5 ???
|
||||
// https://chromium.googlesource.com/chromium/src.git/+/master/media/formats/mp4/aac.cc
|
||||
config[2] |= 2 << 2;
|
||||
config[3] = 0;
|
||||
}
|
||||
return {config: config, samplerate: adtsSampleingRates[adtsSampleingIndex], channelCount: adtsChanelConfig, codec: ('mp4a.40.' + adtsObjectType)};
|
||||
}
|
||||
}
|
||||
|
||||
export default ADTS;
|
|
@ -179,7 +179,12 @@ class ExpGolomb {
|
|||
if (profileIdc === 100 ||
|
||||
profileIdc === 110 ||
|
||||
profileIdc === 122 ||
|
||||
profileIdc === 144) {
|
||||
profileIdc === 244 ||
|
||||
profileIdc === 44 ||
|
||||
profileIdc === 83 ||
|
||||
profileIdc === 86 ||
|
||||
profileIdc === 118 ||
|
||||
profileIdc === 128) {
|
||||
var chromaFormatIdc = this.readUEG();
|
||||
if (chromaFormatIdc === 3) {
|
||||
this.skipBits(1); // separate_colour_plane_flag
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
* upon discontinuity or level switch detection, it will also notifies the remuxer so that it can reset its state.
|
||||
*/
|
||||
|
||||
import ADTS from './adts';
|
||||
import Event from '../events';
|
||||
import ExpGolomb from './exp-golomb';
|
||||
// import Hex from '../utils/hex';
|
||||
|
@ -21,7 +22,6 @@
|
|||
this.observer = observer;
|
||||
this.remuxerClass = remuxerClass;
|
||||
this.lastCC = 0;
|
||||
this.PES_TIMESCALE = 90000;
|
||||
this.remuxer = new this.remuxerClass(observer);
|
||||
}
|
||||
|
||||
|
@ -423,16 +423,19 @@
|
|||
// 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) {
|
||||
var track = this._avcTrack,
|
||||
samples = track.samples;
|
||||
//logger.log('first NALU found with overflow:' + overflow);
|
||||
if (this._avcTrack.samples.length) {
|
||||
var lastavcSample = this._avcTrack.samples[this._avcTrack.samples.length - 1];
|
||||
var lastUnit = lastavcSample.units.units[lastavcSample.units.units.length - 1];
|
||||
var tmp = new Uint8Array(lastUnit.data.byteLength + overflow);
|
||||
if (samples.length) {
|
||||
var lastavcSample = samples[samples.length - 1],
|
||||
lastUnits = lastavcSample.units.units,
|
||||
lastUnit = lastUnits[lastUnits.length - 1],
|
||||
tmp = new Uint8Array(lastUnit.data.byteLength + overflow);
|
||||
tmp.set(lastUnit.data, 0);
|
||||
tmp.set(array.subarray(0, overflow), lastUnit.data.byteLength);
|
||||
lastUnit.data = tmp;
|
||||
lastavcSample.units.length += overflow;
|
||||
this._avcTrack.len += overflow;
|
||||
track.len += overflow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -460,7 +463,13 @@
|
|||
}
|
||||
|
||||
_parseAACPES(pes) {
|
||||
var track = this._aacTrack, aacSample, data = pes.data, config, adtsFrameSize, adtsStartOffset, adtsHeaderLen, stamp, nbSamples, len;
|
||||
var track = this._aacTrack,
|
||||
data = pes.data,
|
||||
pts = pes.pts,
|
||||
startOffset = 0,
|
||||
duration = this._duration,
|
||||
audioCodec = this.audioCodec,
|
||||
config, frameLength, frameDuration, frameIndex, offset, headerLength, stamp, len, aacSample;
|
||||
if (this.aacOverFlow) {
|
||||
var tmp = new Uint8Array(this.aacOverFlow.byteLength + data.byteLength);
|
||||
tmp.set(this.aacOverFlow, 0);
|
||||
|
@ -468,16 +477,16 @@
|
|||
data = tmp;
|
||||
}
|
||||
// look for ADTS header (0xFFFx)
|
||||
for (adtsStartOffset = 0, len = data.length; adtsStartOffset < len - 1; adtsStartOffset++) {
|
||||
if ((data[adtsStartOffset] === 0xff) && (data[adtsStartOffset+1] & 0xf0) === 0xf0) {
|
||||
for (offset = startOffset, len = data.length; offset < len - 1; offset++) {
|
||||
if ((data[offset] === 0xff) && (data[offset+1] & 0xf0) === 0xf0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// if ADTS header does not start straight from the beginning of the PES payload, raise an error
|
||||
if (adtsStartOffset) {
|
||||
if (offset) {
|
||||
var reason, fatal;
|
||||
if (adtsStartOffset < len - 1) {
|
||||
reason = `AAC PES did not start with ADTS header,offset:${adtsStartOffset}`;
|
||||
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';
|
||||
|
@ -489,37 +498,38 @@
|
|||
}
|
||||
}
|
||||
if (!track.audiosamplerate) {
|
||||
config = this._ADTStoAudioConfig(data, adtsStartOffset, this.audioCodec);
|
||||
config = ADTS.getAudioConfig(this.observer,data, offset, audioCodec);
|
||||
track.config = config.config;
|
||||
track.audiosamplerate = config.samplerate;
|
||||
track.channelCount = config.channelCount;
|
||||
track.codec = config.codec;
|
||||
track.timescale = this.remuxer.timescale;
|
||||
track.duration = this.remuxer.timescale * this._duration;
|
||||
track.duration = track.timescale * duration;
|
||||
logger.log(`parsed codec:${track.codec},rate:${config.samplerate},nb channel:${config.channelCount}`);
|
||||
}
|
||||
nbSamples = 0;
|
||||
while ((adtsStartOffset + 5) < len) {
|
||||
frameIndex = 0;
|
||||
frameDuration = 1024 * 90000 / track.audiosamplerate;
|
||||
while ((offset + 5) < len) {
|
||||
// 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
|
||||
adtsFrameSize = ((data[adtsStartOffset + 3] & 0x03) << 11);
|
||||
// byte 4
|
||||
adtsFrameSize |= (data[adtsStartOffset + 4] << 3);
|
||||
// byte 5
|
||||
adtsFrameSize |= ((data[adtsStartOffset + 5] & 0xE0) >>> 5);
|
||||
adtsHeaderLen = (!!(data[adtsStartOffset + 1] & 0x01) ? 7 : 9);
|
||||
adtsFrameSize -= adtsHeaderLen;
|
||||
stamp = Math.round(pes.pts + nbSamples * 1024 * this.PES_TIMESCALE / track.audiosamplerate);
|
||||
frameLength = ((data[offset + 3] & 0x03) << 11) |
|
||||
(data[offset + 4] << 3) |
|
||||
((data[offset + 5] & 0xE0) >>> 5);
|
||||
frameLength -= headerLength;
|
||||
stamp = Math.round(pts + frameIndex * frameDuration);
|
||||
//stamp = pes.pts;
|
||||
//console.log('AAC frame, offset/length/pts:' + (adtsStartOffset+7) + '/' + adtsFrameSize + '/' + stamp.toFixed(0));
|
||||
if ((adtsFrameSize > 0) && ((adtsStartOffset + adtsHeaderLen + adtsFrameSize) <= len)) {
|
||||
aacSample = {unit: data.subarray(adtsStartOffset + adtsHeaderLen, adtsStartOffset + adtsHeaderLen + adtsFrameSize), pts: stamp, dts: stamp};
|
||||
this._aacTrack.samples.push(aacSample);
|
||||
this._aacTrack.len += adtsFrameSize;
|
||||
adtsStartOffset += adtsFrameSize + adtsHeaderLen;
|
||||
nbSamples++;
|
||||
|
||||
//console.log('AAC frame, offset/length/pts:' + (offset+headerLength) + '/' + frameLength + '/' + stamp.toFixed(0));
|
||||
if ((frameLength > 0) && ((offset + headerLength + frameLength) <= len)) {
|
||||
aacSample = {unit: data.subarray(offset + headerLength, offset + headerLength + frameLength), pts: stamp, dts: stamp};
|
||||
track.samples.push(aacSample);
|
||||
track.len += frameLength;
|
||||
offset += frameLength + headerLength;
|
||||
frameIndex++;
|
||||
// look for ADTS header (0xFFFx)
|
||||
for ( ; adtsStartOffset < (len - 1); adtsStartOffset++) {
|
||||
if ((data[adtsStartOffset] === 0xff) && ((data[adtsStartOffset + 1] & 0xf0) === 0xf0)) {
|
||||
for ( ; offset < (len - 1); offset++) {
|
||||
if ((data[offset] === 0xff) && ((data[offset + 1] & 0xf0) === 0xf0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -527,135 +537,13 @@
|
|||
break;
|
||||
}
|
||||
}
|
||||
if (adtsStartOffset < len) {
|
||||
this.aacOverFlow = data.subarray(adtsStartOffset, len);
|
||||
if (offset < len) {
|
||||
this.aacOverFlow = data.subarray(offset, len);
|
||||
} else {
|
||||
this.aacOverFlow = null;
|
||||
}
|
||||
}
|
||||
|
||||
_ADTStoAudioConfig(data, offset, audioCodec) {
|
||||
var adtsObjectType, // :int
|
||||
adtsSampleingIndex, // :int
|
||||
adtsExtensionSampleingIndex, // :int
|
||||
adtsChanelConfig, // :int
|
||||
config,
|
||||
userAgent = navigator.userAgent.toLowerCase(),
|
||||
adtsSampleingRates = [
|
||||
96000, 88200,
|
||||
64000, 48000,
|
||||
44100, 32000,
|
||||
24000, 22050,
|
||||
16000, 12000,
|
||||
11025, 8000,
|
||||
7350];
|
||||
// byte 2
|
||||
adtsObjectType = ((data[offset + 2] & 0xC0) >>> 6) + 1;
|
||||
adtsSampleingIndex = ((data[offset + 2] & 0x3C) >>> 2);
|
||||
if(adtsSampleingIndex > adtsSampleingRates.length-1) {
|
||||
this.observer.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.FRAG_PARSING_ERROR, fatal: true, reason: `invalid ADTS sampling index:${adtsSampleingIndex}`});
|
||||
return;
|
||||
}
|
||||
adtsChanelConfig = ((data[offset + 2] & 0x01) << 2);
|
||||
// byte 3
|
||||
adtsChanelConfig |= ((data[offset + 3] & 0xC0) >>> 6);
|
||||
logger.log(`manifest codec:${audioCodec},ADTS data:type:${adtsObjectType},sampleingIndex:${adtsSampleingIndex}[${adtsSampleingRates[adtsSampleingIndex]}Hz],channelConfig:${adtsChanelConfig}`);
|
||||
// firefox: freq less than 24kHz = AAC SBR (HE-AAC)
|
||||
if (userAgent.indexOf('firefox') !== -1) {
|
||||
if (adtsSampleingIndex >= 6) {
|
||||
adtsObjectType = 5;
|
||||
config = new Array(4);
|
||||
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
||||
// there is a factor 2 between frame sample rate and output sample rate
|
||||
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex - 3;
|
||||
} else {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
||||
}
|
||||
// Android : always use AAC
|
||||
} else if (userAgent.indexOf('android') !== -1) {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
||||
} else {
|
||||
/* for other browsers (chrome ...)
|
||||
always force audio type to be HE-AAC SBR, as some browsers do not support audio codec switch properly (like Chrome ...)
|
||||
*/
|
||||
adtsObjectType = 5;
|
||||
config = new Array(4);
|
||||
// if (manifest codec is HE-AAC or HE-AACv2) OR (manifest codec not specified AND frequency less than 24kHz)
|
||||
if ((audioCodec && ((audioCodec.indexOf('mp4a.40.29') !== -1) ||
|
||||
(audioCodec.indexOf('mp4a.40.5') !== -1))) ||
|
||||
(!audioCodec && adtsSampleingIndex >= 6)) {
|
||||
// HE-AAC uses SBR (Spectral Band Replication) , high frequencies are constructed from low frequencies
|
||||
// there is a factor 2 between frame sample rate and output sample rate
|
||||
// multiply frequency by 2 (see table below, equivalent to substract 3)
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex - 3;
|
||||
} else {
|
||||
// if (manifest codec is AAC) AND (frequency less than 24kHz OR nb channel is 1) OR (manifest codec not specified and mono audio)
|
||||
// Chrome fails to play back with AAC LC mono when initialized with HE-AAC. This is not a problem with stereo.
|
||||
if (audioCodec && audioCodec.indexOf('mp4a.40.2') !== -1 && (adtsSampleingIndex >= 6 || adtsChanelConfig === 1) ||
|
||||
(!audioCodec && adtsChanelConfig === 1)) {
|
||||
adtsObjectType = 2;
|
||||
config = new Array(2);
|
||||
}
|
||||
adtsExtensionSampleingIndex = adtsSampleingIndex;
|
||||
}
|
||||
}
|
||||
/* refer to http://wiki.multimedia.cx/index.php?title=MPEG-4_Audio#Audio_Specific_Config
|
||||
ISO 14496-3 (AAC).pdf - Table 1.13 — Syntax of AudioSpecificConfig()
|
||||
Audio Profile / Audio Object Type
|
||||
0: Null
|
||||
1: AAC Main
|
||||
2: AAC LC (Low Complexity)
|
||||
3: AAC SSR (Scalable Sample Rate)
|
||||
4: AAC LTP (Long Term Prediction)
|
||||
5: SBR (Spectral Band Replication)
|
||||
6: AAC Scalable
|
||||
sampling freq
|
||||
0: 96000 Hz
|
||||
1: 88200 Hz
|
||||
2: 64000 Hz
|
||||
3: 48000 Hz
|
||||
4: 44100 Hz
|
||||
5: 32000 Hz
|
||||
6: 24000 Hz
|
||||
7: 22050 Hz
|
||||
8: 16000 Hz
|
||||
9: 12000 Hz
|
||||
10: 11025 Hz
|
||||
11: 8000 Hz
|
||||
12: 7350 Hz
|
||||
13: Reserved
|
||||
14: Reserved
|
||||
15: frequency is written explictly
|
||||
Channel Configurations
|
||||
These are the channel configurations:
|
||||
0: Defined in AOT Specifc Config
|
||||
1: 1 channel: front-center
|
||||
2: 2 channels: front-left, front-right
|
||||
*/
|
||||
// audioObjectType = profile => profile, the MPEG-4 Audio Object Type minus 1
|
||||
config[0] = adtsObjectType << 3;
|
||||
// samplingFrequencyIndex
|
||||
config[0] |= (adtsSampleingIndex & 0x0E) >> 1;
|
||||
config[1] |= (adtsSampleingIndex & 0x01) << 7;
|
||||
// channelConfiguration
|
||||
config[1] |= adtsChanelConfig << 3;
|
||||
if (adtsObjectType === 5) {
|
||||
// adtsExtensionSampleingIndex
|
||||
config[1] |= (adtsExtensionSampleingIndex & 0x0E) >> 1;
|
||||
config[2] = (adtsExtensionSampleingIndex & 0x01) << 7;
|
||||
// adtsObjectType (force to 2, chrome is checking that object type is less than 5 ???
|
||||
// https://chromium.googlesource.com/chromium/src.git/+/master/media/formats/mp4/aac.cc
|
||||
config[2] |= 2 << 2;
|
||||
config[3] = 0;
|
||||
}
|
||||
return {config: config, samplerate: adtsSampleingRates[adtsSampleingIndex], channelCount: adtsChanelConfig, codec: ('mp4a.40.' + adtsObjectType)};
|
||||
}
|
||||
|
||||
_parseID3PES(pes) {
|
||||
this._id3Track.samples.push(pes);
|
||||
}
|
||||
|
|
|
@ -66,8 +66,8 @@ class LevelHelper {
|
|||
fragments = details.fragments;
|
||||
frag = fragments[fragIdx];
|
||||
if(!isNaN(frag.startPTS)) {
|
||||
startPTS = Math.max(startPTS,frag.startPTS);
|
||||
endPTS = Math.min(endPTS, frag.endPTS);
|
||||
startPTS = Math.min(startPTS,frag.startPTS);
|
||||
endPTS = Math.max(endPTS, frag.endPTS);
|
||||
}
|
||||
|
||||
var drift = startPTS - frag.start;
|
||||
|
@ -99,12 +99,12 @@ class LevelHelper {
|
|||
if (toIdx > fromIdx) {
|
||||
fragFrom.duration = fragToPTS-fragFrom.start;
|
||||
if(fragFrom.duration < 0) {
|
||||
logger.error(`negative duration computed for ${fragFrom}, there should be some duration drift between playlist and fragment!`);
|
||||
logger.error(`negative duration computed for frag ${fragFrom.sn},level ${fragFrom.level}, there should be some duration drift between playlist and fragment!`);
|
||||
}
|
||||
} else {
|
||||
fragTo.duration = fragFrom.start - fragToPTS;
|
||||
if(fragTo.duration < 0) {
|
||||
logger.error(`negative duration computed for ${fragTo}, there should be some duration drift between playlist and fragment!`);
|
||||
logger.error(`negative duration computed for frag ${fragTo.sn},level ${fragTo.level}, there should be some duration drift between playlist and fragment!`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
70
dashboard-ui/bower_components/hls.js/src/hls.js
vendored
70
dashboard-ui/bower_components/hls.js/src/hls.js
vendored
|
@ -34,36 +34,50 @@ class Hls {
|
|||
return ErrorDetails;
|
||||
}
|
||||
|
||||
static get DefaultConfig() {
|
||||
if(!Hls.defaultConfig) {
|
||||
Hls.defaultConfig = {
|
||||
autoStartLoad: true,
|
||||
debug: false,
|
||||
maxBufferLength: 30,
|
||||
maxBufferSize: 60 * 1000 * 1000,
|
||||
liveSyncDurationCount:3,
|
||||
liveMaxLatencyDurationCount: Infinity,
|
||||
maxMaxBufferLength: 600,
|
||||
enableWorker: true,
|
||||
enableSoftwareAES: true,
|
||||
manifestLoadingTimeOut: 10000,
|
||||
manifestLoadingMaxRetry: 1,
|
||||
manifestLoadingRetryDelay: 1000,
|
||||
levelLoadingTimeOut: 10000,
|
||||
levelLoadingMaxRetry: 4,
|
||||
levelLoadingRetryDelay: 1000,
|
||||
fragLoadingTimeOut: 20000,
|
||||
fragLoadingMaxRetry: 6,
|
||||
fragLoadingRetryDelay: 1000,
|
||||
fragLoadingLoopThreshold: 3,
|
||||
// fpsDroppedMonitoringPeriod: 5000,
|
||||
// fpsDroppedMonitoringThreshold: 0.2,
|
||||
appendErrorMaxRetry: 3,
|
||||
loader: XhrLoader,
|
||||
fLoader: undefined,
|
||||
pLoader: undefined,
|
||||
abrController : AbrController,
|
||||
mediaController: MSEMediaController
|
||||
};
|
||||
}
|
||||
return Hls.defaultConfig;
|
||||
}
|
||||
|
||||
static set DefaultConfig(defaultConfig) {
|
||||
Hls.defaultConfig = defaultConfig;
|
||||
}
|
||||
|
||||
constructor(config = {}) {
|
||||
var configDefault = {
|
||||
autoStartLoad: true,
|
||||
debug: false,
|
||||
maxBufferLength: 30,
|
||||
maxBufferSize: 60 * 1000 * 1000,
|
||||
liveSyncDurationCount:3,
|
||||
liveMaxLatencyDurationCount: Infinity,
|
||||
maxMaxBufferLength: 600,
|
||||
enableWorker: true,
|
||||
enableSoftwareAES: true,
|
||||
fragLoadingTimeOut: 20000,
|
||||
fragLoadingMaxRetry: 6,
|
||||
fragLoadingRetryDelay: 1000,
|
||||
fragLoadingLoopThreshold: 3,
|
||||
manifestLoadingTimeOut: 10000,
|
||||
manifestLoadingMaxRetry: 1,
|
||||
manifestLoadingRetryDelay: 1000,
|
||||
// fpsDroppedMonitoringPeriod: 5000,
|
||||
// fpsDroppedMonitoringThreshold: 0.2,
|
||||
appendErrorMaxRetry: 3,
|
||||
loader: XhrLoader,
|
||||
fLoader: undefined,
|
||||
pLoader: undefined,
|
||||
abrController : AbrController,
|
||||
mediaController: MSEMediaController
|
||||
};
|
||||
for (var prop in configDefault) {
|
||||
var defaultConfig = Hls.DefaultConfig;
|
||||
for (var prop in defaultConfig) {
|
||||
if (prop in config) { continue; }
|
||||
config[prop] = configDefault[prop];
|
||||
config[prop] = defaultConfig[prop];
|
||||
}
|
||||
|
||||
if (config.liveMaxLatencyDurationCount !== undefined && config.liveMaxLatencyDurationCount <= config.liveSyncDurationCount) {
|
||||
|
|
|
@ -27,7 +27,7 @@ class FragmentLoader {
|
|||
this.frag.loaded = 0;
|
||||
var config = this.hls.config;
|
||||
frag.loader = this.loader = typeof(config.fLoader) !== 'undefined' ? new config.fLoader(config) : new config.loader(config);
|
||||
this.loader.load(frag.url, 'arraybuffer', this.loadsuccess.bind(this), this.loaderror.bind(this), this.loadtimeout.bind(this), config.fragLoadingTimeOut, 1, config.fragLoadingRetryDelay, this.loadprogress.bind(this), frag);
|
||||
this.loader.load(frag.url, 'arraybuffer', this.loadsuccess.bind(this), this.loaderror.bind(this), this.loadtimeout.bind(this), config.fragLoadingTimeOut, 1, 0, this.loadprogress.bind(this), frag);
|
||||
}
|
||||
|
||||
loadsuccess(event, stats) {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import Event from '../events';
|
||||
import {ErrorTypes, ErrorDetails} from '../errors';
|
||||
import URLHelper from '../utils/url';
|
||||
import AttrList from '../utils/attr-list';
|
||||
//import {logger} from '../utils/logger';
|
||||
|
||||
class PlaylistLoader {
|
||||
|
@ -36,12 +37,24 @@ class PlaylistLoader {
|
|||
}
|
||||
|
||||
load(url, id1, id2) {
|
||||
var config = this.hls.config;
|
||||
var config = this.hls.config,
|
||||
retry,
|
||||
timeout,
|
||||
retryDelay;
|
||||
this.url = url;
|
||||
this.id = id1;
|
||||
this.id2 = id2;
|
||||
if(this.id === undefined) {
|
||||
retry = config.manifestLoadingMaxRetry;
|
||||
timeout = config.manifestLoadingTimeOut;
|
||||
retryDelay = config.manifestLoadingRetryDelay;
|
||||
} else {
|
||||
retry = config.levelLoadingMaxRetry;
|
||||
timeout = config.levelLoadingTimeOut;
|
||||
retryDelay = config.levelLoadingRetryDelay;
|
||||
}
|
||||
this.loader = typeof(config.pLoader) !== 'undefined' ? new config.pLoader(config) : new config.loader(config);
|
||||
this.loader.load(url, '', this.loadsuccess.bind(this), this.loaderror.bind(this), this.loadtimeout.bind(this), config.manifestLoadingTimeOut, config.manifestLoadingMaxRetry, config.manifestLoadingRetryDelay);
|
||||
this.loader.load(url, '', this.loadsuccess.bind(this), this.loaderror.bind(this), this.loadtimeout.bind(this), timeout, retry, retryDelay);
|
||||
}
|
||||
|
||||
resolve(url, baseUrl) {
|
||||
|
@ -49,42 +62,38 @@ class PlaylistLoader {
|
|||
}
|
||||
|
||||
parseMasterPlaylist(string, baseurl) {
|
||||
var levels = [], level = {}, result, codecs, codec;
|
||||
let levels = [], result;
|
||||
|
||||
// https://regex101.com is your friend
|
||||
var re = /#EXT-X-STREAM-INF:([^\n\r]*(BAND)WIDTH=(\d+))?([^\n\r]*(CODECS)=\"([^\"\n\r]*)\",?)?([^\n\r]*(RES)OLUTION=(\d+)x(\d+))?([^\n\r]*(NAME)=\"(.*)\")?[^\n\r]*[\r\n]+([^\r\n]+)/g;
|
||||
const re = /#EXT-X-STREAM-INF:([^\n\r]*)[\r\n]+([^\r\n]+)/g;
|
||||
while ((result = re.exec(string)) != null){
|
||||
result.shift();
|
||||
result = result.filter(function(n) { return (n !== undefined); });
|
||||
level.url = this.resolve(result.pop(), baseurl);
|
||||
while (result.length > 0) {
|
||||
switch (result.shift()) {
|
||||
case 'RES':
|
||||
level.width = parseInt(result.shift());
|
||||
level.height = parseInt(result.shift());
|
||||
break;
|
||||
case 'BAND':
|
||||
level.bitrate = parseInt(result.shift());
|
||||
break;
|
||||
case 'NAME':
|
||||
level.name = result.shift();
|
||||
break;
|
||||
case 'CODECS':
|
||||
codecs = result.shift().split(',');
|
||||
while (codecs.length > 0) {
|
||||
codec = codecs.shift();
|
||||
if (codec.indexOf('avc1') !== -1) {
|
||||
level.videoCodec = this.avc1toavcoti(codec);
|
||||
} else {
|
||||
level.audioCodec = codec;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
const level = {};
|
||||
|
||||
var attrs = level.attrs = new AttrList(result[1]);
|
||||
level.url = this.resolve(result[2], baseurl);
|
||||
|
||||
var resolution = attrs.decimalResolution('RESOLUTION');
|
||||
if(resolution) {
|
||||
level.width = resolution.width;
|
||||
level.height = resolution.height;
|
||||
}
|
||||
level.bitrate = attrs.decimalInteger('BANDWIDTH');
|
||||
level.name = attrs.NAME;
|
||||
|
||||
var codecs = attrs.CODECS;
|
||||
if(codecs) {
|
||||
codecs = codecs.split(',');
|
||||
for (let i = 0; i < codecs.length; i++) {
|
||||
const codec = codecs[i];
|
||||
if (codec.indexOf('avc1') !== -1) {
|
||||
level.videoCodec = this.avc1toavcoti(codec);
|
||||
} else {
|
||||
level.audioCodec = codec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
levels.push(level);
|
||||
level = {};
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
|
@ -101,26 +110,24 @@ class PlaylistLoader {
|
|||
return result;
|
||||
}
|
||||
|
||||
parseKeyParamsByRegex(string, regexp) {
|
||||
var result = regexp.exec(string);
|
||||
if (result) {
|
||||
result.shift();
|
||||
result = result.filter(function(n) { return (n !== undefined); });
|
||||
if (result.length === 2) {
|
||||
return result[1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
cloneObj(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
parseLevelPlaylist(string, baseurl, id) {
|
||||
var currentSN = 0, totalduration = 0, level = {url: baseurl, fragments: [], live: true, startSN: 0}, result, regexp, cc = 0, frag, byteRangeEndOffset, byteRangeStartOffset;
|
||||
var levelkey = {method : null, key : null, iv : null, uri : null};
|
||||
regexp = /(?:#EXT-X-(MEDIA-SEQUENCE):(\d+))|(?:#EXT-X-(TARGETDURATION):(\d+))|(?:#EXT-X-(KEY):(.*))|(?:#EXT(INF):([\d\.]+)[^\r\n]*([\r\n]+[^#|\r\n]+)?)|(?:#EXT-X-(BYTERANGE):([\d]+[@[\d]*)]*[\r\n]+([^#|\r\n]+)?|(?:#EXT-X-(ENDLIST))|(?:#EXT-X-(DIS)CONTINUITY))/g;
|
||||
var currentSN = 0,
|
||||
totalduration = 0,
|
||||
level = {url: baseurl, fragments: [], live: true, startSN: 0},
|
||||
levelkey = {method : null, key : null, iv : null, uri : null},
|
||||
cc = 0,
|
||||
programDateTime = null,
|
||||
frag = null,
|
||||
result,
|
||||
regexp,
|
||||
byteRangeEndOffset,
|
||||
byteRangeStartOffset;
|
||||
|
||||
regexp = /(?:#EXT-X-(MEDIA-SEQUENCE):(\d+))|(?:#EXT-X-(TARGETDURATION):(\d+))|(?:#EXT-X-(KEY):(.*))|(?:#EXT(INF):([\d\.]+)[^\r\n]*([\r\n]+[^#|\r\n]+)?)|(?:#EXT-X-(BYTERANGE):([\d]+[@[\d]*)]*[\r\n]+([^#|\r\n]+)?|(?:#EXT-X-(ENDLIST))|(?:#EXT-X-(DIS)CONTINUITY))|(?:#EXT-X-(PROGRAM-DATE-TIME):(.*))/g;
|
||||
while ((result = regexp.exec(string)) !== null) {
|
||||
result.shift();
|
||||
result = result.filter(function(n) { return (n !== undefined); });
|
||||
|
@ -145,7 +152,6 @@ class PlaylistLoader {
|
|||
byteRangeStartOffset = parseInt(params[1]);
|
||||
}
|
||||
byteRangeEndOffset = parseInt(params[0]) + byteRangeStartOffset;
|
||||
frag = level.fragments.length ? level.fragments[level.fragments.length - 1] : null;
|
||||
if (frag && !frag.url) {
|
||||
frag.byteRangeStartOffset = byteRangeStartOffset;
|
||||
frag.byteRangeEndOffset = byteRangeEndOffset;
|
||||
|
@ -167,17 +173,21 @@ class PlaylistLoader {
|
|||
} else {
|
||||
fragdecryptdata = levelkey;
|
||||
}
|
||||
level.fragments.push({url: result[2] ? this.resolve(result[2], baseurl) : null, duration: duration, start: totalduration, sn: sn, level: id, cc: cc, byteRangeStartOffset: byteRangeStartOffset, byteRangeEndOffset: byteRangeEndOffset, decryptdata : fragdecryptdata});
|
||||
var url = result[2] ? this.resolve(result[2], baseurl) : null;
|
||||
frag = {url: url, duration: duration, start: totalduration, sn: sn, level: id, cc: cc, byteRangeStartOffset: byteRangeStartOffset, byteRangeEndOffset: byteRangeEndOffset, decryptdata : fragdecryptdata, programDateTime: programDateTime};
|
||||
level.fragments.push(frag);
|
||||
totalduration += duration;
|
||||
byteRangeStartOffset = null;
|
||||
programDateTime = null;
|
||||
}
|
||||
break;
|
||||
case 'KEY':
|
||||
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.4
|
||||
var decryptparams = result[1];
|
||||
var decryptmethod = this.parseKeyParamsByRegex(decryptparams, /(METHOD)=([^,]*)/),
|
||||
decrypturi = this.parseKeyParamsByRegex(decryptparams, /(URI)=["]([^,]*)["]/),
|
||||
decryptiv = this.parseKeyParamsByRegex(decryptparams, /(IV)=([^,]*)/);
|
||||
var keyAttrs = new AttrList(decryptparams);
|
||||
var decryptmethod = keyAttrs.enumeratedString('METHOD'),
|
||||
decrypturi = keyAttrs.URI,
|
||||
decryptiv = keyAttrs.hexadecimalInteger('IV');
|
||||
if (decryptmethod) {
|
||||
levelkey = { method: null, key: null, iv: null, uri: null };
|
||||
if ((decrypturi) && (decryptmethod === 'AES-128')) {
|
||||
|
@ -186,40 +196,42 @@ class PlaylistLoader {
|
|||
levelkey.uri = this.resolve(decrypturi, baseurl);
|
||||
levelkey.key = null;
|
||||
// Initialization Vector (IV)
|
||||
if (decryptiv) {
|
||||
levelkey.iv = decryptiv;
|
||||
if (levelkey.iv.substring(0, 2) === '0x') {
|
||||
levelkey.iv = levelkey.iv.substring(2);
|
||||
}
|
||||
levelkey.iv = levelkey.iv.match(/.{8}/g);
|
||||
levelkey.iv[0] = parseInt(levelkey.iv[0], 16);
|
||||
levelkey.iv[1] = parseInt(levelkey.iv[1], 16);
|
||||
levelkey.iv[2] = parseInt(levelkey.iv[2], 16);
|
||||
levelkey.iv[3] = parseInt(levelkey.iv[3], 16);
|
||||
levelkey.iv = new Uint32Array(levelkey.iv);
|
||||
}
|
||||
levelkey.iv = decryptiv;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'PROGRAM-DATE-TIME':
|
||||
programDateTime = new Date(Date.parse(result[1]));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
//logger.log('found ' + level.fragments.length + ' fragments');
|
||||
if(frag && !frag.url) {
|
||||
level.fragments.pop();
|
||||
totalduration-=frag.duration;
|
||||
}
|
||||
level.totalduration = totalduration;
|
||||
level.endSN = currentSN - 1;
|
||||
return level;
|
||||
}
|
||||
|
||||
loadsuccess(event, stats) {
|
||||
var string = event.currentTarget.responseText, url = event.currentTarget.responseURL, id = this.id, id2 = this.id2, hls = this.hls, levels;
|
||||
var target = event.currentTarget,
|
||||
string = target.responseText,
|
||||
url = target.responseURL,
|
||||
id = this.id,
|
||||
id2 = this.id2,
|
||||
hls = this.hls,
|
||||
levels;
|
||||
// responseURL not supported on some browsers (it is used to detect URL redirection)
|
||||
if (url === undefined) {
|
||||
// fallback to initial URL
|
||||
url = this.url;
|
||||
}
|
||||
stats.tload = performance.now();
|
||||
stats.mtime = new Date(event.currentTarget.getResponseHeader('Last-Modified'));
|
||||
stats.mtime = new Date(target.getResponseHeader('Last-Modified'));
|
||||
if (string.indexOf('#EXTM3U') === 0) {
|
||||
if (string.indexOf('#EXTINF:') > 0) {
|
||||
// 1 level playlist
|
||||
|
|
|
@ -54,23 +54,7 @@ class MP4 {
|
|||
}
|
||||
}
|
||||
|
||||
MP4.MAJOR_BRAND = new Uint8Array([
|
||||
'i'.charCodeAt(0),
|
||||
's'.charCodeAt(0),
|
||||
'o'.charCodeAt(0),
|
||||
'm'.charCodeAt(0)
|
||||
]);
|
||||
|
||||
MP4.AVC1_BRAND = new Uint8Array([
|
||||
'a'.charCodeAt(0),
|
||||
'v'.charCodeAt(0),
|
||||
'c'.charCodeAt(0),
|
||||
'1'.charCodeAt(0)
|
||||
]);
|
||||
|
||||
MP4.MINOR_VERSION = new Uint8Array([0, 0, 0, 1]);
|
||||
|
||||
MP4.VIDEO_HDLR = new Uint8Array([
|
||||
var videoHdlr = new Uint8Array([
|
||||
0x00, // version 0
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x00, // pre_defined
|
||||
|
@ -83,7 +67,7 @@ class MP4 {
|
|||
0x64, 0x6c, 0x65, 0x72, 0x00 // name: 'VideoHandler'
|
||||
]);
|
||||
|
||||
MP4.AUDIO_HDLR = new Uint8Array([
|
||||
var audioHdlr = new Uint8Array([
|
||||
0x00, // version 0
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x00, // pre_defined
|
||||
|
@ -97,11 +81,11 @@ class MP4 {
|
|||
]);
|
||||
|
||||
MP4.HDLR_TYPES = {
|
||||
'video': MP4.VIDEO_HDLR,
|
||||
'audio': MP4.AUDIO_HDLR
|
||||
'video': videoHdlr,
|
||||
'audio': audioHdlr
|
||||
};
|
||||
|
||||
MP4.DREF = new Uint8Array([
|
||||
var dref = new Uint8Array([
|
||||
0x00, // version 0
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x01, // entry_count
|
||||
|
@ -110,13 +94,15 @@ class MP4 {
|
|||
0x00, // version 0
|
||||
0x00, 0x00, 0x01 // entry_flags
|
||||
]);
|
||||
MP4.STCO = new Uint8Array([
|
||||
|
||||
var stco = new Uint8Array([
|
||||
0x00, // version
|
||||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x00 // entry_count
|
||||
]);
|
||||
MP4.STSC = MP4.STCO;
|
||||
MP4.STTS = MP4.STCO;
|
||||
|
||||
MP4.STTS = MP4.STSC = MP4.STCO = stco;
|
||||
|
||||
MP4.STSZ = new Uint8Array([
|
||||
0x00, // version
|
||||
0x00, 0x00, 0x00, // flags
|
||||
|
@ -143,28 +129,34 @@ class MP4 {
|
|||
0x00, 0x00, 0x00, // flags
|
||||
0x00, 0x00, 0x00, 0x01]);// entry_count
|
||||
|
||||
MP4.FTYP = MP4.box(MP4.types.ftyp, MP4.MAJOR_BRAND, MP4.MINOR_VERSION, MP4.MAJOR_BRAND, MP4.AVC1_BRAND);
|
||||
MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, MP4.DREF));
|
||||
var majorBrand = new Uint8Array([105,115,111,109]); // isom
|
||||
var avc1Brand = new Uint8Array([97,118,99,49]); // avc1
|
||||
var minorVersion = new Uint8Array([0, 0, 0, 1]);
|
||||
|
||||
MP4.FTYP = MP4.box(MP4.types.ftyp, majorBrand, minorVersion, majorBrand, avc1Brand);
|
||||
MP4.DINF = MP4.box(MP4.types.dinf, MP4.box(MP4.types.dref, dref));
|
||||
}
|
||||
|
||||
static box(type) {
|
||||
var
|
||||
payload = Array.prototype.slice.call(arguments, 1),
|
||||
size = 0,
|
||||
size = 8,
|
||||
i = payload.length,
|
||||
len = i,
|
||||
result,
|
||||
view;
|
||||
result;
|
||||
// calculate the total size we need to allocate
|
||||
while (i--) {
|
||||
size += payload[i].byteLength;
|
||||
}
|
||||
result = new Uint8Array(size + 8);
|
||||
view = new DataView(result.buffer);
|
||||
view.setUint32(0, result.byteLength);
|
||||
result = new Uint8Array(size);
|
||||
result[0] = (size >> 24) & 0xff;
|
||||
result[1] = (size >> 16) & 0xff;
|
||||
result[2] = (size >> 8) & 0xff;
|
||||
result[3] = size & 0xff;
|
||||
result.set(type, 4);
|
||||
// copy the payload into the result
|
||||
for (i = 0, size = 8; i < len; i++) {
|
||||
// copy payload[i] array @ offset size
|
||||
result.set(payload[i], size);
|
||||
size += payload[i].byteLength;
|
||||
}
|
||||
|
@ -564,7 +556,7 @@ class MP4 {
|
|||
(size >>> 16) & 0xFF,
|
||||
(size >>> 8) & 0xFF,
|
||||
size & 0xFF, // sample_size
|
||||
(flags.isLeading << 2) | sample.flags.dependsOn,
|
||||
(flags.isLeading << 2) | flags.dependsOn,
|
||||
(flags.isDependedOn << 6) |
|
||||
(flags.hasRedundancy << 4) |
|
||||
(flags.paddingValue << 1) |
|
||||
|
|
|
@ -119,7 +119,7 @@ class MP4Remuxer {
|
|||
|
||||
remuxVideo(track, timeOffset, contiguous) {
|
||||
var view,
|
||||
i = 8,
|
||||
offset = 8,
|
||||
pesTimeScale = this.PES_TIMESCALE,
|
||||
pes2mp4ScaleFactor = this.PES2MP4SCALEFACTOR,
|
||||
avcSample,
|
||||
|
@ -142,25 +142,28 @@ class MP4Remuxer {
|
|||
// convert NALU bitstream to MP4 format (prepend NALU with size field)
|
||||
while (avcSample.units.units.length) {
|
||||
unit = avcSample.units.units.shift();
|
||||
view.setUint32(i, unit.data.byteLength);
|
||||
i += 4;
|
||||
mdat.set(unit.data, i);
|
||||
i += unit.data.byteLength;
|
||||
view.setUint32(offset, unit.data.byteLength);
|
||||
offset += 4;
|
||||
mdat.set(unit.data, offset);
|
||||
offset += unit.data.byteLength;
|
||||
mp4SampleLength += 4 + unit.data.byteLength;
|
||||
}
|
||||
pts = avcSample.pts - this._initDTS;
|
||||
dts = avcSample.dts - this._initDTS;
|
||||
//logger.log('Video/PTS/DTS:' + pts + '/' + dts);
|
||||
// ensure DTS is not bigger than PTS
|
||||
dts = Math.min(pts,dts);
|
||||
//logger.log(`Video/PTS/DTS:${pts}/${dts}`);
|
||||
// if not first AVC sample of video track, normalize PTS/DTS with previous sample value
|
||||
// and ensure that sample duration is positive
|
||||
if (lastDTS !== undefined) {
|
||||
ptsnorm = this._PTSNormalize(pts, lastDTS);
|
||||
dtsnorm = this._PTSNormalize(dts, lastDTS);
|
||||
mp4Sample.duration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor;
|
||||
if (mp4Sample.duration < 0) {
|
||||
//logger.log('invalid sample duration at PTS/DTS::' + avcSample.pts + '/' + avcSample.dts + ':' + mp4Sample.duration);
|
||||
mp4Sample.duration = 0;
|
||||
var sampleDuration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor;
|
||||
if (sampleDuration <= 0) {
|
||||
logger.log(`invalid sample duration at PTS/DTS: ${avcSample.pts}/${avcSample.dts}:${sampleDuration}`);
|
||||
sampleDuration = 1;
|
||||
}
|
||||
mp4Sample.duration = sampleDuration;
|
||||
} else {
|
||||
var nextAvcDts = this.nextAvcDts,delta;
|
||||
// first AVC sample of video track, normalize PTS/DTS
|
||||
|
@ -179,7 +182,7 @@ class MP4Remuxer {
|
|||
dtsnorm = nextAvcDts;
|
||||
// offset PTS as well, ensure that PTS is smaller or equal than new DTS
|
||||
ptsnorm = Math.max(ptsnorm - delta, dtsnorm);
|
||||
logger.log('Video/PTS/DTS adjusted:' + ptsnorm + '/' + dtsnorm);
|
||||
logger.log(`Video/PTS/DTS adjusted: ${ptsnorm}/${dtsnorm},delta:${delta}`);
|
||||
}
|
||||
}
|
||||
// remember first PTS of our avcSamples, ensure value is positive
|
||||
|
@ -209,18 +212,21 @@ class MP4Remuxer {
|
|||
samples.push(mp4Sample);
|
||||
lastDTS = dtsnorm;
|
||||
}
|
||||
var lastSampleDuration = 0;
|
||||
if (samples.length >= 2) {
|
||||
mp4Sample.duration = samples[samples.length - 2].duration;
|
||||
lastSampleDuration = samples[samples.length - 2].duration;
|
||||
mp4Sample.duration = lastSampleDuration;
|
||||
}
|
||||
// next AVC sample DTS should be equal to last sample DTS + last sample duration
|
||||
this.nextAvcDts = dtsnorm + mp4Sample.duration * pes2mp4ScaleFactor;
|
||||
this.nextAvcDts = dtsnorm + lastSampleDuration * pes2mp4ScaleFactor;
|
||||
track.len = 0;
|
||||
track.nbNalu = 0;
|
||||
if(navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
|
||||
if(samples.length && navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
|
||||
var flags = samples[0].flags;
|
||||
// chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue
|
||||
// https://code.google.com/p/chromium/issues/detail?id=229412
|
||||
samples[0].flags.dependsOn = 2;
|
||||
samples[0].flags.isNonSync = 0;
|
||||
flags.dependsOn = 2;
|
||||
flags.isNonSync = 0;
|
||||
}
|
||||
track.samples = samples;
|
||||
moof = MP4.moof(track.sequenceNumber++, firstDTS / pes2mp4ScaleFactor, track);
|
||||
|
@ -229,9 +235,9 @@ class MP4Remuxer {
|
|||
moof: moof,
|
||||
mdat: mdat,
|
||||
startPTS: firstPTS / pesTimeScale,
|
||||
endPTS: (ptsnorm + pes2mp4ScaleFactor * mp4Sample.duration) / pesTimeScale,
|
||||
endPTS: (ptsnorm + pes2mp4ScaleFactor * lastSampleDuration) / pesTimeScale,
|
||||
startDTS: firstDTS / pesTimeScale,
|
||||
endDTS: (dtsnorm + pes2mp4ScaleFactor * mp4Sample.duration) / pesTimeScale,
|
||||
endDTS: this.nextAvcDts / pesTimeScale,
|
||||
type: 'video',
|
||||
nb: samples.length
|
||||
});
|
||||
|
@ -239,7 +245,7 @@ class MP4Remuxer {
|
|||
|
||||
remuxAudio(track,timeOffset, contiguous) {
|
||||
var view,
|
||||
i = 8,
|
||||
offset = 8,
|
||||
pesTimeScale = this.PES_TIMESCALE,
|
||||
pes2mp4ScaleFactor = this.PES2MP4SCALEFACTOR,
|
||||
aacSample, mp4Sample,
|
||||
|
@ -247,27 +253,32 @@ class MP4Remuxer {
|
|||
mdat, moof,
|
||||
firstPTS, firstDTS, lastDTS,
|
||||
pts, dts, ptsnorm, dtsnorm,
|
||||
samples = [];
|
||||
/* concatenate the audio data and construct the mdat in place
|
||||
(need 8 more bytes to fill length and mdat type) */
|
||||
mdat = new Uint8Array(track.len + 8);
|
||||
view = new DataView(mdat.buffer);
|
||||
view.setUint32(0, mdat.byteLength);
|
||||
mdat.set(MP4.types.mdat, 4);
|
||||
while (track.samples.length) {
|
||||
aacSample = track.samples.shift();
|
||||
samples = [],
|
||||
samples0 = [];
|
||||
|
||||
track.samples.forEach(aacSample => {
|
||||
if(pts === undefined || aacSample.pts > pts) {
|
||||
samples0.push(aacSample);
|
||||
pts = aacSample.pts;
|
||||
} else {
|
||||
logger.warn('dropping past audio frame');
|
||||
}
|
||||
});
|
||||
|
||||
while (samples0.length) {
|
||||
aacSample = samples0.shift();
|
||||
unit = aacSample.unit;
|
||||
mdat.set(unit, i);
|
||||
i += unit.byteLength;
|
||||
pts = aacSample.pts - this._initDTS;
|
||||
dts = aacSample.dts - this._initDTS;
|
||||
//logger.log('Audio/PTS:' + aacSample.pts.toFixed(0));
|
||||
//logger.log(`Audio/PTS:${aacSample.pts.toFixed(0)}`);
|
||||
// if not first sample
|
||||
if (lastDTS !== undefined) {
|
||||
ptsnorm = this._PTSNormalize(pts, lastDTS);
|
||||
dtsnorm = this._PTSNormalize(dts, lastDTS);
|
||||
// we use DTS to compute sample duration, but we use PTS to compute initPTS which is used to sync audio and video
|
||||
// let's compute sample duration
|
||||
mp4Sample.duration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor;
|
||||
if (mp4Sample.duration < 0) {
|
||||
// not expected to happen ...
|
||||
logger.log(`invalid AAC sample duration at PTS:${aacSample.pts}:${mp4Sample.duration}`);
|
||||
mp4Sample.duration = 0;
|
||||
}
|
||||
|
@ -280,11 +291,13 @@ class MP4Remuxer {
|
|||
if (contiguous || Math.abs(delta) < 600) {
|
||||
// log delta
|
||||
if (delta) {
|
||||
if (delta > 1) {
|
||||
if (delta > 0) {
|
||||
logger.log(`${delta} ms hole between AAC samples detected,filling it`);
|
||||
// set PTS to next PTS, and ensure PTS is greater or equal than last DTS
|
||||
} else if (delta < -1) {
|
||||
logger.log(`${(-delta)} ms overlapping between AAC samples detected`);
|
||||
} else if (delta < 0) {
|
||||
// drop overlapping audio frames... browser will deal with it
|
||||
logger.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`);
|
||||
track.len -= unit.byteLength;
|
||||
continue;
|
||||
}
|
||||
// set DTS to next DTS
|
||||
ptsnorm = dtsnorm = nextAacPts;
|
||||
|
@ -293,7 +306,15 @@ class MP4Remuxer {
|
|||
// remember first PTS of our aacSamples, ensure value is positive
|
||||
firstPTS = Math.max(0, ptsnorm);
|
||||
firstDTS = Math.max(0, dtsnorm);
|
||||
/* concatenate the audio data and construct the mdat in place
|
||||
(need 8 more bytes to fill length and mdat type) */
|
||||
mdat = new Uint8Array(track.len + 8);
|
||||
view = new DataView(mdat.buffer);
|
||||
view.setUint32(0, mdat.byteLength);
|
||||
mdat.set(MP4.types.mdat, 4);
|
||||
}
|
||||
mdat.set(unit, offset);
|
||||
offset += unit.byteLength;
|
||||
//console.log('PTS/DTS/initDTS/normPTS/normDTS/relative PTS : ${aacSample.pts}/${aacSample.dts}/${this._initDTS}/${ptsnorm}/${dtsnorm}/${(aacSample.pts/4294967296).toFixed(3)}');
|
||||
mp4Sample = {
|
||||
size: unit.byteLength,
|
||||
|
@ -310,27 +331,32 @@ class MP4Remuxer {
|
|||
samples.push(mp4Sample);
|
||||
lastDTS = dtsnorm;
|
||||
}
|
||||
var lastSampleDuration = 0;
|
||||
var nbSamples = samples.length;
|
||||
//set last sample duration as being identical to previous sample
|
||||
if (samples.length >= 2) {
|
||||
mp4Sample.duration = samples[samples.length - 2].duration;
|
||||
if (nbSamples >= 2) {
|
||||
lastSampleDuration = samples[nbSamples - 2].duration;
|
||||
mp4Sample.duration = lastSampleDuration;
|
||||
}
|
||||
if (nbSamples) {
|
||||
// next aac sample PTS should be equal to last sample PTS + duration
|
||||
this.nextAacPts = ptsnorm + pes2mp4ScaleFactor * lastSampleDuration;
|
||||
//logger.log('Audio/PTS/PTSend:' + aacSample.pts.toFixed(0) + '/' + this.nextAacDts.toFixed(0));
|
||||
track.len = 0;
|
||||
track.samples = samples;
|
||||
moof = MP4.moof(track.sequenceNumber++, firstDTS / pes2mp4ScaleFactor, track);
|
||||
track.samples = [];
|
||||
this.observer.trigger(Event.FRAG_PARSING_DATA, {
|
||||
moof: moof,
|
||||
mdat: mdat,
|
||||
startPTS: firstPTS / pesTimeScale,
|
||||
endPTS: this.nextAacPts / pesTimeScale,
|
||||
startDTS: firstDTS / pesTimeScale,
|
||||
endDTS: (dtsnorm + pes2mp4ScaleFactor * lastSampleDuration) / pesTimeScale,
|
||||
type: 'audio',
|
||||
nb: nbSamples
|
||||
});
|
||||
}
|
||||
// next aac sample PTS should be equal to last sample PTS + duration
|
||||
this.nextAacPts = ptsnorm + pes2mp4ScaleFactor * mp4Sample.duration;
|
||||
//logger.log('Audio/PTS/PTSend:' + aacSample.pts.toFixed(0) + '/' + this.nextAacDts.toFixed(0));
|
||||
track.len = 0;
|
||||
track.samples = samples;
|
||||
moof = MP4.moof(track.sequenceNumber++, firstDTS / pes2mp4ScaleFactor, track);
|
||||
track.samples = [];
|
||||
this.observer.trigger(Event.FRAG_PARSING_DATA, {
|
||||
moof: moof,
|
||||
mdat: mdat,
|
||||
startPTS: firstPTS / pesTimeScale,
|
||||
endPTS: this.nextAacPts / pesTimeScale,
|
||||
startDTS: firstDTS / pesTimeScale,
|
||||
endDTS: (dtsnorm + pes2mp4ScaleFactor * mp4Sample.duration) / pesTimeScale,
|
||||
type: 'audio',
|
||||
nb: samples.length
|
||||
});
|
||||
}
|
||||
|
||||
remuxID3(track,timeOffset) {
|
||||
|
|
83
dashboard-ui/bower_components/hls.js/src/utils/attr-list.js
vendored
Normal file
83
dashboard-ui/bower_components/hls.js/src/utils/attr-list.js
vendored
Normal file
|
@ -0,0 +1,83 @@
|
|||
|
||||
// adapted from https://github.com/kanongil/node-m3u8parse/blob/master/attrlist.js
|
||||
class AttrList {
|
||||
|
||||
constructor(attrs) {
|
||||
if (typeof attrs === 'string') {
|
||||
attrs = AttrList.parseAttrList(attrs);
|
||||
}
|
||||
for(var attr in attrs){
|
||||
if(attrs.hasOwnProperty(attr)) {
|
||||
this[attr] = attrs[attr];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decimalInteger(attrName) {
|
||||
const intValue = parseInt(this[attrName], 10);
|
||||
if (intValue > Number.MAX_SAFE_INTEGER) {
|
||||
return Infinity;
|
||||
}
|
||||
return intValue;
|
||||
}
|
||||
|
||||
hexadecimalInteger(attrName) {
|
||||
if(this[attrName]) {
|
||||
let stringValue = (this[attrName] || '0x').slice(2);
|
||||
stringValue = ((stringValue.length & 1) ? '0' : '') + stringValue;
|
||||
|
||||
const value = new Uint8Array(stringValue.length / 2);
|
||||
for (let i = 0; i < stringValue.length / 2; i++) {
|
||||
value[i] = parseInt(stringValue.slice(i * 2, i * 2 + 2), 16);
|
||||
}
|
||||
return value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
hexadecimalIntegerAsNumber(attrName) {
|
||||
const intValue = parseInt(this[attrName], 16);
|
||||
if (intValue > Number.MAX_SAFE_INTEGER) {
|
||||
return Infinity;
|
||||
}
|
||||
return intValue;
|
||||
}
|
||||
|
||||
decimalFloatingPoint(attrName) {
|
||||
return parseFloat(this[attrName]);
|
||||
}
|
||||
|
||||
enumeratedString(attrName) {
|
||||
return this[attrName];
|
||||
}
|
||||
|
||||
decimalResolution(attrName) {
|
||||
const res = /^(\d+)x(\d+)$/.exec(this[attrName]);
|
||||
if (res === null) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
width: parseInt(res[1], 10),
|
||||
height: parseInt(res[2], 10)
|
||||
};
|
||||
}
|
||||
|
||||
static parseAttrList(input) {
|
||||
const re = /(.+?)=((?:\".*?\")|.*?)(?:,|$)/g;
|
||||
var match, attrs = {};
|
||||
while ((match = re.exec(input)) !== null) {
|
||||
var value = match[2], quote = '"';
|
||||
|
||||
if (value.indexOf(quote) === 0 &&
|
||||
value.lastIndexOf(quote) === (value.length-1)) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
attrs[match[1]] = value;
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default AttrList;
|
|
@ -18,19 +18,21 @@ class XhrLoader {
|
|||
}
|
||||
|
||||
abort() {
|
||||
if (this.loader && this.loader.readyState !== 4) {
|
||||
var loader = this.loader,
|
||||
timeoutHandle = this.timeoutHandle;
|
||||
if (loader && loader.readyState !== 4) {
|
||||
this.stats.aborted = true;
|
||||
this.loader.abort();
|
||||
loader.abort();
|
||||
}
|
||||
if (this.timeoutHandle) {
|
||||
window.clearTimeout(this.timeoutHandle);
|
||||
if (timeoutHandle) {
|
||||
window.clearTimeout(timeoutHandle);
|
||||
}
|
||||
}
|
||||
|
||||
load(url, responseType, onSuccess, onError, onTimeout, timeout, maxRetry, retryDelay, onProgress = null, frag = null) {
|
||||
this.url = url;
|
||||
if (frag && !isNaN(frag.byteRangeStartOffset) && !isNaN(frag.byteRangeEndOffset)) {
|
||||
this.byteRange = frag.byteRangeStartOffset + '-' + frag.byteRangeEndOffset;
|
||||
this.byteRange = frag.byteRangeStartOffset + '-' + (frag.byteRangeEndOffset-1);
|
||||
}
|
||||
this.responseType = responseType;
|
||||
this.onSuccess = onSuccess;
|
||||
|
@ -47,9 +49,9 @@ class XhrLoader {
|
|||
|
||||
loadInternal() {
|
||||
var xhr = this.loader = new XMLHttpRequest();
|
||||
xhr.onload = this.loadsuccess.bind(this);
|
||||
xhr.onerror = this.loaderror.bind(this);
|
||||
xhr.onreadystatechange = this.statechange.bind(this);
|
||||
xhr.onprogress = this.loadprogress.bind(this);
|
||||
|
||||
xhr.open('GET', this.url, true);
|
||||
if (this.byteRange) {
|
||||
xhr.setRequestHeader('Range', 'bytes=' + this.byteRange);
|
||||
|
@ -63,24 +65,33 @@ class XhrLoader {
|
|||
xhr.send();
|
||||
}
|
||||
|
||||
loadsuccess(event) {
|
||||
window.clearTimeout(this.timeoutHandle);
|
||||
this.stats.tload = performance.now();
|
||||
this.onSuccess(event, this.stats);
|
||||
}
|
||||
|
||||
loaderror(event) {
|
||||
if (this.stats.retry < this.maxRetry) {
|
||||
logger.warn(`${event.type} while loading ${this.url}, retrying in ${this.retryDelay}...`);
|
||||
this.destroy();
|
||||
window.setTimeout(this.loadInternal.bind(this), this.retryDelay);
|
||||
// exponential backoff
|
||||
this.retryDelay = Math.min(2 * this.retryDelay, 64000);
|
||||
this.stats.retry++;
|
||||
} else {
|
||||
window.clearTimeout(this.timeoutHandle);
|
||||
logger.error(`${event.type} while loading ${this.url}` );
|
||||
this.onError(event);
|
||||
statechange(event) {
|
||||
var xhr = event.currentTarget,
|
||||
status = xhr.status,
|
||||
stats = this.stats;
|
||||
// don't proceed if xhr has been aborted
|
||||
// 4 = Response from server has been completely loaded.
|
||||
if (!stats.aborted && xhr.readyState === 4) {
|
||||
// http status between 200 to 299 are all successful
|
||||
if (status >= 200 && status < 300) {
|
||||
window.clearTimeout(this.timeoutHandle);
|
||||
stats.tload = performance.now();
|
||||
this.onSuccess(event, stats);
|
||||
} else {
|
||||
// error ...
|
||||
if (stats.retry < this.maxRetry) {
|
||||
logger.warn(`${status} while loading ${this.url}, retrying in ${this.retryDelay}...`);
|
||||
this.destroy();
|
||||
window.setTimeout(this.loadInternal.bind(this), this.retryDelay);
|
||||
// exponential backoff
|
||||
this.retryDelay = Math.min(2 * this.retryDelay, 64000);
|
||||
stats.retry++;
|
||||
} else {
|
||||
window.clearTimeout(this.timeoutHandle);
|
||||
logger.error(`${status} while loading ${this.url}` );
|
||||
this.onError(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue