jellyfish-web/dashboard-ui/bower_components/hls.js/src/loader/playlist-loader.js
2016-04-08 12:49:34 -04:00

290 lines
9.8 KiB
JavaScript

/**
* Playlist Loader
*/
import Event from '../events';
import EventHandler from '../event-handler';
import {ErrorTypes, ErrorDetails} from '../errors';
import URLHelper from '../utils/url';
import AttrList from '../utils/attr-list';
//import {logger} from '../utils/logger';
class PlaylistLoader extends EventHandler {
constructor(hls) {
super(hls,
Event.MANIFEST_LOADING,
Event.LEVEL_LOADING);
}
destroy() {
if (this.loader) {
this.loader.destroy();
this.loader = null;
}
this.url = this.id = null;
EventHandler.prototype.destroy.call(this);
}
onManifestLoading(data) {
this.load(data.url, null);
}
onLevelLoading(data) {
this.load(data.url, data.level, data.id);
}
load(url, id1, id2) {
var config = this.hls.config,
retry,
timeout,
retryDelay;
this.url = url;
this.id = id1;
this.id2 = id2;
if(this.id === null) {
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), timeout, retry, retryDelay);
}
resolve(url, baseUrl) {
return URLHelper.buildAbsoluteURL(baseUrl, url);
}
parseMasterPlaylist(string, baseurl) {
let levels = [], result;
// https://regex101.com is your friend
const re = /#EXT-X-STREAM-INF:([^\n\r]*)[\r\n]+([^\r\n]+)/g;
while ((result = re.exec(string)) != null){
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('AVERAGE-BANDWIDTH') || 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);
}
return levels;
}
avc1toavcoti(codec) {
var result, avcdata = codec.split('.');
if (avcdata.length > 2) {
result = avcdata.shift() + '.';
result += parseInt(avcdata.shift()).toString(16);
result += ('000' + parseInt(avcdata.shift()).toString(16)).substr(-4);
} else {
result = codec;
}
return result;
}
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},
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); });
switch (result[0]) {
case 'MEDIA-SEQUENCE':
currentSN = level.startSN = parseInt(result[1]);
break;
case 'TARGETDURATION':
level.targetduration = parseFloat(result[1]);
break;
case 'ENDLIST':
level.live = false;
break;
case 'DIS':
cc++;
break;
case 'BYTERANGE':
var params = result[1].split('@');
if (params.length === 1) {
byteRangeStartOffset = byteRangeEndOffset;
} else {
byteRangeStartOffset = parseInt(params[1]);
}
byteRangeEndOffset = parseInt(params[0]) + byteRangeStartOffset;
if (frag && !frag.url) {
frag.byteRangeStartOffset = byteRangeStartOffset;
frag.byteRangeEndOffset = byteRangeEndOffset;
frag.url = this.resolve(result[2], baseurl);
}
break;
case 'INF':
var duration = parseFloat(result[1]);
if (!isNaN(duration)) {
var fragdecryptdata,
sn = currentSN++;
if (levelkey.method && levelkey.uri && !levelkey.iv) {
fragdecryptdata = this.cloneObj(levelkey);
var uint8View = new Uint8Array(16);
for (var i = 12; i < 16; i++) {
uint8View[i] = (sn >> 8*(15-i)) & 0xff;
}
fragdecryptdata.iv = uint8View;
} else {
fragdecryptdata = levelkey;
}
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 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')) {
levelkey.method = decryptmethod;
// URI to get the key
levelkey.uri = this.resolve(decrypturi, baseurl);
levelkey.key = null;
// Initialization Vector (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 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(target.getResponseHeader('Last-Modified'));
if (string.indexOf('#EXTM3U') === 0) {
if (string.indexOf('#EXTINF:') > 0) {
// 1 level playlist
// if first request, fire manifest loaded event, level will be reloaded afterwards
// (this is to have a uniform logic for 1 level/multilevel playlists)
if (this.id === null) {
hls.trigger(Event.MANIFEST_LOADED, {levels: [{url: url}], url: url, stats: stats});
} else {
var levelDetails = this.parseLevelPlaylist(string, url, id);
stats.tparsed = performance.now();
hls.trigger(Event.LEVEL_LOADED, {details: levelDetails, level: id, id: id2, stats: stats});
}
} else {
levels = this.parseMasterPlaylist(string, url);
// multi level playlist, parse level info
if (levels.length) {
hls.trigger(Event.MANIFEST_LOADED, {levels: levels, url: url, stats: stats});
} else {
hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.MANIFEST_PARSING_ERROR, fatal: true, url: url, reason: 'no level found in manifest'});
}
}
} else {
hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.MANIFEST_PARSING_ERROR, fatal: true, url: url, reason: 'no EXTM3U delimiter'});
}
}
loaderror(event) {
var details, fatal;
if (this.id === null) {
details = ErrorDetails.MANIFEST_LOAD_ERROR;
fatal = true;
} else {
details = ErrorDetails.LEVEL_LOAD_ERROR;
fatal = false;
}
if (this.loader) {
this.loader.abort();
}
this.hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: details, fatal: fatal, url: this.url, loader: this.loader, response: event.currentTarget, level: this.id, id: this.id2});
}
loadtimeout() {
var details, fatal;
if (this.id === null) {
details = ErrorDetails.MANIFEST_LOAD_TIMEOUT;
fatal = true;
} else {
details = ErrorDetails.LEVEL_LOAD_TIMEOUT;
fatal = false;
}
if (this.loader) {
this.loader.abort();
}
this.hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: details, fatal: fatal, url: this.url, loader: this.loader, level: this.id, id: this.id2});
}
}
export default PlaylistLoader;