view release on metacpan or search on metacpan
share/public_html/static/hls.js view on Meta::CPAN
break;
default:
}
}
return out;
};
return ID3;
}();
var utf8ArrayToStr = ID3._utf8ArrayToStr;
/* harmony default export */ __webpack_exports__["a"] = (ID3);
/***/ }),
/* 6 */
/***/ (function(module, exports) {
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
function EventEmitter() {
this._events = this._events || {};
this._maxListeners = this._maxListeners || undefined;
}
module.exports = EventEmitter;
// Backwards-compat with node 0.10.x
EventEmitter.EventEmitter = EventEmitter;
EventEmitter.prototype._events = undefined;
EventEmitter.prototype._maxListeners = undefined;
// By default EventEmitters will print a warning if more than 10 listeners are
// added to it. This is a useful default which helps finding memory leaks.
EventEmitter.defaultMaxListeners = 10;
// Obviously not all Emitters should be limited to 10. This function allows
// that to be increased. Set to zero for unlimited.
EventEmitter.prototype.setMaxListeners = function(n) {
if (!isNumber(n) || n < 0 || isNaN(n))
throw TypeError('n must be a positive number');
this._maxListeners = n;
return this;
};
EventEmitter.prototype.emit = function(type) {
var er, handler, len, args, i, listeners;
if (!this._events)
this._events = {};
// If there is no 'error' event listener then throw.
if (type === 'error') {
if (!this._events.error ||
(isObject(this._events.error) && !this._events.error.length)) {
er = arguments[1];
if (er instanceof Error) {
throw er; // Unhandled 'error' event
} else {
// At least give some kind of context to the user
var err = new Error('Uncaught, unspecified "error" event. (' + er + ')');
err.context = er;
throw err;
}
}
}
handler = this._events[type];
if (isUndefined(handler))
return false;
if (isFunction(handler)) {
switch (arguments.length) {
// fast cases
case 1:
handler.call(this);
break;
case 2:
handler.call(this, arguments[1]);
break;
case 3:
handler.call(this, arguments[1], arguments[2]);
break;
// slower
default:
args = Array.prototype.slice.call(arguments, 1);
handler.apply(this, args);
}
} else if (isObject(handler)) {
args = Array.prototype.slice.call(arguments, 1);
listeners = handler.slice();
len = listeners.length;
for (i = 0; i < len; i++)
listeners[i].apply(this, args);
}
return true;
};
share/public_html/static/hls.js view on Meta::CPAN
var audioGroups = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
var result = void 0;
var medias = [];
var id = 0;
MASTER_PLAYLIST_MEDIA_REGEX.lastIndex = 0;
while ((result = MASTER_PLAYLIST_MEDIA_REGEX.exec(string)) !== null) {
var media = {};
var attrs = new attr_list(result[1]);
if (attrs.TYPE === type) {
media.groupId = attrs['GROUP-ID'];
media.name = attrs.NAME;
media.type = type;
media.default = attrs.DEFAULT === 'YES';
media.autoselect = attrs.AUTOSELECT === 'YES';
media.forced = attrs.FORCED === 'YES';
if (attrs.URI) {
media.url = M3U8Parser.resolve(attrs.URI, baseurl);
}
media.lang = attrs.LANGUAGE;
if (!media.name) {
media.name = media.lang;
}
if (audioGroups.length) {
var groupCodec = M3U8Parser.findGroup(audioGroups, media.groupId);
media.audioCodec = groupCodec ? groupCodec.codec : audioGroups[0].codec;
}
media.id = id++;
medias.push(media);
}
}
return medias;
};
M3U8Parser.parseLevelPlaylist = function parseLevelPlaylist(string, baseurl, id, type, levelUrlId) {
var currentSN = 0,
totalduration = 0,
level = { type: null, version: null, url: baseurl, fragments: [], live: true, startSN: 0 },
levelkey = new level_key(),
cc = 0,
prevFrag = null,
frag = new loader_fragment(),
result = void 0,
i = void 0;
LEVEL_PLAYLIST_REGEX_FAST.lastIndex = 0;
while ((result = LEVEL_PLAYLIST_REGEX_FAST.exec(string)) !== null) {
var duration = result[1];
if (duration) {
// INF
frag.duration = parseFloat(duration);
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
var title = (' ' + result[2]).slice(1);
frag.title = title || null;
frag.tagList.push(title ? ['INF', duration, title] : ['INF', duration]);
} else if (result[3]) {
// url
if (!isNaN(frag.duration)) {
var sn = currentSN++;
frag.type = type;
frag.start = totalduration;
frag.levelkey = levelkey;
frag.sn = sn;
frag.level = id;
frag.cc = cc;
frag.urlId = levelUrlId;
frag.baseurl = baseurl;
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
frag.relurl = (' ' + result[3]).slice(1);
if (level.programDateTime) {
if (prevFrag) {
if (frag.rawProgramDateTime) {
// PDT discontinuity found
frag.pdt = Date.parse(frag.rawProgramDateTime);
} else {
// Contiguous fragment
frag.pdt = prevFrag.pdt + prevFrag.duration * 1000;
}
} else {
// First fragment
frag.pdt = Date.parse(level.programDateTime);
}
frag.endPdt = frag.pdt + frag.duration * 1000;
}
level.fragments.push(frag);
prevFrag = frag;
totalduration += frag.duration;
frag = new loader_fragment();
}
} else if (result[4]) {
// X-BYTERANGE
frag.rawByteRange = (' ' + result[4]).slice(1);
if (prevFrag) {
var lastByteRangeEndOffset = prevFrag.byteRangeEndOffset;
if (lastByteRangeEndOffset) {
frag.lastByteRangeEndOffset = lastByteRangeEndOffset;
}
}
} else if (result[5]) {
// PROGRAM-DATE-TIME
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
frag.rawProgramDateTime = (' ' + result[5]).slice(1);
frag.tagList.push(['PROGRAM-DATE-TIME', frag.rawProgramDateTime]);
if (level.programDateTime === undefined) {
level.programDateTime = new Date(new Date(Date.parse(result[5])) - 1000 * totalduration);
}
} else {
result = result[0].match(LEVEL_PLAYLIST_REGEX_SLOW);
for (i = 1; i < result.length; i++) {
if (result[i] !== undefined) {
break;
}
}
// avoid sliced strings https://github.com/video-dev/hls.js/issues/939
var value1 = (' ' + result[i + 1]).slice(1);
var value2 = (' ' + result[i + 2]).slice(1);
switch (result[i]) {
case '#':
frag.tagList.push(value2 ? [value1, value2] : [value1]);
break;
case 'PLAYLIST-TYPE':
level.type = value1.toUpperCase();
break;
case 'MEDIA-SEQUENCE':
currentSN = level.startSN = parseInt(value1);
break;
case 'TARGETDURATION':
level.targetduration = parseFloat(value1);
break;
case 'VERSION':
level.version = parseInt(value1);
break;
case 'EXTM3U':
break;
case 'ENDLIST':
level.live = false;
break;
case 'DIS':
cc++;
frag.tagList.push(['DIS']);
break;
case 'DISCONTINUITY-SEQ':
cc = parseInt(value1);
break;
case 'KEY':
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-08#section-3.4.4
var decryptparams = value1;
var keyAttrs = new attr_list(decryptparams);
var decryptmethod = keyAttrs.enumeratedString('METHOD'),
decrypturi = keyAttrs.URI,
decryptiv = keyAttrs.hexadecimalInteger('IV');
if (decryptmethod) {
levelkey = new level_key();
if (decrypturi && ['AES-128', 'SAMPLE-AES', 'SAMPLE-AES-CENC'].indexOf(decryptmethod) >= 0) {
levelkey.method = decryptmethod;
// URI to get the key
levelkey.baseuri = baseurl;
levelkey.reluri = decrypturi;
levelkey.key = null;
// Initialization Vector (IV)
levelkey.iv = decryptiv;
}
}
break;
case 'START':
var startParams = value1;
var startAttrs = new attr_list(startParams);
var startTimeOffset = startAttrs.decimalFloatingPoint('TIME-OFFSET');
// TIME-OFFSET can be 0
if (!isNaN(startTimeOffset)) {
level.startTimeOffset = startTimeOffset;
}
break;
case 'MAP':
var mapAttrs = new attr_list(value1);
frag.relurl = mapAttrs.URI;
frag.rawByteRange = mapAttrs.BYTERANGE;
frag.baseurl = baseurl;
frag.level = id;
frag.type = type;
frag.sn = 'initSegment';
level.initSegment = frag;
frag = new loader_fragment();
break;
default:
logger["b" /* logger */].warn('line parsed but not handled: ' + result);
break;
}
}
}
frag = prevFrag;
// logger.log('found ' + level.fragments.length + ' fragments');
if (frag && !frag.relurl) {
level.fragments.pop();
totalduration -= frag.duration;
}
level.totalduration = totalduration;
level.averagetargetduration = totalduration / level.fragments.length;
level.endSN = currentSN - 1;
level.startCC = level.fragments[0] ? level.fragments[0].cc : 0;
level.endCC = cc;
if (!level.initSegment && level.fragments.length) {
// this is a bit lurky but HLS really has no other way to tell us
// if the fragments are TS or MP4, except if we download them :/
// but this is to be able to handle SIDX.
if (level.fragments.every(function (frag) {
return MP4_REGEX_SUFFIX.test(frag.relurl);
})) {
logger["b" /* logger */].warn('MP4 fragments found but no init segment (probably no MAP, incomplete M3U8), trying to fetch SIDX');
frag = new loader_fragment();
frag.relurl = level.fragments[0].relurl;
frag.baseurl = baseurl;
frag.level = id;
frag.type = type;
frag.sn = 'initSegment';
level.initSegment = frag;
level.needSidxRanges = true;
}
}
return level;
};
return M3U8Parser;
}();
share/public_html/static/hls.js view on Meta::CPAN
var levels = m3u8_parser.parseMasterPlaylist(string, url);
if (!levels.length) {
this._handleManifestParsingError(response, context, 'no level found in manifest', networkDetails);
return;
}
// multi level playlist, parse level info
var audioGroups = levels.map(function (level) {
return {
id: level.attrs.AUDIO,
codec: level.audioCodec
};
});
var audioTracks = m3u8_parser.parseMasterPlaylistMedia(string, url, 'AUDIO', audioGroups);
var subtitles = m3u8_parser.parseMasterPlaylistMedia(string, url, 'SUBTITLES');
if (audioTracks.length) {
// check if we have found an audio track embedded in main playlist (audio track without URI attribute)
var embeddedAudioFound = false;
audioTracks.forEach(function (audioTrack) {
if (!audioTrack.url) {
embeddedAudioFound = true;
}
});
// if no embedded audio track defined, but audio codec signaled in quality level,
// we need to signal this main audio track this could happen with playlists with
// alt audio rendition in which quality levels (main)
// contains both audio+video. but with mixed audio track not signaled
if (embeddedAudioFound === false && levels[0].audioCodec && !levels[0].attrs.AUDIO) {
logger["b" /* logger */].log('audio codec signaled in quality level, but no embedded audio track signaled, create one');
audioTracks.unshift({
type: 'main',
name: 'main'
});
}
}
hls.trigger(events["a" /* default */].MANIFEST_LOADED, {
levels: levels,
audioTracks: audioTracks,
subtitles: subtitles,
url: url,
stats: stats,
networkDetails: networkDetails
});
};
PlaylistLoader.prototype._handleTrackOrLevelPlaylist = function _handleTrackOrLevelPlaylist(response, stats, context, networkDetails) {
var hls = this.hls;
var id = context.id,
level = context.level,
type = context.type;
var url = PlaylistLoader.getResponseUrl(response, context);
var levelUrlId = isNaN(id) ? 0 : id;
var levelId = isNaN(level) ? levelUrlId : level; // level -> id -> 0
var levelType = PlaylistLoader.mapContextToLevelType(context);
var levelDetails = m3u8_parser.parseLevelPlaylist(response.data, url, levelId, levelType, levelUrlId);
// set stats on level structure
levelDetails.tload = stats.tload;
// We have done our first request (Manifest-type) and receive
// not a master playlist but a chunk-list (track/level)
// We fire the manifest-loaded event anyway with the parsed level-details
// by creating a single-level structure for it.
if (type === ContextType.MANIFEST) {
var singleLevel = {
url: url,
details: levelDetails
};
hls.trigger(events["a" /* default */].MANIFEST_LOADED, {
levels: [singleLevel],
audioTracks: [],
url: url,
stats: stats,
networkDetails: networkDetails
});
}
// save parsing time
stats.tparsed = performance.now();
// in case we need SIDX ranges
// return early after calling load for
// the SIDX box.
if (levelDetails.needSidxRanges) {
var sidxUrl = levelDetails.initSegment.url;
this.load(sidxUrl, {
isSidxRequest: true,
type: type,
level: level,
levelDetails: levelDetails,
id: id,
rangeStart: 0,
rangeEnd: 2048,
responseType: 'arraybuffer'
});
return;
}
// extend the context with the new levelDetails property
context.levelDetails = levelDetails;
this._handlePlaylistLoaded(response, stats, context, networkDetails);
};
PlaylistLoader.prototype._handleSidxRequest = function _handleSidxRequest(response, context) {
var sidxInfo = mp4demuxer["a" /* default */].parseSegmentIndex(new Uint8Array(response.data));
sidxInfo.references.forEach(function (segmentRef, index) {
var segRefInfo = segmentRef.info;
var frag = context.levelDetails.fragments[index];
if (frag.byteRange.length === 0) {
share/public_html/static/hls.js view on Meta::CPAN
*/
var fragment_loader_FragmentLoader = function (_EventHandler) {
fragment_loader__inherits(FragmentLoader, _EventHandler);
function FragmentLoader(hls) {
fragment_loader__classCallCheck(this, FragmentLoader);
var _this = fragment_loader__possibleConstructorReturn(this, _EventHandler.call(this, hls, events["a" /* default */].FRAG_LOADING));
_this.loaders = {};
return _this;
}
FragmentLoader.prototype.destroy = function destroy() {
var loaders = this.loaders;
for (var loaderName in loaders) {
var loader = loaders[loaderName];
if (loader) {
loader.destroy();
}
}
this.loaders = {};
_EventHandler.prototype.destroy.call(this);
};
FragmentLoader.prototype.onFragLoading = function onFragLoading(data) {
var frag = data.frag,
type = frag.type,
loaders = this.loaders,
config = this.hls.config,
FragmentILoader = config.fLoader,
DefaultILoader = config.loader;
// reset fragment state
frag.loaded = 0;
var loader = loaders[type];
if (loader) {
logger["b" /* logger */].warn('abort previous fragment loader for type: ' + type);
loader.abort();
}
loader = loaders[type] = frag.loader = config.fLoader ? new FragmentILoader(config) : new DefaultILoader(config);
var loaderContext = void 0,
loaderConfig = void 0,
loaderCallbacks = void 0;
loaderContext = { url: frag.url, frag: frag, responseType: 'arraybuffer', progressData: false };
var start = frag.byteRangeStartOffset,
end = frag.byteRangeEndOffset;
if (!isNaN(start) && !isNaN(end)) {
loaderContext.rangeStart = start;
loaderContext.rangeEnd = end;
}
loaderConfig = {
timeout: config.fragLoadingTimeOut,
maxRetry: 0,
retryDelay: 0,
maxRetryDelay: config.fragLoadingMaxRetryTimeout
};
loaderCallbacks = {
onSuccess: this.loadsuccess.bind(this),
onError: this.loaderror.bind(this),
onTimeout: this.loadtimeout.bind(this),
onProgress: this.loadprogress.bind(this)
};
loader.load(loaderContext, loaderConfig, loaderCallbacks);
};
FragmentLoader.prototype.loadsuccess = function loadsuccess(response, stats, context) {
var networkDetails = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;
var payload = response.data,
frag = context.frag;
// detach fragment loader on load success
frag.loader = undefined;
this.loaders[frag.type] = undefined;
this.hls.trigger(events["a" /* default */].FRAG_LOADED, { payload: payload, frag: frag, stats: stats, networkDetails: networkDetails });
};
FragmentLoader.prototype.loaderror = function loaderror(response, context) {
var networkDetails = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
var loader = context.loader;
if (loader) {
loader.abort();
}
this.loaders[context.type] = undefined;
this.hls.trigger(events["a" /* default */].ERROR, { type: errors["b" /* ErrorTypes */].NETWORK_ERROR, details: errors["a" /* ErrorDetails */].FRAG_LOAD_ERROR, fatal: false, frag: context.frag, response: response, networkDetails: networkDetails })...
};
FragmentLoader.prototype.loadtimeout = function loadtimeout(stats, context) {
var networkDetails = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
var loader = context.loader;
if (loader) {
loader.abort();
}
this.loaders[context.type] = undefined;
this.hls.trigger(events["a" /* default */].ERROR, { type: errors["b" /* ErrorTypes */].NETWORK_ERROR, details: errors["a" /* ErrorDetails */].FRAG_LOAD_TIMEOUT, fatal: false, frag: context.frag, networkDetails: networkDetails });
};
// data will be used for progressive parsing
FragmentLoader.prototype.loadprogress = function loadprogress(stats, context, data) {
share/public_html/static/hls.js view on Meta::CPAN
});
return bestFragment;
};
/**
* @param {Object} fragment The fragment to check
* @returns {String} Returns the fragment state when a fragment never loaded or if it partially loaded
*/
FragmentTracker.prototype.getState = function getState(fragment) {
var fragKey = this.getFragmentKey(fragment);
var fragmentEntity = this.fragments[fragKey];
var state = FragmentState.NOT_LOADED;
if (fragmentEntity !== undefined) {
if (!fragmentEntity.buffered) {
state = FragmentState.APPENDING;
} else if (this.isPartial(fragmentEntity) === true) {
state = FragmentState.PARTIAL;
} else {
state = FragmentState.OK;
}
}
return state;
};
FragmentTracker.prototype.isPartial = function isPartial(fragmentEntity) {
return fragmentEntity.buffered === true && (fragmentEntity.range.video !== undefined && fragmentEntity.range.video.partial === true || fragmentEntity.range.audio !== undefined && fragmentEntity.range.audio.partial === true);
};
FragmentTracker.prototype.isTimeBuffered = function isTimeBuffered(startPTS, endPTS, timeRange) {
var startTime = void 0,
endTime = void 0;
for (var i = 0; i < timeRange.length; i++) {
startTime = timeRange.start(i) - this.bufferPadding;
endTime = timeRange.end(i) + this.bufferPadding;
if (startPTS >= startTime && endPTS <= endTime) {
return true;
}
if (endPTS <= startTime) {
// No need to check the rest of the timeRange as it is in order
return false;
}
}
return false;
};
/**
* Fires when a fragment loading is completed
*/
FragmentTracker.prototype.onFragLoaded = function onFragLoaded(e) {
var fragment = e.frag;
// don't track initsegment (for which sn is not a number)
// don't track frags used for bitrateTest, they're irrelevant.
if (!isNaN(fragment.sn) && !fragment.bitrateTest) {
var fragKey = this.getFragmentKey(fragment);
var fragmentEntity = {
body: fragment,
range: Object.create(null),
buffered: false
};
this.fragments[fragKey] = fragmentEntity;
}
};
/**
* Fires when the buffer is updated
*/
FragmentTracker.prototype.onBufferAppended = function onBufferAppended(e) {
var _this5 = this;
// Store the latest timeRanges loaded in the buffer
this.timeRanges = e.timeRanges;
Object.keys(this.timeRanges).forEach(function (elementaryStream) {
var timeRange = _this5.timeRanges[elementaryStream];
_this5.detectEvictedFragments(elementaryStream, timeRange);
});
};
/**
* Fires after a fragment has been loaded into the source buffer
*/
FragmentTracker.prototype.onFragBuffered = function onFragBuffered(e) {
this.detectPartialFragments(e.frag);
};
/**
* Return true if fragment tracker has the fragment.
* @param {Object} fragment
* @returns {boolean}
*/
FragmentTracker.prototype.hasFragment = function hasFragment(fragment) {
var fragKey = this.getFragmentKey(fragment);
return this.fragments[fragKey] !== undefined;
};
/**
* Remove a fragment from fragment tracker until it is loaded again
* @param {Object} fragment The fragment to remove
*/
FragmentTracker.prototype.removeFragment = function removeFragment(fragment) {
var fragKey = this.getFragmentKey(fragment);
delete this.fragments[fragKey];
};
/**
* Remove all fragments from fragment tracker.
share/public_html/static/hls.js view on Meta::CPAN
observer.on(events["a" /* default */].ERROR, forwardMessage);
observer.on(events["a" /* default */].FRAG_PARSING_METADATA, forwardMessage);
observer.on(events["a" /* default */].FRAG_PARSING_USERDATA, forwardMessage);
observer.on(events["a" /* default */].INIT_PTS_FOUND, forwardMessage);
var typeSupported = {
mp4: MediaSource.isTypeSupported('video/mp4'),
mpeg: MediaSource.isTypeSupported('audio/mpeg'),
mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"')
};
// navigator.vendor is not always available in Web Worker
// refer to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/navigator
var vendor = navigator.vendor;
if (config.enableWorker && typeof Worker !== 'undefined') {
logger["b" /* logger */].log('demuxing in webworker');
var w = void 0;
try {
w = this.w = webworkify_webpack_default()(/*require.resolve*/(12));
this.onwmsg = this.onWorkerMessage.bind(this);
w.addEventListener('message', this.onwmsg);
w.onerror = function (event) {
hls.trigger(events["a" /* default */].ERROR, { type: errors["b" /* ErrorTypes */].OTHER_ERROR, details: errors["a" /* ErrorDetails */].INTERNAL_EXCEPTION, fatal: true, event: 'demuxerWorker', err: { message: event.message + ' (' + event.fil...
};
w.postMessage({ cmd: 'init', typeSupported: typeSupported, vendor: vendor, id: id, config: JSON.stringify(config) });
} catch (err) {
logger["b" /* logger */].error('error while initializing DemuxerWorker, fallback on DemuxerInline');
if (w) {
// revoke the Object URL that was used to create demuxer worker, so as not to leak it
global.URL.revokeObjectURL(w.objectURL);
}
this.demuxer = new demuxer_inline["a" /* default */](observer, typeSupported, config, vendor);
this.w = undefined;
}
} else {
this.demuxer = new demuxer_inline["a" /* default */](observer, typeSupported, config, vendor);
}
}
Demuxer.prototype.destroy = function destroy() {
var w = this.w;
if (w) {
w.removeEventListener('message', this.onwmsg);
w.terminate();
this.w = null;
} else {
var demuxer = this.demuxer;
if (demuxer) {
demuxer.destroy();
this.demuxer = null;
}
}
var observer = this.observer;
if (observer) {
observer.removeAllListeners();
this.observer = null;
}
};
Demuxer.prototype.push = function push(data, initSegment, audioCodec, videoCodec, frag, duration, accurateTimeOffset, defaultInitPTS) {
var w = this.w;
var timeOffset = !isNaN(frag.startDTS) ? frag.startDTS : frag.start;
var decryptdata = frag.decryptdata;
var lastFrag = this.frag;
var discontinuity = !(lastFrag && frag.cc === lastFrag.cc);
var trackSwitch = !(lastFrag && frag.level === lastFrag.level);
var nextSN = lastFrag && frag.sn === lastFrag.sn + 1;
var contiguous = !trackSwitch && nextSN;
if (discontinuity) {
logger["b" /* logger */].log(this.id + ':discontinuity detected');
}
if (trackSwitch) {
logger["b" /* logger */].log(this.id + ':switch detected');
}
this.frag = frag;
if (w) {
// post fragment payload as transferable objects for ArrayBuffer (no copy)
w.postMessage({ cmd: 'demux', data: data, decryptdata: decryptdata, initSegment: initSegment, audioCodec: audioCodec, videoCodec: videoCodec, timeOffset: timeOffset, discontinuity: discontinuity, trackSwitch: trackSwitch, contiguous: contiguous...
} else {
var demuxer = this.demuxer;
if (demuxer) {
demuxer.push(data, decryptdata, initSegment, audioCodec, videoCodec, timeOffset, discontinuity, trackSwitch, contiguous, duration, accurateTimeOffset, defaultInitPTS);
}
}
};
Demuxer.prototype.onWorkerMessage = function onWorkerMessage(ev) {
var data = ev.data,
hls = this.hls;
switch (data.event) {
case 'init':
// revoke the Object URL that was used to create demuxer worker, so as not to leak it
global.URL.revokeObjectURL(this.w.objectURL);
break;
// special case for FRAG_PARSING_DATA: data1 and data2 are transferable objects
case events["a" /* default */].FRAG_PARSING_DATA:
data.data.data1 = new Uint8Array(data.data1);
if (data.data2) {
data.data.data2 = new Uint8Array(data.data2);
}
/* falls through */
default:
data.data = data.data || {};
data.data.frag = this.frag;
data.data.id = this.id;
hls.trigger(data.event, data.data);
break;
}
};
return Demuxer;
}();
/* harmony default export */ var demux_demuxer = (demuxer_Demuxer);
// CONCATENATED MODULE: ./src/controller/level-helper.js
/**
* @module LevelHelper
*
* Providing methods dealing with playlist sliding and drift
*
* TODO: Create an actual `Level` class/model that deals with all this logic in an object-oriented-manner.
*
* */
function addGroupId(level, type, id) {
switch (type) {
case 'audio':
if (!level.audioGroupIds) {
level.audioGroupIds = [];
}
level.audioGroupIds.push(id);
break;
case 'text':
if (!level.textGroupIds) {
level.textGroupIds = [];
}
level.textGroupIds.push(id);
break;
}
}
function updatePTS(fragments, fromIdx, toIdx) {
var fragFrom = fragments[fromIdx],
fragTo = fragments[toIdx],
fragToPTS = fragTo.startPTS;
// if we know startPTS[toIdx]
if (!isNaN(fragToPTS)) {
// update fragment duration.
// it helps to fix drifts between playlist reported duration and fragment real duration
if (toIdx > fromIdx) {
fragFrom.duration = fragToPTS - fragFrom.start;
if (fragFrom.duration < 0) {
logger["b" /* logger */].warn('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["b" /* logger */].warn('negative duration computed for frag ' + fragTo.sn + ',level ' + fragTo.level + ', there should be some duration drift between playlist and fragment!');
}
}
} else {
// we dont know startPTS[toIdx]
if (toIdx > fromIdx) {
fragTo.start = fragFrom.start + fragFrom.duration;
} else {
fragTo.start = Math.max(fragFrom.start - fragTo.duration, 0);
}
}
}
function updateFragPTSDTS(details, frag, startPTS, endPTS, startDTS, endDTS) {
// update frag PTS/DTS
var maxStartPTS = startPTS;
if (!isNaN(frag.startPTS)) {
// delta PTS between audio and video
var deltaPTS = Math.abs(frag.startPTS - startPTS);
if (isNaN(frag.deltaPTS)) {
frag.deltaPTS = deltaPTS;
} else {
frag.deltaPTS = Math.max(deltaPTS, frag.deltaPTS);
}
maxStartPTS = Math.max(startPTS, frag.startPTS);
startPTS = Math.min(startPTS, frag.startPTS);
endPTS = Math.max(endPTS, frag.endPTS);
startDTS = Math.min(startDTS, frag.startDTS);
endDTS = Math.max(endDTS, frag.endDTS);
}
var drift = startPTS - frag.start;
frag.start = frag.startPTS = startPTS;
frag.maxStartPTS = maxStartPTS;
frag.endPTS = endPTS;
frag.startDTS = startDTS;
frag.endDTS = endDTS;
frag.duration = endPTS - startPTS;
var sn = frag.sn;
// exit if sn out of range
if (!details || sn < details.startSN || sn > details.endSN) {
return 0;
}
var fragIdx = void 0,
fragments = void 0,
i = void 0;
fragIdx = sn - details.startSN;
fragments = details.fragments;
// update frag reference in fragments array
// rationale is that fragments array might not contain this frag object.
// this will happpen if playlist has been refreshed between frag loading and call to updateFragPTSDTS()
// if we don't update frag, we won't be able to propagate PTS info on the playlist
// resulting in invalid sliding computation
fragments[fragIdx] = frag;
// adjust fragment PTS/duration from seqnum-1 to frag 0
for (i = fragIdx; i > 0; i--) {
updatePTS(fragments, i, i - 1);
}
// adjust fragment PTS/duration from seqnum to last frag
for (i = fragIdx; i < fragments.length - 1; i++) {
updatePTS(fragments, i, i + 1);
}
details.PTSKnown = true;
// logger.log(` frag start/end:${startPTS.toFixed(3)}/${endPTS.toFixed(3)}`);
return drift;
}
function mergeDetails(oldDetails, newDetails) {
var start = Math.max(oldDetails.startSN, newDetails.startSN) - newDetails.startSN,
end = Math.min(oldDetails.endSN, newDetails.endSN) - newDetails.startSN,
delta = newDetails.startSN - oldDetails.startSN,
oldfragments = oldDetails.fragments,
newfragments = newDetails.fragments,
ccOffset = 0,
PTSFrag = void 0;
// potentially retrieve cached initsegment
if (newDetails.initSegment && oldDetails.initSegment) {
newDetails.initSegment = oldDetails.initSegment;
}
// check if old/new playlists have fragments in common
if (end < start) {
newDetails.PTSKnown = false;
return;
}
// loop through overlapping SN and update startPTS , cc, and duration if any found
for (var i = start; i <= end; i++) {
var oldFrag = oldfragments[delta + i],
newFrag = newfragments[i];
if (newFrag && oldFrag) {
ccOffset = oldFrag.cc - newFrag.cc;
if (!isNaN(oldFrag.startPTS)) {
newFrag.start = newFrag.startPTS = oldFrag.startPTS;
newFrag.endPTS = oldFrag.endPTS;
newFrag.duration = oldFrag.duration;
newFrag.backtracked = oldFrag.backtracked;
newFrag.dropped = oldFrag.dropped;
PTSFrag = newFrag;
}
}
}
if (ccOffset) {
logger["b" /* logger */].log('discontinuity sliding from playlist, take drift into account');
for (i = 0; i < newfragments.length; i++) {
newfragments[i].cc += ccOffset;
}
}
// if at least one fragment contains PTS info, recompute PTS information for all fragments
if (PTSFrag) {
updateFragPTSDTS(newDetails, PTSFrag, PTSFrag.startPTS, PTSFrag.endPTS, PTSFrag.startDTS, PTSFrag.endDTS);
} else {
// ensure that delta is within oldfragments range
// also adjust sliding in case delta is 0 (we could have old=[50-60] and new=old=[50-61])
// in that case we also need to adjust start offset of all fragments
if (delta >= 0 && delta < oldfragments.length) {
// adjust start by sliding offset
var sliding = oldfragments[delta].start;
for (i = 0; i < newfragments.length; i++) {
newfragments[i].start += sliding;
}
}
}
// if we are here, it means we have fragments overlapping between
// old and new level. reliable PTS info is thus relying on old level
newDetails.PTSKnown = oldDetails.PTSKnown;
}
// CONCATENATED MODULE: ./src/utils/time-ranges.js
/**
* TimeRanges to string helper
*/
var TimeRanges = {
toString: function toString(r) {
var log = '',
len = r.length;
for (var i = 0; i < len; i++) {
log += '[' + r.start(i).toFixed(3) + ',' + r.end(i).toFixed(3) + ']';
}
return log;
}
};
/* harmony default export */ var time_ranges = (TimeRanges);
// CONCATENATED MODULE: ./src/utils/discontinuities.js
function findFirstFragWithCC(fragments, cc) {
var firstFrag = null;
share/public_html/static/hls.js view on Meta::CPAN
var shouldAlign = false;
if (lastLevel && lastLevel.details && details) {
if (details.endCC > details.startCC || lastFrag && lastFrag.cc < details.startCC) {
shouldAlign = true;
}
}
return shouldAlign;
}
// Find the first frag in the previous level which matches the CC of the first frag of the new level
function findDiscontinuousReferenceFrag(prevDetails, curDetails) {
var prevFrags = prevDetails.fragments;
var curFrags = curDetails.fragments;
if (!curFrags.length || !prevFrags.length) {
logger["b" /* logger */].log('No fragments to align');
return;
}
var prevStartFrag = findFirstFragWithCC(prevFrags, curFrags[0].cc);
if (!prevStartFrag || prevStartFrag && !prevStartFrag.startPTS) {
logger["b" /* logger */].log('No frag in previous level to align on');
return;
}
return prevStartFrag;
}
function adjustPts(sliding, details) {
details.fragments.forEach(function (frag) {
if (frag) {
var start = frag.start + sliding;
frag.start = frag.startPTS = start;
frag.endPTS = start + frag.duration;
}
});
details.PTSKnown = true;
}
// If a change in CC is detected, the PTS can no longer be relied upon
// Attempt to align the level by using the last level - find the last frag matching the current CC and use it's PTS
// as a reference
function alignDiscontinuities(lastFrag, lastLevel, details) {
if (shouldAlignOnDiscontinuities(lastFrag, lastLevel, details)) {
var referenceFrag = findDiscontinuousReferenceFrag(lastLevel.details, details);
if (referenceFrag) {
logger["b" /* logger */].log('Adjusting PTS using last level due to CC increase within current level');
adjustPts(referenceFrag.start, details);
}
}
// try to align using programDateTime attribute (if available)
if (details.PTSKnown === false && lastLevel && lastLevel.details && lastLevel.details.fragments && lastLevel.details.fragments.length) {
// if last level sliding is 1000 and its first frag PROGRAM-DATE-TIME is 2017-08-20 1:10:00 AM
// and if new details first frag PROGRAM DATE-TIME is 2017-08-20 1:10:08 AM
// then we can deduce that playlist B sliding is 1000+8 = 1008s
var lastPDT = lastLevel.details.programDateTime;
var newPDT = details.programDateTime;
// date diff is in ms. frag.start is in seconds
var sliding = (newPDT - lastPDT) / 1000 + lastLevel.details.fragments[0].start;
if (!isNaN(sliding)) {
logger["b" /* logger */].log('adjusting PTS using programDateTime delta, sliding:' + sliding.toFixed(3));
adjustPts(sliding, details);
}
}
}
// CONCATENATED MODULE: ./src/task-loop.js
function task_loop__classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function task_loop__possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function task_loop__inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.cre...
/**
* Sub-class specialization of EventHandler base class.
*
* TaskLoop allows to schedule a task function being called (optionnaly repeatedly) on the main loop,
* scheduled asynchroneously, avoiding recursive calls in the same tick.
*
* The task itself is implemented in `doTick`. It can be requested and called for single execution
* using the `tick` method.
*
* It will be assured that the task execution method (`tick`) only gets called once per main loop "tick",
* no matter how often it gets requested for execution. Execution in further ticks will be scheduled accordingly.
*
* If further execution requests have already been scheduled on the next tick, it can be checked with `hasNextTick`,
* and cancelled with `clearNextTick`.
*
* The task can be scheduled as an interval repeatedly with a period as parameter (see `setInterval`, `clearInterval`).
*
* Sub-classes need to implement the `doTick` method which will effectively have the task execution routine.
*
* Further explanations:
*
* The baseclass has a `tick` method that will schedule the doTick call. It may be called synchroneously
* only for a stack-depth of one. On re-entrant calls, sub-sequent calls are scheduled for next main loop ticks.
*
* When the task execution (`tick` method) is called in re-entrant way this is detected and
* we are limiting the task execution per call stack to exactly one, but scheduling/post-poning further
* task processing on the next main loop iteration (also known as "next tick" in the Node/JS runtime lingo).
*/
var TaskLoop = function (_EventHandler) {
task_loop__inherits(TaskLoop, _EventHandler);
function TaskLoop(hls) {
task_loop__classCallCheck(this, TaskLoop);
for (var _len = arguments.length, events = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
events[_key - 1] = arguments[_key];
}
var _this = task_loop__possibleConstructorReturn(this, _EventHandler.call.apply(_EventHandler, [this, hls].concat(events)));
_this._tickInterval = null;
_this._tickTimer = null;
_this._tickCallCount = 0;
_this._boundTick = _this.tick.bind(_this);
return _this;
share/public_html/static/hls.js view on Meta::CPAN
clearTimeout(this._tickTimer);
this._tickTimer = null;
return true;
}
return false;
};
/**
* Will call the subclass doTick implementation in this main loop tick
* or in the next one (via setTimeout(,0)) in case it has already been called
* in this tick (in case this is a re-entrant call).
*/
TaskLoop.prototype.tick = function tick() {
this._tickCallCount++;
if (this._tickCallCount === 1) {
this.doTick();
// re-entrant call to tick from previous doTick call stack
// -> schedule a call on the next main loop iteration to process this task processing request
if (this._tickCallCount > 1) {
// make sure only one timer exists at any time at max
this.clearNextTick();
this._tickTimer = setTimeout(this._boundTick, 0);
}
this._tickCallCount = 0;
}
};
/**
* For subclass to implement task logic
* @abstract
*/
TaskLoop.prototype.doTick = function doTick() {};
return TaskLoop;
}(event_handler);
/* harmony default export */ var task_loop = (TaskLoop);
// CONCATENATED MODULE: ./src/controller/fragment-finders.js
/**
* Calculates the PDT of the next load position.
* bufferEnd in this function is usually the position of the playhead.
* @param {number} [start = 0] - The PTS of the first fragment within the level
* @param {number} [bufferEnd = 0] - The end of the contiguous buffered range the playhead is currently within
* @param {*} levelDetails - An object containing the parsed and computed properties of the currently playing level
* @returns {number} nextPdt - The computed PDT
*/
function calculateNextPDT() {
var start = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
var bufferEnd = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
var levelDetails = arguments[2];
var pdt = 0;
if (levelDetails.programDateTime) {
var parsedDateInt = Date.parse(levelDetails.programDateTime);
if (!isNaN(parsedDateInt)) {
pdt = bufferEnd * 1000 + parsedDateInt - 1000 * start;
}
}
return pdt;
}
/**
* Finds the first fragment whose endPDT value exceeds the given PDT.
* @param {Array<Fragment>} fragments - The array of candidate fragments
* @param {number|null} [PDTValue = null] - The PDT value which must be exceeded
* @returns {*|null} fragment - The best matching fragment
*/
function findFragmentByPDT(fragments) {
var PDTValue = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
if (!Array.isArray(fragments) || !fragments.length || PDTValue === null) {
return null;
}
// if less than start
var firstSegment = fragments[0];
if (PDTValue < firstSegment.pdt) {
return null;
}
var lastSegment = fragments[fragments.length - 1];
if (PDTValue >= lastSegment.endPdt) {
return null;
}
for (var seg = 0; seg < fragments.length; ++seg) {
var frag = fragments[seg];
if (PDTValue < frag.endPdt) {
return frag;
}
}
return null;
}
/**
* Finds a fragment based on the SN of the previous fragment; or based on the needs of the current buffer.
* This method compensates for small buffer gaps by applying a tolerance to the start of any candidate fragment, thus
* breaking any traps which would cause the same fragment to be continuously selected within a small range.
* @param {*} fragPrevious - The last frag successfully appended
* @param {Array<Fragment>} fragments - The array of candidate fragments
* @param {number} [bufferEnd = 0] - The end of the contiguous buffered range the playhead is currently within
* @param {number} [end = 0] - The computed end time of the stream
* @param {number} maxFragLookUpTolerance - The amount of time that a fragment's start can be within in order to be considered contiguous
* @returns {*} foundFrag - The best matching fragment
*/
function findFragmentBySN(fragPrevious, fragments) {
var bufferEnd = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
var end = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
var maxFragLookUpTolerance = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : 0;
var foundFrag = void 0;
var fragNext = fragPrevious ? fragments[fragPrevious.sn - fragments[0].sn + 1] : null;
if (bufferEnd < end) {
share/public_html/static/hls.js view on Meta::CPAN
frag = foundFrag;
var curSNIdx = frag.sn - levelDetails.startSN;
var sameLevel = fragPrevious && frag.level === fragPrevious.level;
var prevFrag = fragments[curSNIdx - 1];
var nextFrag = fragments[curSNIdx + 1];
// logger.log('find SN matching with pos:' + bufferEnd + ':' + frag.sn);
if (fragPrevious && frag.sn === fragPrevious.sn) {
if (sameLevel && !frag.backtracked) {
if (frag.sn < levelDetails.endSN) {
var deltaPTS = fragPrevious.deltaPTS;
// if there is a significant delta between audio and video, larger than max allowed hole,
// and if previous remuxed fragment did not start with a keyframe. (fragPrevious.dropped)
// let's try to load previous fragment again to get last keyframe
// then we will reload again current fragment (that way we should be able to fill the buffer hole ...)
if (deltaPTS && deltaPTS > config.maxBufferHole && fragPrevious.dropped && curSNIdx) {
frag = prevFrag;
logger["b" /* logger */].warn('SN just loaded, with large PTS gap between audio and video, maybe frag is not starting with a keyframe ? load previous one to try to overcome this');
} else {
frag = nextFrag;
logger["b" /* logger */].log('SN just loaded, load next one: ' + frag.sn);
}
} else {
frag = null;
}
} else if (frag.backtracked) {
// Only backtrack a max of 1 consecutive fragment to prevent sliding back too far when little or no frags start with keyframes
if (nextFrag && nextFrag.backtracked) {
logger["b" /* logger */].warn('Already backtracked from fragment ' + nextFrag.sn + ', will not backtrack to fragment ' + frag.sn + '. Loading fragment ' + nextFrag.sn);
frag = nextFrag;
} else {
// If a fragment has dropped frames and it's in a same level/sequence, load the previous fragment to try and find the keyframe
// Reset the dropped count now since it won't be reset until we parse the fragment again, which prevents infinite backtracking on the same segment
logger["b" /* logger */].warn('Loaded fragment with dropped frames, backtracking 1 segment to find a keyframe');
frag.dropped = 0;
if (prevFrag) {
frag = prevFrag;
frag.backtracked = true;
} else if (curSNIdx) {
// can't backtrack on very first fragment
frag = null;
}
}
}
}
}
return frag;
};
StreamController.prototype._loadKey = function _loadKey(frag) {
this.state = State.KEY_LOADING;
this.hls.trigger(events["a" /* default */].KEY_LOADING, { frag: frag });
};
StreamController.prototype._loadFragment = function _loadFragment(frag) {
// Check if fragment is not loaded
var fragState = this.fragmentTracker.getState(frag);
this.fragCurrent = frag;
this.startFragRequested = true;
// Don't update nextLoadPosition for fragments which are not buffered
if (!isNaN(frag.sn) && !frag.bitrateTest) {
this.nextLoadPosition = frag.start + frag.duration;
}
// Allow backtracked fragments to load
if (frag.backtracked || fragState === FragmentState.NOT_LOADED || fragState === FragmentState.PARTIAL) {
frag.autoLevel = this.hls.autoLevelEnabled;
frag.bitrateTest = this.bitrateTest;
this.hls.trigger(events["a" /* default */].FRAG_LOADING, { frag: frag });
// lazy demuxer init, as this could take some time ... do it during frag loading
if (!this.demuxer) {
this.demuxer = new demux_demuxer(this.hls, 'main');
}
this.state = State.FRAG_LOADING;
} else if (fragState === FragmentState.APPENDING) {
// Lower the buffer size and try again
if (this._reduceMaxBufferLength(frag.duration)) {
this.fragmentTracker.removeFragment(frag);
}
}
};
StreamController.prototype.getBufferedFrag = function getBufferedFrag(position) {
return this.fragmentTracker.getBufferedFrag(position, playlist_loader.LevelType.MAIN);
};
StreamController.prototype.followingBufferedFrag = function followingBufferedFrag(frag) {
if (frag) {
// try to get range of next fragment (500ms after this range)
return this.getBufferedFrag(frag.endPTS + 0.5);
}
return null;
};
StreamController.prototype._checkFragmentChanged = function _checkFragmentChanged() {
var fragPlayingCurrent = void 0,
currentTime = void 0,
video = this.media;
if (video && video.readyState && video.seeking === false) {
currentTime = video.currentTime;
/* if video element is in seeked state, currentTime can only increase.
(assuming that playback rate is positive ...)
As sometimes currentTime jumps back to zero after a
media decode error, check this, to avoid seeking back to
wrong position after a media decode error
*/
if (currentTime > this.lastCurrentTime) {
this.lastCurrentTime = currentTime;
}
if (BufferHelper.isBuffered(video, currentTime)) {
fragPlayingCurrent = this.getBufferedFrag(currentTime);
} else if (BufferHelper.isBuffered(video, currentTime + 0.1)) {
/* ensure that FRAG_CHANGED event is triggered at startup,
when first video frame is displayed and playback is paused.
add a tolerance of 100ms, in case current position is not buffered,
check if current pos+100ms is buffered and use that buffer range
for FRAG_CHANGED event reporting */
fragPlayingCurrent = this.getBufferedFrag(currentTime + 0.1);
share/public_html/static/hls.js view on Meta::CPAN
StreamController.prototype.flushMainBuffer = function flushMainBuffer(startOffset, endOffset) {
this.state = State.BUFFER_FLUSHING;
var flushScope = { startOffset: startOffset, endOffset: endOffset };
// if alternate audio tracks are used, only flush video, otherwise flush everything
if (this.altAudio) {
flushScope.type = 'video';
}
this.hls.trigger(events["a" /* default */].BUFFER_FLUSHING, flushScope);
};
StreamController.prototype.onMediaAttached = function onMediaAttached(data) {
var media = this.media = this.mediaBuffer = data.media;
this.onvseeking = this.onMediaSeeking.bind(this);
this.onvseeked = this.onMediaSeeked.bind(this);
this.onvended = this.onMediaEnded.bind(this);
media.addEventListener('seeking', this.onvseeking);
media.addEventListener('seeked', this.onvseeked);
media.addEventListener('ended', this.onvended);
var config = this.config;
if (this.levels && config.autoStartLoad) {
this.hls.startLoad(config.startPosition);
}
};
StreamController.prototype.onMediaDetaching = function onMediaDetaching() {
var media = this.media;
if (media && media.ended) {
logger["b" /* logger */].log('MSE detaching and video ended, reset startPosition');
this.startPosition = this.lastCurrentTime = 0;
}
// reset fragment backtracked flag
var levels = this.levels;
if (levels) {
levels.forEach(function (level) {
if (level.details) {
level.details.fragments.forEach(function (fragment) {
fragment.backtracked = undefined;
});
}
});
}
// remove video listeners
if (media) {
media.removeEventListener('seeking', this.onvseeking);
media.removeEventListener('seeked', this.onvseeked);
media.removeEventListener('ended', this.onvended);
this.onvseeking = this.onvseeked = this.onvended = null;
}
this.media = this.mediaBuffer = null;
this.loadedmetadata = false;
this.stopLoad();
};
StreamController.prototype.onMediaSeeking = function onMediaSeeking() {
var media = this.media,
currentTime = media ? media.currentTime : undefined,
config = this.config;
if (!isNaN(currentTime)) {
logger["b" /* logger */].log('media seeking to ' + currentTime.toFixed(3));
}
var mediaBuffer = this.mediaBuffer ? this.mediaBuffer : media;
var bufferInfo = BufferHelper.bufferInfo(mediaBuffer, currentTime, this.config.maxBufferHole);
if (this.state === State.FRAG_LOADING) {
var fragCurrent = this.fragCurrent;
// check if we are seeking to a unbuffered area AND if frag loading is in progress
if (bufferInfo.len === 0 && fragCurrent) {
var tolerance = config.maxFragLookUpTolerance,
fragStartOffset = fragCurrent.start - tolerance,
fragEndOffset = fragCurrent.start + fragCurrent.duration + tolerance;
// check if we seek position will be out of currently loaded frag range : if out cancel frag load, if in, don't do anything
if (currentTime < fragStartOffset || currentTime > fragEndOffset) {
if (fragCurrent.loader) {
logger["b" /* logger */].log('seeking outside of buffer while fragment load in progress, cancel fragment load');
fragCurrent.loader.abort();
}
this.fragCurrent = null;
this.fragPrevious = null;
// switch to IDLE state to load new fragment
this.state = State.IDLE;
} else {
logger["b" /* logger */].log('seeking outside of buffer but within currently loaded fragment range');
}
}
} else if (this.state === State.ENDED) {
// if seeking to unbuffered area, clean up fragPrevious
if (bufferInfo.len === 0) {
this.fragPrevious = 0;
}
// switch to IDLE state to check for potential new fragment
this.state = State.IDLE;
}
if (media) {
this.lastCurrentTime = currentTime;
}
// in case seeking occurs although no media buffered, adjust startPosition and nextLoadPosition to seek target
if (!this.loadedmetadata) {
this.nextLoadPosition = this.startPosition = currentTime;
}
// tick to speed up processing
this.tick();
};
StreamController.prototype.onMediaSeeked = function onMediaSeeked() {
var media = this.media,
currentTime = media ? media.currentTime : undefined;
if (!isNaN(currentTime)) {
logger["b" /* logger */].log('media seeked to ' + currentTime.toFixed(3));
}
// tick to speed up FRAGMENT_PLAYING triggering
this.tick();
};
StreamController.prototype.onMediaEnded = function onMediaEnded() {
logger["b" /* logger */].log('media ended');
// reset startPosition and lastCurrentTime to restart playback @ stream beginning
this.startPosition = this.lastCurrentTime = 0;
};
StreamController.prototype.onManifestLoading = function onManifestLoading() {
// reset buffer on manifest loading
logger["b" /* logger */].log('trigger BUFFER_RESET');
this.hls.trigger(events["a" /* default */].BUFFER_RESET);
this.fragmentTracker.removeAllFragments();
this.stalled = false;
this.startPosition = this.lastCurrentTime = 0;
};
StreamController.prototype.onManifestParsed = function onManifestParsed(data) {
var aac = false,
heaac = false,
codec = void 0;
data.levels.forEach(function (level) {
// detect if we have different kind of audio codecs used amongst playlists
codec = level.audioCodec;
if (codec) {
if (codec.indexOf('mp4a.40.2') !== -1) {
aac = true;
}
if (codec.indexOf('mp4a.40.5') !== -1) {
heaac = true;
}
}
});
this.audioCodecSwitch = aac && heaac;
if (this.audioCodecSwitch) {
logger["b" /* logger */].log('both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC');
}
this.levels = data.levels;
this.startFragRequested = false;
var config = this.config;
if (config.autoStartLoad || this.forceStartLoad) {
this.hls.startLoad(config.startPosition);
}
};
StreamController.prototype.onLevelLoaded = function onLevelLoaded(data) {
var newDetails = data.details;
var newLevelId = data.level;
var lastLevel = this.levels[this.levelLastLoaded];
var curLevel = this.levels[newLevelId];
var duration = newDetails.totalduration;
var sliding = 0;
logger["b" /* logger */].log('level ' + newLevelId + ' loaded [' + newDetails.startSN + ',' + newDetails.endSN + '],duration:' + duration);
if (newDetails.live) {
var curDetails = curLevel.details;
if (curDetails && newDetails.fragments.length > 0) {
// we already have details for that level, merge them
mergeDetails(curDetails, newDetails);
sliding = newDetails.fragments[0].start;
this.liveSyncPosition = this.computeLivePosition(sliding, curDetails);
if (newDetails.PTSKnown && !isNaN(sliding)) {
logger["b" /* logger */].log('live playlist sliding:' + sliding.toFixed(3));
} else {
logger["b" /* logger */].log('live playlist - outdated PTS, unknown sliding');
alignDiscontinuities(this.fragPrevious, lastLevel, newDetails);
}
} else {
logger["b" /* logger */].log('live playlist - first load, unknown sliding');
newDetails.PTSKnown = false;
alignDiscontinuities(this.fragPrevious, lastLevel, newDetails);
}
} else {
newDetails.PTSKnown = false;
}
// override level info
curLevel.details = newDetails;
this.levelLastLoaded = newLevelId;
this.hls.trigger(events["a" /* default */].LEVEL_UPDATED, { details: newDetails, level: newLevelId });
if (this.startFragRequested === false) {
// compute start position if set to -1. use it straight away if value is defined
if (this.startPosition === -1 || this.lastCurrentTime === -1) {
// first, check if start time offset has been set in playlist, if yes, use this value
var startTimeOffset = newDetails.startTimeOffset;
if (!isNaN(startTimeOffset)) {
if (startTimeOffset < 0) {
logger["b" /* logger */].log('negative start time offset ' + startTimeOffset + ', count from end of last fragment');
startTimeOffset = sliding + duration + startTimeOffset;
}
logger["b" /* logger */].log('start time offset found in playlist, adjust startPosition to ' + startTimeOffset);
this.startPosition = startTimeOffset;
} else {
// if live playlist, set start position to be fragment N-this.config.liveSyncDurationCount (usually 3)
if (newDetails.live) {
this.startPosition = this.computeLivePosition(sliding, newDetails);
logger["b" /* logger */].log('configure startPosition to ' + this.startPosition);
} else {
this.startPosition = 0;
}
}
this.lastCurrentTime = this.startPosition;
}
this.nextLoadPosition = this.startPosition;
}
// only switch batck to IDLE state if we were waiting for level to start downloading a new fragment
if (this.state === State.WAITING_LEVEL) {
this.state = State.IDLE;
}
// trigger handler right now
this.tick();
};
StreamController.prototype.onKeyLoaded = function onKeyLoaded() {
if (this.state === State.KEY_LOADING) {
this.state = State.IDLE;
this.tick();
}
};
StreamController.prototype.onFragLoaded = function onFragLoaded(data) {
var fragCurrent = this.fragCurrent,
fragLoaded = data.frag;
if (this.state === State.FRAG_LOADING && fragCurrent && fragLoaded.type === 'main' && fragLoaded.level === fragCurrent.level && fragLoaded.sn === fragCurrent.sn) {
var stats = data.stats;
var currentLevel = this.levels[fragCurrent.level];
var details = currentLevel.details;
logger["b" /* logger */].log('Loaded ' + fragCurrent.sn + ' of [' + details.startSN + ' ,' + details.endSN + '],level ' + fragCurrent.level);
// reset frag bitrate test in any case after frag loaded event
this.bitrateTest = false;
this.stats = stats;
// if this frag was loaded to perform a bitrate test AND if hls.nextLoadLevel is greater than 0
// then this means that we should be able to load a fragment at a higher quality level
if (fragLoaded.bitrateTest === true && this.hls.nextLoadLevel) {
// switch back to IDLE state ... we just loaded a fragment to determine adequate start bitrate and initialize autoswitch algo
this.state = State.IDLE;
this.startFragRequested = false;
stats.tparsed = stats.tbuffered = window.performance.now();
this.hls.trigger(events["a" /* default */].FRAG_BUFFERED, { stats: stats, frag: fragCurrent, id: 'main' });
this.tick();
} else if (fragLoaded.sn === 'initSegment') {
this.state = State.IDLE;
stats.tparsed = stats.tbuffered = window.performance.now();
details.initSegment.data = data.payload;
this.hls.trigger(events["a" /* default */].FRAG_BUFFERED, { stats: stats, frag: fragCurrent, id: 'main' });
share/public_html/static/hls.js view on Meta::CPAN
logger["b" /* logger */].log('swapping playlist audio codec');
if (audioCodec.indexOf('mp4a.40.5') !== -1) {
audioCodec = 'mp4a.40.2';
} else {
audioCodec = 'mp4a.40.5';
}
}
// in case AAC and HE-AAC audio codecs are signalled in manifest
// force HE-AAC , as it seems that most browsers prefers that way,
// except for mono streams OR on FF
// these conditions might need to be reviewed ...
if (this.audioCodecSwitch) {
// don't force HE-AAC if mono stream
if (track.metadata.channelCount !== 1 &&
// don't force HE-AAC if firefox
ua.indexOf('firefox') === -1) {
audioCodec = 'mp4a.40.5';
}
}
// HE-AAC is broken on Android, always signal audio codec as AAC even if variant manifest states otherwise
if (ua.indexOf('android') !== -1 && track.container !== 'audio/mpeg') {
// Exclude mpeg audio
audioCodec = 'mp4a.40.2';
logger["b" /* logger */].log('Android: force audio codec to ' + audioCodec);
}
track.levelCodec = audioCodec;
track.id = data.id;
}
track = tracks.video;
if (track) {
track.levelCodec = this.levels[this.level].videoCodec;
track.id = data.id;
}
this.hls.trigger(events["a" /* default */].BUFFER_CODECS, tracks);
// loop through tracks that are going to be provided to bufferController
for (trackName in tracks) {
track = tracks[trackName];
logger["b" /* logger */].log('main track:' + trackName + ',container:' + track.container + ',codecs[level/parsed]=[' + track.levelCodec + '/' + track.codec + ']');
var initSegment = track.initSegment;
if (initSegment) {
this.appended = true;
// arm pending Buffering flag before appending a segment
this.pendingBuffering = true;
this.hls.trigger(events["a" /* default */].BUFFER_APPENDING, { type: trackName, data: initSegment, parent: 'main', content: 'initSegment' });
}
}
// trigger handler right now
this.tick();
}
};
StreamController.prototype.onFragParsingData = function onFragParsingData(data) {
var _this2 = this;
var fragCurrent = this.fragCurrent;
var fragNew = data.frag;
if (fragCurrent && data.id === 'main' && fragNew.sn === fragCurrent.sn && fragNew.level === fragCurrent.level && !(data.type === 'audio' && this.altAudio) && // filter out main audio if audio track is loaded through audio stream controller
this.state === State.PARSING) {
var level = this.levels[this.level],
frag = fragCurrent;
if (isNaN(data.endPTS)) {
data.endPTS = data.startPTS + fragCurrent.duration;
data.endDTS = data.startDTS + fragCurrent.duration;
}
if (data.hasAudio === true) {
frag.addElementaryStream(loader_fragment.ElementaryStreamTypes.AUDIO);
}
if (data.hasVideo === true) {
frag.addElementaryStream(loader_fragment.ElementaryStreamTypes.VIDEO);
}
logger["b" /* 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 + ',dropped:' + (data.dropped || 0...
// Detect gaps in a fragment and try to fix it by finding a keyframe in the previous fragment (see _findFragments)
if (data.type === 'video') {
frag.dropped = data.dropped;
if (frag.dropped) {
if (!frag.backtracked) {
var levelDetails = level.details;
if (levelDetails && frag.sn === levelDetails.startSN) {
logger["b" /* logger */].warn('missing video frame(s) on first frag, appending with gap', frag.sn);
} else {
logger["b" /* logger */].warn('missing video frame(s), backtracking fragment', frag.sn);
// Return back to the IDLE state without appending to buffer
// Causes findFragments to backtrack a segment and find the keyframe
// Audio fragments arriving before video sets the nextLoadPosition, causing _findFragments to skip the backtracked fragment
this.fragmentTracker.removeFragment(frag);
frag.backtracked = true;
this.nextLoadPosition = data.startPTS;
this.state = State.IDLE;
this.fragPrevious = frag;
this.tick();
return;
}
} else {
logger["b" /* logger */].warn('Already backtracked on this fragment, appending with the gap', frag.sn);
}
} else {
// Only reset the backtracked flag if we've loaded the frag without any dropped frames
frag.backtracked = false;
}
}
var drift = updateFragPTSDTS(level.details, frag, data.startPTS, data.endPTS, data.startDTS, data.endDTS),
hls = this.hls;
hls.trigger(events["a" /* default */].LEVEL_PTS_UPDATED, { details: level.details, level: this.level, drift: drift, type: data.type, start: data.startPTS, end: data.endPTS });
// has remuxer dropped video frames located before first keyframe ?
[data.data1, data.data2].forEach(function (buffer) {
// only append in PARSING state (rationale is that an appending error could happen synchronously on first segment appending)
// in that case it is useless to append following segments
if (buffer && buffer.length && _this2.state === State.PARSING) {
_this2.appended = true;
// arm pending Buffering flag before appending a segment
_this2.pendingBuffering = true;
hls.trigger(events["a" /* default */].BUFFER_APPENDING, { type: data.type, data: buffer, parent: 'main', content: 'data' });
}
});
// trigger handler right now
this.tick();
share/public_html/static/hls.js view on Meta::CPAN
return;
}
var stats = loader.stats;
/* only monitor frag retrieval time if
(video not paused OR first fragment being loaded(ready state === HAVE_NOTHING = 0)) AND autoswitching enabled AND not lowest level (=> means that we have several levels) */
if (video && stats && (!video.paused && video.playbackRate !== 0 || !video.readyState) && frag.autoLevel && frag.level) {
var requestDelay = abr_controller_performance.now() - stats.trequest,
playbackRate = Math.abs(video.playbackRate);
// monitor fragment load progress after half of expected fragment duration,to stabilize bitrate
if (requestDelay > 500 * frag.duration / playbackRate) {
var levels = hls.levels,
loadRate = Math.max(1, stats.bw ? stats.bw / 8 : stats.loaded * 1000 / requestDelay),
// byte/s; at least 1 byte/s to avoid division by zero
// compute expected fragment length using frag duration and level bitrate. also ensure that expected len is gte than already loaded size
level = levels[frag.level],
levelBitrate = level.realBitrate ? Math.max(level.realBitrate, level.bitrate) : level.bitrate,
expectedLen = stats.total ? stats.total : Math.max(stats.loaded, Math.round(frag.duration * levelBitrate / 8)),
pos = video.currentTime,
fragLoadedDelay = (expectedLen - stats.loaded) / loadRate,
bufferStarvationDelay = (BufferHelper.bufferInfo(video, pos, hls.config.maxBufferHole).end - pos) / playbackRate;
// consider emergency switch down only if we have less than 2 frag buffered AND
// time to finish loading current fragment is bigger than buffer starvation delay
// ie if we risk buffer starvation if bw does not increase quickly
if (bufferStarvationDelay < 2 * frag.duration / playbackRate && fragLoadedDelay > bufferStarvationDelay) {
var fragLevelNextLoadedDelay = void 0,
nextLoadLevel = void 0;
// lets iterate through lower level and try to find the biggest one that could avoid rebuffering
// we start from current level - 1 and we step down , until we find a matching level
for (nextLoadLevel = frag.level - 1; nextLoadLevel > minAutoLevel; nextLoadLevel--) {
// compute time to load next fragment at lower level
// 0.8 : consider only 80% of current bw to be conservative
// 8 = bits per byte (bps/Bps)
var levelNextBitrate = levels[nextLoadLevel].realBitrate ? Math.max(levels[nextLoadLevel].realBitrate, levels[nextLoadLevel].bitrate) : levels[nextLoadLevel].bitrate;
fragLevelNextLoadedDelay = frag.duration * levelNextBitrate / (8 * 0.8 * loadRate);
if (fragLevelNextLoadedDelay < bufferStarvationDelay) {
// we found a lower level that be rebuffering free with current estimated bw !
break;
}
}
// only emergency switch down if it takes less time to load new fragment at lowest level instead
// of finishing loading current one ...
if (fragLevelNextLoadedDelay < fragLoadedDelay) {
logger["b" /* logger */].warn('loading too slow, abort fragment loading and switch to level ' + nextLoadLevel + ':fragLoadedDelay[' + nextLoadLevel + ']<fragLoadedDelay[' + (frag.level - 1) + '];bufferStarvationDelay:' + fragLevelNextLoad...
// force next load level in auto mode
hls.nextLoadLevel = nextLoadLevel;
// update bw estimate for this fragment before cancelling load (this will help reducing the bw)
this._bwEstimator.sample(requestDelay, stats.loaded);
// abort fragment loading
loader.abort();
// stop abandon rules timer
this.clearTimer();
hls.trigger(events["a" /* default */].FRAG_LOAD_EMERGENCY_ABORTED, { frag: frag, stats: stats });
}
}
}
}
};
AbrController.prototype.onFragLoaded = function onFragLoaded(data) {
var frag = data.frag;
if (frag.type === 'main' && !isNaN(frag.sn)) {
// stop monitoring bw once frag loaded
this.clearTimer();
// store level id after successful fragment load
this.lastLoadedFragLevel = frag.level;
// reset forced auto level value so that next level will be selected
this._nextAutoLevel = -1;
// compute level average bitrate
if (this.hls.config.abrMaxWithRealBitrate) {
var level = this.hls.levels[frag.level];
var loadedBytes = (level.loaded ? level.loaded.bytes : 0) + data.stats.loaded;
var loadedDuration = (level.loaded ? level.loaded.duration : 0) + data.frag.duration;
level.loaded = { bytes: loadedBytes, duration: loadedDuration };
level.realBitrate = Math.round(8 * loadedBytes / loadedDuration);
}
// if fragment has been loaded to perform a bitrate test,
if (data.frag.bitrateTest) {
var stats = data.stats;
stats.tparsed = stats.tbuffered = stats.tload;
this.onFragBuffered(data);
}
}
};
AbrController.prototype.onFragBuffered = function onFragBuffered(data) {
var stats = data.stats;
var frag = data.frag;
// only update stats on first frag buffering
// if same frag is loaded multiple times, it might be in browser cache, and loaded quickly
// and leading to wrong bw estimation
// on bitrate test, also only update stats once (if tload = tbuffered == on FRAG_LOADED)
if (stats.aborted !== true && frag.type === 'main' && !isNaN(frag.sn) && (!frag.bitrateTest || stats.tload === stats.tbuffered)) {
// use tparsed-trequest instead of tbuffered-trequest to compute fragLoadingProcessing; rationale is that buffer appending only happens once media is attached
// in case we use config.startFragPrefetch while media is not attached yet, fragment might be parsed while media not attached yet, but it will only be buffered on media attached
// as a consequence it could happen really late in the process. meaning that appending duration might appears huge ... leading to underestimated throughput estimation
var fragLoadingProcessingMs = stats.tparsed - stats.trequest;
logger["b" /* logger */].log('latency/loading/parsing/append/kbps:' + Math.round(stats.tfirst - stats.trequest) + '/' + Math.round(stats.tload - stats.tfirst) + '/' + Math.round(stats.tparsed - stats.tload) + '/' + Math.round(stats.tbuffered - ...
this._bwEstimator.sample(fragLoadingProcessingMs, stats.loaded);
stats.bwEstimate = this._bwEstimator.getEstimate();
// if fragment has been loaded to perform a bitrate test, (hls.startLevel = -1), store bitrate test delay duration
if (frag.bitrateTest) {
this.bitrateTestDelay = fragLoadingProcessingMs / 1000;
} else {
this.bitrateTestDelay = 0;
}
}
};
AbrController.prototype.onError = function onError(data) {
// stop timer in case of frag loading error
switch (data.details) {
case errors["a" /* ErrorDetails */].FRAG_LOAD_ERROR:
case errors["a" /* ErrorDetails */].FRAG_LOAD_TIMEOUT:
this.clearTimer();
break;
default:
break;
}
};
AbrController.prototype.clearTimer = function clearTimer() {
clearInterval(this.timer);
this.timer = null;
};
// return next auto level
AbrController.prototype._findBestLevel = function _findBestLevel(currentLevel, currentFragDuration, currentBw, minAutoLevel, maxAutoLevel, maxFetchDuration, bwFactor, bwUpFactor, levels) {
for (var i = maxAutoLevel; i >= minAutoLevel; i--) {
var levelInfo = levels[i],
levelDetails = levelInfo.details,
avgDuration = levelDetails ? levelDetails.totalduration / levelDetails.fragments.length : currentFragDuration,
live = levelDetails ? levelDetails.live : false,
adjustedbw = void 0;
// follow algorithm captured from stagefright :
// https://android.googlesource.com/platform/frameworks/av/+/master/media/libstagefright/httplive/LiveSession.cpp
// Pick the highest bandwidth stream below or equal to estimated bandwidth.
// consider only 80% of the available bandwidth, but if we are switching up,
// be even more conservative (70%) to avoid overestimating and immediately
// switching back.
if (i <= currentLevel) {
adjustedbw = bwFactor * currentBw;
} else {
adjustedbw = bwUpFactor * currentBw;
}
var bitrate = levels[i].realBitrate ? Math.max(levels[i].realBitrate, levels[i].bitrate) : levels[i].bitrate,
fetchDuration = bitrate * avgDuration / adjustedbw;
logger["b" /* logger */].trace('level/adjustedbw/bitrate/avgDuration/maxFetchDuration/fetchDuration: ' + i + '/' + Math.round(adjustedbw) + '/' + bitrate + '/' + avgDuration + '/' + maxFetchDuration + '/' + fetchDuration);
// if adjusted bw is greater than level bitrate AND
share/public_html/static/hls.js view on Meta::CPAN
logger["b" /* logger */].log('all media data available, signal endOfStream() to MediaSource and stop loading fragment');
// Notify the media element that it now has all of the media data
try {
mediaSource.endOfStream();
} catch (e) {
logger["b" /* logger */].warn('exception while calling mediaSource.endOfStream()');
}
this._needsEos = false;
};
BufferController.prototype.onBufferFlushing = function onBufferFlushing(data) {
this.flushRange.push({ start: data.startOffset, end: data.endOffset, type: data.type });
// attempt flush immediately
this.flushBufferCounter = 0;
this.doFlush();
};
BufferController.prototype.onLevelUpdated = function onLevelUpdated(_ref) {
var details = _ref.details;
if (details.fragments.length > 0) {
this._levelDuration = details.totalduration + details.fragments[0].start;
this._live = details.live;
this.updateMediaElementDuration();
}
};
/**
* Update Media Source duration to current level duration or override to Infinity if configuration parameter
* 'liveDurationInfinity` is set to `true`
* More details: https://github.com/video-dev/hls.js/issues/355
*/
BufferController.prototype.updateMediaElementDuration = function updateMediaElementDuration() {
var config = this.hls.config;
var duration = void 0;
if (this._levelDuration === null || !this.media || !this.mediaSource || !this.sourceBuffer || this.media.readyState === 0 || this.mediaSource.readyState !== 'open') {
return;
}
for (var type in this.sourceBuffer) {
if (this.sourceBuffer[type].updating === true) {
// can't set duration whilst a buffer is updating
return;
}
}
duration = this.media.duration;
// initialise to the value that the media source is reporting
if (this._msDuration === null) {
this._msDuration = this.mediaSource.duration;
}
if (this._live === true && config.liveDurationInfinity === true) {
// Override duration to Infinity
logger["b" /* logger */].log('Media Source duration is set to Infinity');
this._msDuration = this.mediaSource.duration = Infinity;
} else if (this._levelDuration > this._msDuration && this._levelDuration > duration || duration === Infinity || isNaN(duration)) {
// levelDuration was the last value we set.
// not using mediaSource.duration as the browser may tweak this value
// only update Media Source duration if its value increase, this is to avoid
// flushing already buffered portion when switching between quality level
logger["b" /* logger */].log('Updating Media Source duration to ' + this._levelDuration.toFixed(3));
this._msDuration = this.mediaSource.duration = this._levelDuration;
}
};
BufferController.prototype.doFlush = function doFlush() {
// loop through all buffer ranges to flush
while (this.flushRange.length) {
var range = this.flushRange[0];
// flushBuffer will abort any buffer append in progress and flush Audio/Video Buffer
if (this.flushBuffer(range.start, range.end, range.type)) {
// range flushed, remove from flush array
this.flushRange.shift();
this.flushBufferCounter = 0;
} else {
this._needsFlush = true;
// avoid looping, wait for SB update end to retrigger a flush
return;
}
}
if (this.flushRange.length === 0) {
// everything flushed
this._needsFlush = false;
// let's recompute this.appended, which is used to avoid flush looping
var appended = 0;
var sourceBuffer = this.sourceBuffer;
try {
for (var type in sourceBuffer) {
appended += sourceBuffer[type].buffered.length;
}
} catch (error) {
// error could be thrown while accessing buffered, in case sourcebuffer has already been removed from MediaSource
// this is harmess at this stage, catch this to avoid reporting an internal exception
logger["b" /* logger */].error('error while accessing sourceBuffer.buffered');
}
this.appended = appended;
this.hls.trigger(events["a" /* default */].BUFFER_FLUSHED);
}
};
BufferController.prototype.doAppending = function doAppending() {
var hls = this.hls,
sourceBuffer = this.sourceBuffer,
segments = this.segments;
if (Object.keys(sourceBuffer).length) {
if (this.media.error) {
this.segments = [];
logger["b" /* logger */].error('trying to append although a media error occured, flush segment and abort');
return;
}
if (this.appending) {
// logger.log(`sb appending in progress`);
return;
}
if (segments && segments.length) {
share/public_html/static/hls.js view on Meta::CPAN
// |-----------------------------|
// <---> <--->
// ...--------><-----------------------------><---------....
// previous frag matching fragment next frag
// return -1 return 0 return 1
// logger.log(`level/sn/start/end/bufEnd:${level}/${candidate.sn}/${candidate.start}/${(candidate.start+candidate.duration)}/${bufferEnd}`);
// Set the lookup tolerance to be small enough to detect the current segment - ensures we don't skip over very small segments
var candidateLookupTolerance = Math.min(maxFragLookUpTolerance, candidate.duration);
if (candidate.start + candidate.duration - candidateLookupTolerance <= bufferEnd) {
return 1;
} else if (candidate.start - candidateLookupTolerance > bufferEnd && candidate.start) {
// if maxFragLookUpTolerance will have negative value then don't return -1 for first element
return -1;
}
return 0;
};
if (bufferEnd < end) {
if (bufferEnd > end - maxFragLookUpTolerance) {
maxFragLookUpTolerance = 0;
}
// Prefer the next fragment if it's within tolerance
if (fragNext && !fragmentWithinToleranceTest(fragNext)) {
foundFrag = fragNext;
} else {
foundFrag = binary_search.search(fragments, fragmentWithinToleranceTest);
}
} else {
// reach end of playlist
foundFrag = fragments[fragLen - 1];
}
if (foundFrag) {
frag = foundFrag;
start = foundFrag.start;
// logger.log('find SN matching with pos:' + bufferEnd + ':' + frag.sn);
if (fragPrevious && frag.level === fragPrevious.level && frag.sn === fragPrevious.sn) {
if (frag.sn < trackDetails.endSN) {
frag = fragments[frag.sn + 1 - trackDetails.startSN];
logger["b" /* logger */].log('SN just loaded, load next one: ' + frag.sn);
} else {
frag = null;
}
}
}
}
if (frag) {
// logger.log(' loading frag ' + i +',pos/bufEnd:' + pos.toFixed(3) + '/' + bufferEnd.toFixed(3));
if (frag.encrypted) {
logger["b" /* logger */].log('Loading key for ' + frag.sn + ' of [' + trackDetails.startSN + ' ,' + trackDetails.endSN + '],track ' + trackId);
this.state = audio_stream_controller_State.KEY_LOADING;
hls.trigger(events["a" /* default */].KEY_LOADING, { frag: frag });
} else {
logger["b" /* logger */].log('Loading ' + frag.sn + ', cc: ' + frag.cc + ' of [' + trackDetails.startSN + ' ,' + trackDetails.endSN + '],track ' + trackId + ', currentTime:' + pos + ',bufferEnd:' + bufferEnd.toFixed(3));
// only load if fragment is not loaded or if in audio switch
// we force a frag loading in audio switch as fragment tracker might not have evicted previous frags in case of quick audio switch
if (audioSwitch || this.fragmentTracker.getState(frag) === FragmentState.NOT_LOADED) {
this.fragCurrent = frag;
this.startFragRequested = true;
if (!isNaN(frag.sn)) {
this.nextLoadPosition = frag.start + frag.duration;
}
hls.trigger(events["a" /* default */].FRAG_LOADING, { frag: frag });
this.state = audio_stream_controller_State.FRAG_LOADING;
}
}
}
}
break;
case audio_stream_controller_State.WAITING_TRACK:
track = this.tracks[this.trackId];
// check if playlist is already loaded
if (track && track.details) {
this.state = audio_stream_controller_State.IDLE;
}
break;
case audio_stream_controller_State.FRAG_LOADING_WAITING_RETRY:
var now = audio_stream_controller_performance.now();
var retryDate = this.retryDate;
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["b" /* logger */].log('audioStreamController: retryDate reached, switch back to IDLE state');
this.state = audio_stream_controller_State.IDLE;
}
break;
case audio_stream_controller_State.WAITING_INIT_PTS:
var videoTrackCC = this.videoTrackCC;
if (this.initPTS[videoTrackCC] === undefined) {
break;
}
// Ensure we don't get stuck in the WAITING_INIT_PTS state if the waiting frag CC doesn't match any initPTS
var waitingFrag = this.waitingFragment;
if (waitingFrag) {
var waitingFragCC = waitingFrag.frag.cc;
if (videoTrackCC !== waitingFragCC) {
track = this.tracks[this.trackId];
if (track.details && track.details.live) {
logger["b" /* logger */].warn('Waiting fragment CC (' + waitingFragCC + ') does not match video track CC (' + videoTrackCC + ')');
this.waitingFragment = null;
this.state = audio_stream_controller_State.IDLE;
}
} else {
this.state = audio_stream_controller_State.FRAG_LOADING;
this.onFragLoaded(this.waitingFragment);
this.waitingFragment = null;
}
} else {
this.state = audio_stream_controller_State.IDLE;
}
break;
case audio_stream_controller_State.STOPPED:
case audio_stream_controller_State.FRAG_LOADING:
case audio_stream_controller_State.PARSING:
case audio_stream_controller_State.PARSED:
share/public_html/static/hls.js view on Meta::CPAN
this.fragCurrent = null;
this.state = audio_stream_controller_State.PAUSED;
this.waitingFragment = null;
// destroy useless demuxer when switching audio to main
if (!altAudio) {
if (this.demuxer) {
this.demuxer.destroy();
this.demuxer = null;
}
} else {
// switching to audio track, start timer if not already started
this.setInterval(100);
}
// should we switch tracks ?
if (altAudio) {
this.audioSwitch = true;
// main audio track are handled by stream-controller, just do something if switching to alt audio track
this.state = audio_stream_controller_State.IDLE;
}
this.tick();
};
AudioStreamController.prototype.onAudioTrackLoaded = function onAudioTrackLoaded(data) {
var newDetails = data.details,
trackId = data.id,
track = this.tracks[trackId],
duration = newDetails.totalduration,
sliding = 0;
logger["b" /* logger */].log('track ' + trackId + ' loaded [' + newDetails.startSN + ',' + newDetails.endSN + '],duration:' + duration);
if (newDetails.live) {
var curDetails = track.details;
if (curDetails && newDetails.fragments.length > 0) {
// we already have details for that level, merge them
mergeDetails(curDetails, newDetails);
sliding = newDetails.fragments[0].start;
// TODO
// this.liveSyncPosition = this.computeLivePosition(sliding, curDetails);
if (newDetails.PTSKnown) {
logger["b" /* logger */].log('live audio playlist sliding:' + sliding.toFixed(3));
} else {
logger["b" /* logger */].log('live audio playlist - outdated PTS, unknown sliding');
}
} else {
newDetails.PTSKnown = false;
logger["b" /* logger */].log('live audio playlist - first load, unknown sliding');
}
} else {
newDetails.PTSKnown = false;
}
track.details = newDetails;
// compute start position
if (!this.startFragRequested) {
// compute start position if set to -1. use it straight away if value is defined
if (this.startPosition === -1) {
// first, check if start time offset has been set in playlist, if yes, use this value
var startTimeOffset = newDetails.startTimeOffset;
if (!isNaN(startTimeOffset)) {
logger["b" /* logger */].log('start time offset found in playlist, adjust startPosition to ' + startTimeOffset);
this.startPosition = startTimeOffset;
} else {
this.startPosition = 0;
}
}
this.nextLoadPosition = this.startPosition;
}
// only switch batck to IDLE state if we were waiting for track to start downloading a new fragment
if (this.state === audio_stream_controller_State.WAITING_TRACK) {
this.state = audio_stream_controller_State.IDLE;
}
// trigger handler right now
this.tick();
};
AudioStreamController.prototype.onKeyLoaded = function onKeyLoaded() {
if (this.state === audio_stream_controller_State.KEY_LOADING) {
this.state = audio_stream_controller_State.IDLE;
this.tick();
}
};
AudioStreamController.prototype.onFragLoaded = function onFragLoaded(data) {
var fragCurrent = this.fragCurrent,
fragLoaded = data.frag;
if (this.state === audio_stream_controller_State.FRAG_LOADING && fragCurrent && fragLoaded.type === 'audio' && fragLoaded.level === fragCurrent.level && fragLoaded.sn === fragCurrent.sn) {
var track = this.tracks[this.trackId],
details = track.details,
duration = details.totalduration,
trackId = fragCurrent.level,
sn = fragCurrent.sn,
cc = fragCurrent.cc,
audioCodec = this.config.defaultAudioCodec || track.audioCodec || 'mp4a.40.2',
stats = this.stats = data.stats;
if (sn === 'initSegment') {
this.state = audio_stream_controller_State.IDLE;
stats.tparsed = stats.tbuffered = audio_stream_controller_performance.now();
details.initSegment.data = data.payload;
this.hls.trigger(events["a" /* default */].FRAG_BUFFERED, { stats: stats, frag: fragCurrent, id: 'audio' });
this.tick();
} else {
this.state = audio_stream_controller_State.PARSING;
// transmux the MPEG-TS data to ISO-BMFF segments
this.appended = false;
if (!this.demuxer) {
this.demuxer = new demux_demuxer(this.hls, 'audio');
}
// Check if we have video initPTS
// If not we need to wait for it
var initPTS = this.initPTS[cc];
var initSegmentData = details.initSegment ? details.initSegment.data : [];
if (details.initSegment || initPTS !== undefined) {
this.pendingBuffering = true;
logger["b" /* logger */].log('Demuxing ' + sn + ' of [' + details.startSN + ' ,' + details.endSN + '],track ' + trackId);
// time Offset is accurate if level PTS is known, or if playlist is not sliding (not live)
var accurateTimeOffset = false; // details.PTSKnown || !details.live;
this.demuxer.push(data.payload, initSegmentData, audioCodec, null, fragCurrent, duration, accurateTimeOffset, initPTS);
} else {
logger["b" /* logger */].log('unknown video PTS for continuity counter ' + cc + ', waiting for video PTS before demuxing audio frag ' + sn + ' of [' + details.startSN + ' ,' + details.endSN + '],track ' + trackId);
this.waitingFragment = data;
this.state = audio_stream_controller_State.WAITING_INIT_PTS;
}
}
}
this.fragLoadError = 0;
};
AudioStreamController.prototype.onFragParsingInitSegment = function onFragParsingInitSegment(data) {
var fragCurrent = this.fragCurrent;
var fragNew = data.frag;
if (fragCurrent && data.id === 'audio' && fragNew.sn === fragCurrent.sn && fragNew.level === fragCurrent.level && this.state === audio_stream_controller_State.PARSING) {
var tracks = data.tracks,
track = void 0;
// delete any video track found on audio demuxer
if (tracks.video) {
delete tracks.video;
}
// include levelCodec in audio and video tracks
track = tracks.audio;
if (track) {
track.levelCodec = track.codec;
track.id = data.id;
this.hls.trigger(events["a" /* default */].BUFFER_CODECS, tracks);
logger["b" /* logger */].log('audio track:audio,container:' + track.container + ',codecs[level/parsed]=[' + track.levelCodec + '/' + track.codec + ']');
var initSegment = track.initSegment;
if (initSegment) {
var appendObj = { type: 'audio', data: initSegment, parent: 'audio', content: 'initSegment' };
if (this.audioSwitch) {
this.pendingData = [appendObj];
} else {
this.appended = true;
// arm pending Buffering flag before appending a segment
this.pendingBuffering = true;
this.hls.trigger(events["a" /* default */].BUFFER_APPENDING, appendObj);
}
}
// trigger handler right now
this.tick();
}
}
};
AudioStreamController.prototype.onFragParsingData = function onFragParsingData(data) {
var _this2 = this;
var fragCurrent = this.fragCurrent;
var fragNew = data.frag;
if (fragCurrent && data.id === 'audio' && data.type === 'audio' && fragNew.sn === fragCurrent.sn && fragNew.level === fragCurrent.level && this.state === audio_stream_controller_State.PARSING) {
var trackId = this.trackId,
track = this.tracks[trackId],
hls = this.hls;
if (isNaN(data.endPTS)) {
data.endPTS = data.startPTS + fragCurrent.duration;
data.endDTS = data.startDTS + fragCurrent.duration;
}
fragCurrent.addElementaryStream(loader_fragment.ElementaryStreamTypes.AUDIO);
logger["b" /* 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);
updateFragPTSDTS(track.details, fragCurrent, data.startPTS, data.endPTS);
var audioSwitch = this.audioSwitch,
media = this.media,
appendOnBufferFlush = false;
// Only flush audio from old audio tracks when PTS is known on new audio track
if (audioSwitch && media) {
if (media.readyState) {
var currentTime = media.currentTime;
logger["b" /* logger */].log('switching audio track : currentTime:' + currentTime);
if (currentTime >= data.startPTS) {
logger["b" /* logger */].log('switching audio track : flushing all audio');
this.state = audio_stream_controller_State.BUFFER_FLUSHING;
hls.trigger(events["a" /* default */].BUFFER_FLUSHING, { startOffset: 0, endOffset: Number.POSITIVE_INFINITY, type: 'audio' });
appendOnBufferFlush = true;
// Lets announce that the initial audio track switch flush occur
this.audioSwitch = false;
hls.trigger(events["a" /* default */].AUDIO_TRACK_SWITCHED, { id: trackId });
}
} else {
// Lets announce that the initial audio track switch flush occur
this.audioSwitch = false;
hls.trigger(events["a" /* default */].AUDIO_TRACK_SWITCHED, { id: trackId });
}
}
var pendingData = this.pendingData;
if (!pendingData) {
logger["b" /* logger */].warn('Apparently attempt to enqueue media payload without codec initialization data upfront');
hls.trigger(events["a" /* default */].ERROR, { type: errors["b" /* ErrorTypes */].MEDIA_ERROR, details: null, fatal: true });
return;
}
if (!this.audioSwitch) {
[data.data1, data.data2].forEach(function (buffer) {
if (buffer && buffer.length) {
pendingData.push({ type: data.type, data: buffer, parent: 'audio', content: 'data' });
}
});
if (!appendOnBufferFlush && pendingData.length) {
pendingData.forEach(function (appendObj) {
// only append in PARSING state (rationale is that an appending error could happen synchronously on first segment appending)
// in that case it is useless to append following segments
if (_this2.state === audio_stream_controller_State.PARSING) {
// arm pending Buffering flag before appending a segment
_this2.pendingBuffering = true;
_this2.hls.trigger(events["a" /* default */].BUFFER_APPENDING, appendObj);
}
});
this.pendingData = [];
this.appended = true;
}
share/public_html/static/hls.js view on Meta::CPAN
}
}
};
return Cea608Parser;
}();
/* harmony default export */ var cea_608_parser = (Cea608Parser);
// CONCATENATED MODULE: ./src/utils/output-filter.js
function output_filter__classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var OutputFilter = function () {
function OutputFilter(timelineController, trackName) {
output_filter__classCallCheck(this, OutputFilter);
this.timelineController = timelineController;
this.trackName = trackName;
this.startTime = null;
this.endTime = null;
this.screen = null;
}
OutputFilter.prototype.dispatchCue = function dispatchCue() {
if (this.startTime === null) {
return;
}
this.timelineController.addCues(this.trackName, this.startTime, this.endTime, this.screen);
this.startTime = null;
};
OutputFilter.prototype.newCue = function newCue(startTime, endTime, screen) {
if (this.startTime === null || this.startTime > startTime) {
this.startTime = startTime;
}
this.endTime = endTime;
this.screen = screen;
this.timelineController.createCaptionsTrack(this.trackName);
};
return OutputFilter;
}();
/* harmony default export */ var output_filter = (OutputFilter);
// CONCATENATED MODULE: ./src/utils/webvtt-parser.js
// String.prototype.startsWith is not supported in IE11
var startsWith = function startsWith(inputString, searchString, position) {
return inputString.substr(position || 0, searchString.length) === searchString;
};
var cueString2millis = function cueString2millis(timeString) {
var ts = parseInt(timeString.substr(-3));
var secs = parseInt(timeString.substr(-6, 2));
var mins = parseInt(timeString.substr(-9, 2));
var hours = timeString.length > 9 ? parseInt(timeString.substr(0, timeString.indexOf(':'))) : 0;
if (isNaN(ts) || isNaN(secs) || isNaN(mins) || isNaN(hours)) {
return -1;
}
ts += 1000 * secs;
ts += 60 * 1000 * mins;
ts += 60 * 60 * 1000 * hours;
return ts;
};
// From https://github.com/darkskyapp/string-hash
var hash = function hash(text) {
var hash = 5381;
var i = text.length;
while (i) {
hash = hash * 33 ^ text.charCodeAt(--i);
}
return (hash >>> 0).toString();
};
var calculateOffset = function calculateOffset(vttCCs, cc, presentationTime) {
var currCC = vttCCs[cc];
var prevCC = vttCCs[currCC.prevCC];
// This is the first discontinuity or cues have been processed since the last discontinuity
// Offset = current discontinuity time
if (!prevCC || !prevCC.new && currCC.new) {
vttCCs.ccOffset = vttCCs.presentationOffset = currCC.start;
currCC.new = false;
return;
}
// There have been discontinuities since cues were last parsed.
// Offset = time elapsed
while (prevCC && prevCC.new) {
vttCCs.ccOffset += currCC.start - prevCC.start;
currCC.new = false;
currCC = prevCC;
prevCC = vttCCs[currCC.prevCC];
}
vttCCs.presentationOffset = presentationTime;
};
var WebVTTParser = {
parse: function parse(vttByteArray, syncPTS, vttCCs, cc, callBack, errorCallBack) {
// Convert byteArray into string, replacing any somewhat exotic linefeeds with "\n", then split on that character.
var re = /\r\n|\n\r|\n|\r/g;
// Uint8Array.prototype.reduce is not implemented in IE11
var vttLines = Object(id3["b" /* utf8ArrayToStr */])(new Uint8Array(vttByteArray)).trim().replace(re, '\n').split('\n');
var cueTime = '00:00.000';
var mpegTs = 0;
var localTime = 0;
var presentationTime = 0;
var cues = [];
var parsingError = void 0;
var inHeader = true;
// let VTTCue = VTTCue || window.TextTrackCue;
share/public_html/static/hls.js view on Meta::CPAN
observer.on(__WEBPACK_IMPORTED_MODULE_1__events__["a" /* default */].ERROR, forwardMessage);
observer.on(__WEBPACK_IMPORTED_MODULE_1__events__["a" /* default */].FRAG_PARSING_METADATA, forwardMessage);
observer.on(__WEBPACK_IMPORTED_MODULE_1__events__["a" /* default */].FRAG_PARSING_USERDATA, forwardMessage);
observer.on(__WEBPACK_IMPORTED_MODULE_1__events__["a" /* default */].INIT_PTS_FOUND, forwardMessage);
// special case for FRAG_PARSING_DATA: pass data1/data2 as transferable object (no copy)
observer.on(__WEBPACK_IMPORTED_MODULE_1__events__["a" /* default */].FRAG_PARSING_DATA, function (ev, data) {
var transferable = [];
var message = { event: ev, data: data };
if (data.data1) {
message.data1 = data.data1.buffer;
transferable.push(data.data1.buffer);
delete data.data1;
}
if (data.data2) {
message.data2 = data.data2.buffer;
transferable.push(data.data2.buffer);
delete data.data2;
}
self.postMessage(message, transferable);
});
};
/* harmony default export */ __webpack_exports__["default"] = (DemuxerWorker);
/***/ }),
/* 13 */
/***/ (function(module, exports) {
/*! http://mths.be/endswith v0.2.0 by @mathias */
if (!String.prototype.endsWith) {
(function() {
'use strict'; // needed to support `apply`/`call` with `undefined`/`null`
var defineProperty = (function() {
// IE 8 only supports `Object.defineProperty` on DOM elements
try {
var object = {};
var $defineProperty = Object.defineProperty;
var result = $defineProperty(object, object, object) && $defineProperty;
} catch(error) {}
return result;
}());
var toString = {}.toString;
var endsWith = function(search) {
if (this == null) {
throw TypeError();
}
var string = String(this);
if (search && toString.call(search) == '[object RegExp]') {
throw TypeError();
}
var stringLength = string.length;
var searchString = String(search);
var searchLength = searchString.length;
var pos = stringLength;
if (arguments.length > 1) {
var position = arguments[1];
if (position !== undefined) {
// `ToInteger`
pos = position ? Number(position) : 0;
if (pos != pos) { // better `isNaN`
pos = 0;
}
}
}
var end = Math.min(Math.max(pos, 0), stringLength);
var start = end - searchLength;
if (start < 0) {
return false;
}
var index = -1;
while (++index < searchLength) {
if (string.charCodeAt(start + index) != searchString.charCodeAt(index)) {
return false;
}
}
return true;
};
if (defineProperty) {
defineProperty(String.prototype, 'endsWith', {
'value': endsWith,
'configurable': true,
'writable': true
});
} else {
String.prototype.endsWith = endsWith;
}
}());
}
/***/ })
/******/ ])["default"];
});
//# sourceMappingURL=hls.js.map