@videojs/http-streaming
Version:
Play back HLS and DASH with Video.js, even where it's not natively supported
1,643 lines (1,397 loc) • 628 kB
JavaScript
/**
* @videojs/http-streaming
* @version 1.13.3
* @copyright 2020 Brightcove, Inc
* @license Apache-2.0
*/
import URLToolkit from 'url-toolkit';
import window$1 from 'global/window';
import videojs from 'video.js';
import { Parser } from 'm3u8-parser';
import document from 'global/document';
import { parse, parseUTCTiming } from 'mpd-parser';
import mp4Inspector from 'mux.js/lib/tools/mp4-inspector';
import mp4probe from 'mux.js/lib/mp4/probe';
import CaptionParser from 'mux.js/lib/mp4/caption-parser';
import tsInspector from 'mux.js/lib/tools/ts-inspector.js';
import { Decrypter, AsyncStream, decrypt } from 'aes-decrypter';
/**
* @file resolve-url.js - Handling how URLs are resolved and manipulated
*/
var resolveUrl = function resolveUrl(baseURL, relativeURL) {
// return early if we don't need to resolve
if (/^[a-z]+:/i.test(relativeURL)) {
return relativeURL;
}
// if the base URL is relative then combine with the current location
if (!/\/\//i.test(baseURL)) {
baseURL = URLToolkit.buildAbsoluteURL(window$1.location.href, baseURL);
}
return URLToolkit.buildAbsoluteURL(baseURL, relativeURL);
};
/**
* Checks whether xhr request was redirected and returns correct url depending
* on `handleManifestRedirects` option
*
* @api private
*
* @param {String} url - an url being requested
* @param {XMLHttpRequest} req - xhr request result
*
* @return {String}
*/
var resolveManifestRedirect = function resolveManifestRedirect(handleManifestRedirect, url, req) {
// To understand how the responseURL below is set and generated:
// - https://fetch.spec.whatwg.org/#concept-response-url
// - https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
if (handleManifestRedirect && req.responseURL && url !== req.responseURL) {
return req.responseURL;
}
return url;
};
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
var createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
var get = function get(object, property, receiver) {
if (object === null) object = Function.prototype;
var desc = Object.getOwnPropertyDescriptor(object, property);
if (desc === undefined) {
var parent = Object.getPrototypeOf(object);
if (parent === null) {
return undefined;
} else {
return get(parent, property, receiver);
}
} else if ("value" in desc) {
return desc.value;
} else {
var getter = desc.get;
if (getter === undefined) {
return undefined;
}
return getter.call(receiver);
}
};
var inherits = function (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.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
};
var possibleConstructorReturn = function (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;
};
var slicedToArray = function () {
function sliceIterator(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"]) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
return function (arr, i) {
if (Array.isArray(arr)) {
return arr;
} else if (Symbol.iterator in Object(arr)) {
return sliceIterator(arr, i);
} else {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
};
}();
/**
* @file playlist-loader.js
*
* A state machine that manages the loading, caching, and updating of
* M3U8 playlists.
*
*/
var mergeOptions = videojs.mergeOptions,
EventTarget = videojs.EventTarget,
log = videojs.log;
/**
* Loops through all supported media groups in master and calls the provided
* callback for each group
*
* @param {Object} master
* The parsed master manifest object
* @param {Function} callback
* Callback to call for each media group
*/
var forEachMediaGroup = function forEachMediaGroup(master, callback) {
['AUDIO', 'SUBTITLES'].forEach(function (mediaType) {
for (var groupKey in master.mediaGroups[mediaType]) {
for (var labelKey in master.mediaGroups[mediaType][groupKey]) {
var mediaProperties = master.mediaGroups[mediaType][groupKey][labelKey];
callback(mediaProperties, mediaType, groupKey, labelKey);
}
}
});
};
/**
* Returns a new array of segments that is the result of merging
* properties from an older list of segments onto an updated
* list. No properties on the updated playlist will be overridden.
*
* @param {Array} original the outdated list of segments
* @param {Array} update the updated list of segments
* @param {Number=} offset the index of the first update
* segment in the original segment list. For non-live playlists,
* this should always be zero and does not need to be
* specified. For live playlists, it should be the difference
* between the media sequence numbers in the original and updated
* playlists.
* @return a list of merged segment objects
*/
var updateSegments = function updateSegments(original, update, offset) {
var result = update.slice();
offset = offset || 0;
var length = Math.min(original.length, update.length + offset);
for (var i = offset; i < length; i++) {
result[i - offset] = mergeOptions(original[i], result[i - offset]);
}
return result;
};
var resolveSegmentUris = function resolveSegmentUris(segment, baseUri) {
if (!segment.resolvedUri) {
segment.resolvedUri = resolveUrl(baseUri, segment.uri);
}
if (segment.key && !segment.key.resolvedUri) {
segment.key.resolvedUri = resolveUrl(baseUri, segment.key.uri);
}
if (segment.map && !segment.map.resolvedUri) {
segment.map.resolvedUri = resolveUrl(baseUri, segment.map.uri);
}
};
/**
* Returns a new master playlist that is the result of merging an
* updated media playlist into the original version. If the
* updated media playlist does not match any of the playlist
* entries in the original master playlist, null is returned.
*
* @param {Object} master a parsed master M3U8 object
* @param {Object} media a parsed media M3U8 object
* @return {Object} a new object that represents the original
* master playlist with the updated media playlist merged in, or
* null if the merge produced no change.
*/
var updateMaster = function updateMaster(master, media) {
var result = mergeOptions(master, {});
var playlist = result.playlists[media.id];
if (!playlist) {
return null;
}
// consider the playlist unchanged if the number of segments is equal, the media
// sequence number is unchanged, and this playlist hasn't become the end of the playlist
if (playlist.segments && media.segments && playlist.segments.length === media.segments.length && playlist.endList === media.endList && playlist.mediaSequence === media.mediaSequence) {
return null;
}
var mergedPlaylist = mergeOptions(playlist, media);
// if the update could overlap existing segment information, merge the two segment lists
if (playlist.segments) {
mergedPlaylist.segments = updateSegments(playlist.segments, media.segments, media.mediaSequence - playlist.mediaSequence);
}
// resolve any segment URIs to prevent us from having to do it later
mergedPlaylist.segments.forEach(function (segment) {
resolveSegmentUris(segment, mergedPlaylist.resolvedUri);
});
// TODO Right now in the playlists array there are two references to each playlist, one
// that is referenced by index, and one by URI. The index reference may no longer be
// necessary.
for (var i = 0; i < result.playlists.length; i++) {
if (result.playlists[i].id === media.id) {
result.playlists[i] = mergedPlaylist;
}
}
result.playlists[media.id] = mergedPlaylist;
// URI reference added for backwards compatibility
result.playlists[media.uri] = mergedPlaylist;
return result;
};
var createPlaylistID = function createPlaylistID(index, uri) {
return index + '-' + uri;
};
var setupMediaPlaylists = function setupMediaPlaylists(master) {
// setup by-URI lookups and resolve media playlist URIs
var i = master.playlists.length;
while (i--) {
var playlist = master.playlists[i];
playlist.resolvedUri = resolveUrl(master.uri, playlist.uri);
playlist.id = createPlaylistID(i, playlist.uri);
master.playlists[playlist.id] = playlist;
// URI reference added for backwards compatibility
master.playlists[playlist.uri] = playlist;
if (!playlist.attributes) {
// Although the spec states an #EXT-X-STREAM-INF tag MUST have a
// BANDWIDTH attribute, we can play the stream without it. This means a poorly
// formatted master playlist may not have an attribute list. An attributes
// property is added here to prevent undefined references when we encounter
// this scenario.
playlist.attributes = {};
log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.');
}
}
};
var resolveMediaGroupUris = function resolveMediaGroupUris(master) {
forEachMediaGroup(master, function (properties) {
if (properties.uri) {
properties.resolvedUri = resolveUrl(master.uri, properties.uri);
}
});
};
/**
* Calculates the time to wait before refreshing a live playlist
*
* @param {Object} media
* The current media
* @param {Boolean} update
* True if there were any updates from the last refresh, false otherwise
* @return {Number}
* The time in ms to wait before refreshing the live playlist
*/
var refreshDelay = function refreshDelay(media, update) {
var lastSegment = media.segments[media.segments.length - 1];
var delay = void 0;
if (update && lastSegment && lastSegment.duration) {
delay = lastSegment.duration * 1000;
} else {
// if the playlist is unchanged since the last reload or last segment duration
// cannot be determined, try again after half the target duration
delay = (media.targetDuration || 10) * 500;
}
return delay;
};
/**
* Load a playlist from a remote location
*
* @class PlaylistLoader
* @extends Stream
* @param {String} srcUrl the url to start with
* @param {Boolean} withCredentials the withCredentials xhr option
* @constructor
*/
var PlaylistLoader = function (_EventTarget) {
inherits(PlaylistLoader, _EventTarget);
function PlaylistLoader(srcUrl, hls) {
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
classCallCheck(this, PlaylistLoader);
var _this = possibleConstructorReturn(this, (PlaylistLoader.__proto__ || Object.getPrototypeOf(PlaylistLoader)).call(this));
var _options$withCredenti = options.withCredentials,
withCredentials = _options$withCredenti === undefined ? false : _options$withCredenti,
_options$handleManife = options.handleManifestRedirects,
handleManifestRedirects = _options$handleManife === undefined ? false : _options$handleManife;
_this.srcUrl = srcUrl;
_this.hls_ = hls;
_this.withCredentials = withCredentials;
_this.handleManifestRedirects = handleManifestRedirects;
var hlsOptions = hls.options_;
_this.customTagParsers = hlsOptions && hlsOptions.customTagParsers || [];
_this.customTagMappers = hlsOptions && hlsOptions.customTagMappers || [];
if (!_this.srcUrl) {
throw new Error('A non-empty playlist URL is required');
}
// initialize the loader state
_this.state = 'HAVE_NOTHING';
// live playlist staleness timeout
_this.on('mediaupdatetimeout', function () {
if (_this.state !== 'HAVE_METADATA') {
// only refresh the media playlist if no other activity is going on
return;
}
_this.state = 'HAVE_CURRENT_METADATA';
_this.request = _this.hls_.xhr({
uri: resolveUrl(_this.master.uri, _this.media().uri),
withCredentials: _this.withCredentials
}, function (error, req) {
// disposed
if (!_this.request) {
return;
}
if (error) {
return _this.playlistRequestError(_this.request, _this.media(), 'HAVE_METADATA');
}
_this.haveMetadata(_this.request, _this.media().uri, _this.media().id);
});
});
return _this;
}
createClass(PlaylistLoader, [{
key: 'playlistRequestError',
value: function playlistRequestError(xhr, playlist, startingState) {
var uri = playlist.uri,
id = playlist.id;
// any in-flight request is now finished
this.request = null;
if (startingState) {
this.state = startingState;
}
this.error = {
playlist: this.master.playlists[id],
status: xhr.status,
message: 'HLS playlist request error at URL: ' + uri + '.',
responseText: xhr.responseText,
code: xhr.status >= 500 ? 4 : 2
};
this.trigger('error');
}
// update the playlist loader's state in response to a new or
// updated playlist.
}, {
key: 'haveMetadata',
value: function haveMetadata(xhr, url, id) {
var _this2 = this;
// any in-flight request is now finished
this.request = null;
this.state = 'HAVE_METADATA';
var parser = new Parser();
// adding custom tag parsers
this.customTagParsers.forEach(function (customParser) {
return parser.addParser(customParser);
});
// adding custom tag mappers
this.customTagMappers.forEach(function (mapper) {
return parser.addTagMapper(mapper);
});
parser.push(xhr.responseText);
parser.end();
parser.manifest.uri = url;
parser.manifest.id = id;
// m3u8-parser does not attach an attributes property to media playlists so make
// sure that the property is attached to avoid undefined reference errors
parser.manifest.attributes = parser.manifest.attributes || {};
// merge this playlist into the master
var update = updateMaster(this.master, parser.manifest);
this.targetDuration = parser.manifest.targetDuration;
if (update) {
this.master = update;
this.media_ = this.master.playlists[id];
} else {
this.trigger('playlistunchanged');
}
// refresh live playlists after a target duration passes
if (!this.media().endList) {
window$1.clearTimeout(this.mediaUpdateTimeout);
this.mediaUpdateTimeout = window$1.setTimeout(function () {
_this2.trigger('mediaupdatetimeout');
}, refreshDelay(this.media(), !!update));
}
this.trigger('loadedplaylist');
}
/**
* Abort any outstanding work and clean up.
*/
}, {
key: 'dispose',
value: function dispose() {
this.trigger('dispose');
this.stopRequest();
window$1.clearTimeout(this.mediaUpdateTimeout);
window$1.clearTimeout(this.finalRenditionTimeout);
this.off();
}
}, {
key: 'stopRequest',
value: function stopRequest() {
if (this.request) {
var oldRequest = this.request;
this.request = null;
oldRequest.onreadystatechange = null;
oldRequest.abort();
}
}
/**
* When called without any arguments, returns the currently
* active media playlist. When called with a single argument,
* triggers the playlist loader to asynchronously switch to the
* specified media playlist. Calling this method while the
* loader is in the HAVE_NOTHING causes an error to be emitted
* but otherwise has no effect.
*
* @param {Object=} playlist the parsed media playlist
* object to switch to
* @param {Boolean=} is this the last available playlist
*
* @return {Playlist} the current loaded media
*/
}, {
key: 'media',
value: function media(playlist, isFinalRendition) {
var _this3 = this;
// getter
if (!playlist) {
return this.media_;
}
// setter
if (this.state === 'HAVE_NOTHING') {
throw new Error('Cannot switch media playlist from ' + this.state);
}
// find the playlist object if the target playlist has been
// specified by URI
if (typeof playlist === 'string') {
if (!this.master.playlists[playlist]) {
throw new Error('Unknown playlist URI: ' + playlist);
}
playlist = this.master.playlists[playlist];
}
window$1.clearTimeout(this.finalRenditionTimeout);
if (isFinalRendition) {
var delay = playlist.targetDuration / 2 * 1000 || 5 * 1000;
this.finalRenditionTimeout = window$1.setTimeout(this.media.bind(this, playlist, false), delay);
return;
}
var startingState = this.state;
var mediaChange = !this.media_ || playlist.id !== this.media_.id;
// switch to fully loaded playlists immediately
if (this.master.playlists[playlist.id].endList) {
// abort outstanding playlist requests
if (this.request) {
this.request.onreadystatechange = null;
this.request.abort();
this.request = null;
}
this.state = 'HAVE_METADATA';
this.media_ = playlist;
// trigger media change if the active media has been updated
if (mediaChange) {
this.trigger('mediachanging');
this.trigger('mediachange');
}
return;
}
// switching to the active playlist is a no-op
if (!mediaChange) {
return;
}
this.state = 'SWITCHING_MEDIA';
// there is already an outstanding playlist request
if (this.request) {
if (playlist.resolvedUri === this.request.url) {
// requesting to switch to the same playlist multiple times
// has no effect after the first
return;
}
this.request.onreadystatechange = null;
this.request.abort();
this.request = null;
}
// request the new playlist
if (this.media_) {
this.trigger('mediachanging');
}
this.request = this.hls_.xhr({
uri: playlist.resolvedUri,
withCredentials: this.withCredentials
}, function (error, req) {
// disposed
if (!_this3.request) {
return;
}
playlist.resolvedUri = resolveManifestRedirect(_this3.handleManifestRedirects, playlist.resolvedUri, req);
if (error) {
return _this3.playlistRequestError(_this3.request, playlist, startingState);
}
_this3.haveMetadata(req, playlist.uri, playlist.id);
// fire loadedmetadata the first time a media playlist is loaded
if (startingState === 'HAVE_MASTER') {
_this3.trigger('loadedmetadata');
} else {
_this3.trigger('mediachange');
}
});
}
/**
* pause loading of the playlist
*/
}, {
key: 'pause',
value: function pause() {
this.stopRequest();
window$1.clearTimeout(this.mediaUpdateTimeout);
if (this.state === 'HAVE_NOTHING') {
// If we pause the loader before any data has been retrieved, its as if we never
// started, so reset to an unstarted state.
this.started = false;
}
// Need to restore state now that no activity is happening
if (this.state === 'SWITCHING_MEDIA') {
// if the loader was in the process of switching media, it should either return to
// HAVE_MASTER or HAVE_METADATA depending on if the loader has loaded a media
// playlist yet. This is determined by the existence of loader.media_
if (this.media_) {
this.state = 'HAVE_METADATA';
} else {
this.state = 'HAVE_MASTER';
}
} else if (this.state === 'HAVE_CURRENT_METADATA') {
this.state = 'HAVE_METADATA';
}
}
/**
* start loading of the playlist
*/
}, {
key: 'load',
value: function load(isFinalRendition) {
var _this4 = this;
window$1.clearTimeout(this.mediaUpdateTimeout);
var media = this.media();
if (isFinalRendition) {
var delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000;
this.mediaUpdateTimeout = window$1.setTimeout(function () {
return _this4.load();
}, delay);
return;
}
if (!this.started) {
this.start();
return;
}
if (media && !media.endList) {
this.trigger('mediaupdatetimeout');
} else {
this.trigger('loadedplaylist');
}
}
/**
* start loading of the playlist
*/
}, {
key: 'start',
value: function start() {
var _this5 = this;
this.started = true;
// request the specified URL
this.request = this.hls_.xhr({
uri: this.srcUrl,
withCredentials: this.withCredentials
}, function (error, req) {
// disposed
if (!_this5.request) {
return;
}
// clear the loader's request reference
_this5.request = null;
if (error) {
_this5.error = {
status: req.status,
message: 'HLS playlist request error at URL: ' + _this5.srcUrl + '.',
responseText: req.responseText,
// MEDIA_ERR_NETWORK
code: 2
};
if (_this5.state === 'HAVE_NOTHING') {
_this5.started = false;
}
return _this5.trigger('error');
}
var parser = new Parser();
// adding custom tag parsers
_this5.customTagParsers.forEach(function (customParser) {
return parser.addParser(customParser);
});
// adding custom tag mappers
_this5.customTagMappers.forEach(function (mapper) {
return parser.addTagMapper(mapper);
});
parser.push(req.responseText);
parser.end();
_this5.state = 'HAVE_MASTER';
_this5.srcUrl = resolveManifestRedirect(_this5.handleManifestRedirects, _this5.srcUrl, req);
parser.manifest.uri = _this5.srcUrl;
// loaded a master playlist
if (parser.manifest.playlists) {
_this5.master = parser.manifest;
setupMediaPlaylists(_this5.master);
resolveMediaGroupUris(_this5.master);
_this5.trigger('loadedplaylist');
if (!_this5.request) {
// no media playlist was specifically selected so start
// from the first listed one
_this5.media(parser.manifest.playlists[0]);
}
return;
}
var id = createPlaylistID(0, _this5.srcUrl);
// loaded a media playlist
// infer a master playlist if none was previously requested
_this5.master = {
mediaGroups: {
'AUDIO': {},
'VIDEO': {},
'CLOSED-CAPTIONS': {},
'SUBTITLES': {}
},
uri: window$1.location.href,
playlists: [{
uri: _this5.srcUrl,
id: id,
resolvedUri: _this5.srcUrl,
// m3u8-parser does not attach an attributes property to media playlists so make
// sure that the property is attached to avoid undefined reference errors
attributes: {}
}]
};
_this5.master.playlists[id] = _this5.master.playlists[0];
// URI reference added for backwards compatibility
_this5.master.playlists[_this5.srcUrl] = _this5.master.playlists[0];
_this5.haveMetadata(req, _this5.srcUrl, id);
return _this5.trigger('loadedmetadata');
});
}
}]);
return PlaylistLoader;
}(EventTarget);
/**
* @file playlist.js
*
* Playlist related utilities.
*/
var createTimeRange = videojs.createTimeRange;
/**
* walk backward until we find a duration we can use
* or return a failure
*
* @param {Playlist} playlist the playlist to walk through
* @param {Number} endSequence the mediaSequence to stop walking on
*/
var backwardDuration = function backwardDuration(playlist, endSequence) {
var result = 0;
var i = endSequence - playlist.mediaSequence;
// if a start time is available for segment immediately following
// the interval, use it
var segment = playlist.segments[i];
// Walk backward until we find the latest segment with timeline
// information that is earlier than endSequence
if (segment) {
if (typeof segment.start !== 'undefined') {
return { result: segment.start, precise: true };
}
if (typeof segment.end !== 'undefined') {
return {
result: segment.end - segment.duration,
precise: true
};
}
}
while (i--) {
segment = playlist.segments[i];
if (typeof segment.end !== 'undefined') {
return { result: result + segment.end, precise: true };
}
result += segment.duration;
if (typeof segment.start !== 'undefined') {
return { result: result + segment.start, precise: true };
}
}
return { result: result, precise: false };
};
/**
* walk forward until we find a duration we can use
* or return a failure
*
* @param {Playlist} playlist the playlist to walk through
* @param {Number} endSequence the mediaSequence to stop walking on
*/
var forwardDuration = function forwardDuration(playlist, endSequence) {
var result = 0;
var segment = void 0;
var i = endSequence - playlist.mediaSequence;
// Walk forward until we find the earliest segment with timeline
// information
for (; i < playlist.segments.length; i++) {
segment = playlist.segments[i];
if (typeof segment.start !== 'undefined') {
return {
result: segment.start - result,
precise: true
};
}
result += segment.duration;
if (typeof segment.end !== 'undefined') {
return {
result: segment.end - result,
precise: true
};
}
}
// indicate we didn't find a useful duration estimate
return { result: -1, precise: false };
};
/**
* Calculate the media duration from the segments associated with a
* playlist. The duration of a subinterval of the available segments
* may be calculated by specifying an end index.
*
* @param {Object} playlist a media playlist object
* @param {Number=} endSequence an exclusive upper boundary
* for the playlist. Defaults to playlist length.
* @param {Number} expired the amount of time that has dropped
* off the front of the playlist in a live scenario
* @return {Number} the duration between the first available segment
* and end index.
*/
var intervalDuration = function intervalDuration(playlist, endSequence, expired) {
var backward = void 0;
var forward = void 0;
if (typeof endSequence === 'undefined') {
endSequence = playlist.mediaSequence + playlist.segments.length;
}
if (endSequence < playlist.mediaSequence) {
return 0;
}
// do a backward walk to estimate the duration
backward = backwardDuration(playlist, endSequence);
if (backward.precise) {
// if we were able to base our duration estimate on timing
// information provided directly from the Media Source, return
// it
return backward.result;
}
// walk forward to see if a precise duration estimate can be made
// that way
forward = forwardDuration(playlist, endSequence);
if (forward.precise) {
// we found a segment that has been buffered and so it's
// position is known precisely
return forward.result;
}
// return the less-precise, playlist-based duration estimate
return backward.result + expired;
};
/**
* Calculates the duration of a playlist. If a start and end index
* are specified, the duration will be for the subset of the media
* timeline between those two indices. The total duration for live
* playlists is always Infinity.
*
* @param {Object} playlist a media playlist object
* @param {Number=} endSequence an exclusive upper
* boundary for the playlist. Defaults to the playlist media
* sequence number plus its length.
* @param {Number=} expired the amount of time that has
* dropped off the front of the playlist in a live scenario
* @return {Number} the duration between the start index and end
* index.
*/
var duration = function duration(playlist, endSequence, expired) {
if (!playlist) {
return 0;
}
if (typeof expired !== 'number') {
expired = 0;
}
// if a slice of the total duration is not requested, use
// playlist-level duration indicators when they're present
if (typeof endSequence === 'undefined') {
// if present, use the duration specified in the playlist
if (playlist.totalDuration) {
return playlist.totalDuration;
}
// duration should be Infinity for live playlists
if (!playlist.endList) {
return window$1.Infinity;
}
}
// calculate the total duration based on the segment durations
return intervalDuration(playlist, endSequence, expired);
};
/**
* Calculate the time between two indexes in the current playlist
* neight the start- nor the end-index need to be within the current
* playlist in which case, the targetDuration of the playlist is used
* to approximate the durations of the segments
*
* @param {Object} playlist a media playlist object
* @param {Number} startIndex
* @param {Number} endIndex
* @return {Number} the number of seconds between startIndex and endIndex
*/
var sumDurations = function sumDurations(playlist, startIndex, endIndex) {
var durations = 0;
if (startIndex > endIndex) {
var _ref = [endIndex, startIndex];
startIndex = _ref[0];
endIndex = _ref[1];
}
if (startIndex < 0) {
for (var i = startIndex; i < Math.min(0, endIndex); i++) {
durations += playlist.targetDuration;
}
startIndex = 0;
}
for (var _i = startIndex; _i < endIndex; _i++) {
durations += playlist.segments[_i].duration;
}
return durations;
};
/**
* Determines the media index of the segment corresponding to the safe edge of the live
* window which is the duration of the last segment plus 2 target durations from the end
* of the playlist.
*
* A liveEdgePadding can be provided which will be used instead of calculating the safe live edge.
* This corresponds to suggestedPresentationDelay in DASH manifests.
*
* @param {Object} playlist
* a media playlist object
* @param {Number} [liveEdgePadding]
* A number in seconds indicating how far from the end we want to be.
* If provided, this value is used instead of calculating the safe live index from the target durations.
* Corresponds to suggestedPresentationDelay in DASH manifests.
* @return {Number}
* The media index of the segment at the safe live point. 0 if there is no "safe"
* point.
* @function safeLiveIndex
*/
var safeLiveIndex = function safeLiveIndex(playlist, liveEdgePadding) {
if (!playlist.segments.length) {
return 0;
}
var i = playlist.segments.length;
var lastSegmentDuration = playlist.segments[i - 1].duration || playlist.targetDuration;
var safeDistance = typeof liveEdgePadding === 'number' ? liveEdgePadding : lastSegmentDuration + playlist.targetDuration * 2;
if (safeDistance === 0) {
return i;
}
var distanceFromEnd = 0;
while (i--) {
distanceFromEnd += playlist.segments[i].duration;
if (distanceFromEnd >= safeDistance) {
break;
}
}
return Math.max(0, i);
};
/**
* Calculates the playlist end time
*
* @param {Object} playlist a media playlist object
* @param {Number=} expired the amount of time that has
* dropped off the front of the playlist in a live scenario
* @param {Boolean|false} useSafeLiveEnd a boolean value indicating whether or not the
* playlist end calculation should consider the safe live end
* (truncate the playlist end by three segments). This is normally
* used for calculating the end of the playlist's seekable range.
* This takes into account the value of liveEdgePadding.
* Setting liveEdgePadding to 0 is equivalent to setting this to false.
* @param {Number} liveEdgePadding a number indicating how far from the end of the playlist we should be in seconds.
* If this is provided, it is used in the safe live end calculation.
* Setting useSafeLiveEnd=false or liveEdgePadding=0 are equivalent.
* Corresponds to suggestedPresentationDelay in DASH manifests.
* @returns {Number} the end time of playlist
* @function playlistEnd
*/
var playlistEnd = function playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding) {
if (!playlist || !playlist.segments) {
return null;
}
if (playlist.endList) {
return duration(playlist);
}
if (expired === null) {
return null;
}
expired = expired || 0;
var endSequence = useSafeLiveEnd ? safeLiveIndex(playlist, liveEdgePadding) : playlist.segments.length;
return intervalDuration(playlist, playlist.mediaSequence + endSequence, expired);
};
/**
* Calculates the interval of time that is currently seekable in a
* playlist. The returned time ranges are relative to the earliest
* moment in the specified playlist that is still available. A full
* seekable implementation for live streams would need to offset
* these values by the duration of content that has expired from the
* stream.
*
* @param {Object} playlist a media playlist object
* dropped off the front of the playlist in a live scenario
* @param {Number=} expired the amount of time that has
* dropped off the front of the playlist in a live scenario
* @param {Number} liveEdgePadding how far from the end of the playlist we should be in seconds.
* Corresponds to suggestedPresentationDelay in DASH manifests.
* @return {TimeRanges} the periods of time that are valid targets
* for seeking
*/
var seekable = function seekable(playlist, expired, liveEdgePadding) {
var useSafeLiveEnd = true;
var seekableStart = expired || 0;
var seekableEnd = playlistEnd(playlist, expired, useSafeLiveEnd, liveEdgePadding);
if (seekableEnd === null) {
return createTimeRange();
}
return createTimeRange(seekableStart, seekableEnd);
};
var isWholeNumber = function isWholeNumber(num) {
return num - Math.floor(num) === 0;
};
var roundSignificantDigit = function roundSignificantDigit(increment, num) {
// If we have a whole number, just add 1 to it
if (isWholeNumber(num)) {
return num + increment * 0.1;
}
var numDecimalDigits = num.toString().split('.')[1].length;
for (var i = 1; i <= numDecimalDigits; i++) {
var scale = Math.pow(10, i);
var temp = num * scale;
if (isWholeNumber(temp) || i === numDecimalDigits) {
return (temp + increment) / scale;
}
}
};
var ceilLeastSignificantDigit = roundSignificantDigit.bind(null, 1);
var floorLeastSignificantDigit = roundSignificantDigit.bind(null, -1);
/**
* Determine the index and estimated starting time of the segment that
* contains a specified playback position in a media playlist.
*
* @param {Object} playlist the media playlist to query
* @param {Number} currentTime The number of seconds since the earliest
* possible position to determine the containing segment for
* @param {Number} startIndex
* @param {Number} startTime
* @return {Object}
*/
var getMediaInfoForTime = function getMediaInfoForTime(playlist, currentTime, startIndex, startTime) {
var i = void 0;
var segment = void 0;
var numSegments = playlist.segments.length;
var time = currentTime - startTime;
if (time < 0) {
// Walk backward from startIndex in the playlist, adding durations
// until we find a segment that contains `time` and return it
if (startIndex > 0) {
for (i = startIndex - 1; i >= 0; i--) {
segment = playlist.segments[i];
time += floorLeastSignificantDigit(segment.duration);
if (time > 0) {
return {
mediaIndex: i,
startTime: startTime - sumDurations(playlist, startIndex, i)
};
}
}
}
// We were unable to find a good segment within the playlist
// so select the first segment
return {
mediaIndex: 0,
startTime: currentTime
};
}
// When startIndex is negative, we first walk forward to first segment
// adding target durations. If we "run out of time" before getting to
// the first segment, return the first segment
if (startIndex < 0) {
for (i = startIndex; i < 0; i++) {
time -= playlist.targetDuration;
if (time < 0) {
return {
mediaIndex: 0,
startTime: currentTime
};
}
}
startIndex = 0;
}
// Walk forward from startIndex in the playlist, subtracting durations
// until we find a segment that contains `time` and return it
for (i = startIndex; i < numSegments; i++) {
segment = playlist.segments[i];
time -= ceilLeastSignificantDigit(segment.duration);
if (time < 0) {
return {
mediaIndex: i,
startTime: startTime + sumDurations(playlist, startIndex, i)
};
}
}
// We are out of possible candidates so load the last one...
return {
mediaIndex: numSegments - 1,
startTime: currentTime
};
};
/**
* Check whether the playlist is blacklisted or not.
*
* @param {Object} playlist the media playlist object
* @return {boolean} whether the playlist is blacklisted or not
* @function isBlacklisted
*/
var isBlacklisted = function isBlacklisted(playlist) {
return playlist.excludeUntil && playlist.excludeUntil > Date.now();
};
/**
* Check whether the playlist is compatible with current playback configuration or has
* been blacklisted permanently for being incompatible.
*
* @param {Object} playlist the media playlist object
* @return {boolean} whether the playlist is incompatible or not
* @function isIncompatible
*/
var isIncompatible = function isIncompatible(playlist) {
return playlist.excludeUntil && playlist.excludeUntil === Infinity;
};
/**
* Check whether the playlist is enabled or not.
*
* @param {Object} playlist the media playlist object
* @return {boolean} whether the playlist is enabled or not
* @function isEnabled
*/
var isEnabled = function isEnabled(playlist) {
var blacklisted = isBlacklisted(playlist);
return !playlist.disabled && !blacklisted;
};
/**
* Check whether the playlist has been manually disabled through the representations api.
*
* @param {Object} playlist the media playlist object
* @return {boolean} whether the playlist is disabled manually or not
* @function isDisabled
*/
var isDisabled = function isDisabled(playlist) {
return playlist.disabled;
};
/**
* Returns whether the current playlist is an AES encrypted HLS stream
*
* @return {Boolean} true if it's an AES encrypted HLS stream
*/
var isAes = function isAes(media) {
for (var i = 0; i < media.segments.length; i++) {
if (media.segments[i].key) {
return true;
}
}
return false;
};
/**
* Returns whether the current playlist contains fMP4
*
* @return {Boolean} true if the playlist contains fMP4
*/
var isFmp4 = function isFmp4(media) {
for (var i = 0; i < media.segments.length; i++) {
if (media.segments[i].map) {
return true;
}
}
return false;
};
/**
* Checks if the playlist has a value for the specified attribute
*
* @param {String} attr
* Attribute to check for
* @param {Object} playlist
* The media playlist object
* @return {Boolean}
* Whether the playlist contains a value for the attribute or not
* @function hasAttribute
*/
var hasAttribute = function hasAttribute(attr, playlist) {
return playlist.attributes && playlist.attributes[attr];
};
/**
* Estimates the time required to complete a segment download from the specified playlist
*
* @param {Number} segmentDuration
* Duration of requested segment
* @param {Number} bandwidth
* Current measured bandwidth of the player
* @param {Object} playlist
* The media playlist object
* @param {Number=} bytesReceived
* Number of bytes already received for the request. Defaults to 0
* @return {Number|NaN}
* The estimated time to request the segment. NaN if bandwidth information for
* the given playlist is unavailable
* @function estimateSegmentRequestTime
*/
var estimateSegmentRequestTime = function estimateSegmentRequestTime(segmentDuration, bandwidth, playlist) {
var bytesReceived = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : 0;
if (!hasAttribute('BANDWIDTH', playlist)) {
return NaN;
}
var size = segmentDuration * playlist.attributes.BANDWIDTH;
return (size - bytesReceived * 8) / bandwidth;
};
/*
* Returns whether the current playlist is the lowest rendition
*
* @return {Boolean} true if on lowest rendition
*/
var isLowestEnabledRendition = function isLowestEnabledRendition(master, media) {
if (master.playlists.length === 1) {
return true;
}
var currentBandwidth = media.attributes.BANDWIDTH || Number.MAX_VALUE;
return master.playlists.filter(function (playlist) {
if (!isEnabled(playlist)) {
return false;
}
return (playlist.attributes.BANDWIDTH || 0) < currentBandwidth;
}).length === 0;
};
// exports
var Playlist = {
duration: duration,
seekable: seekable,
safeLiveIndex: safeLiveIndex,
getMediaInfoForTime: getMediaInfoForTime,
isEnabled: isEnabled,
isDisabled: isDisabled,
isBlacklisted: isBlacklisted,
isIncompatible: isIncompatible,
playlistEnd: playlistEnd,
isAes: isAes,
isFmp4: isFmp4,
hasAttribute: hasAttribute,
estimateSegmentRequestTime: estimateSegmentRequestTime,
isLowestEnabledRendition: isLowestEnabledRendition
};
/**
* @file xhr.js
*/
var videojsXHR = videojs.xhr,
mergeOptions$1 = videojs.mergeOptions;
var xhrFactory = function xhrFactory() {
var xhr = function XhrFunction(options, callback) {
// Add a default timeout for all hls requests
options = mergeOptions$1({
timeout: 45e3
}, options);
// Allow an optional user-specified function to modify the option
// object before we construct the xhr request
var beforeRequest = XhrFunction.beforeRequest || videojs.Hls.xhr.beforeRequest;
if (beforeRequest && typeof beforeRequest === 'function') {
var newOptions = beforeRequest(options);
if (newOptions) {
options = newOptions;
}
}
var request = videojsXHR(options, function (error, response) {
var reqResponse = request.response;
if (!error && reqResponse) {
request.responseTime = Date.now();
request.roundTripTime = request.responseTime - request.requestTime;
request.bytesReceived = reqResponse.byteLength || reqResponse.length;
if (!request.bandwidth) {
request.bandwidth = Math.floor(request.bytesReceived / request.roundTripTime * 8 * 1000);
}
}
if (response.headers) {
request.responseHeaders = response.headers;
}
// videojs.xhr now uses a specific code on the error
// object to signal that a request has timed out instead
// of setting a boolean on the request object
if (error && error.code === 'ETIMEDOUT') {
request.timedout = true;
}
// videojs.xhr no longer considers status codes outside of 200 and 0
// (for file uris) to be errors, but the old XHR did, so emulate that
// behavior. Status 206 may be used in response to byterange requests.
if (!error && !request.aborted && response.statusCode !== 200 && response.statusCode !== 206 && response.statusCode !== 0) {
error = new Error('XHR Failed with a response of: ' + (request && (reqResponse || request.responseText)));
}
callback(error, request);
});
var originalAbort = request.abort;
request.abort = function () {
request.aborted = true;
return originalAbort.apply(request, arguments);
};
request.uri = options.uri;
request.requestTime = Date.now();
return request;
};
return xhr;
};
/**
* Turns segment byterange into a string suitable for use in
* HTTP Range requests
*
* @param {Object} byterange - an object with two values defining the start and end
* of a byte-range
*/
var byterangeStr = function byterangeStr(byterange) {
var byterangeStart = void 0;
var byterangeEnd = void 0;
// `byterangeEnd` is one less than `offset + length` because the HTTP range
// header uses inclusive ranges
byterangeEnd = byterange.offset + byterange.length - 1;
byterangeStart = byterange.offset;
return 'bytes=' + byterangeStart + '-' + byterangeEnd;
};
/**
* Defines headers for use in the xhr request for a particular segment.
*
* @param {Object} segment - a simplified copy of the segmentInfo object
* from SegmentLoader
*/
var segmentXhrHeaders = function segmentXhrHeaders(segment) {
var headers = {};
if (segment.byterange) {
headers.Range = byterangeStr(segment.byterange);
}
return headers;
};
/**
* @file bin-utils.js
*/
/**
* convert a TimeRange to text
*
* @param {TimeRange} range the timerange to use for conversion
* @param {Number} i the iterator on the range to convert
*/
var textRange = function textRange(range, i) {
return range.start(i) + '-' + range.end(i);
};
/**
* format a number as hex string
*
* @param {Number} e The number
* @param {Number} i the iterator
*/
var formatHexString = function formatHexString(e, i) {
var value = e.toString(16);
return '00'.substring(0, 2 - value.length) + value + (i % 2 ? ' ' : '');
};
var formatAsciiString = function formatAsciiString(e) {
if (e >= 0x20 && e < 0x7e) {
return String.fromCharCode(e);
}
return '.';
};
/**
* Creates an object for sending to a web worker modifying properties that are TypedArrays
* into a new object with seperated properties for the buffer, byteOffset, and byteLength.
*
* @param {Object} message
* Object of properties and values to send to the web worker
* @return {Object}
* Modified message with TypedArray values expanded
* @function createTransferableMessage
*/
var createTransferableMessage = function createTransferableMessage(message) {
var transferable = {};
Object.keys(message).forEach(function (key) {
var value = message[key];
if (ArrayBuffer.isView(value)) {
transferable[key] = {
bytes: value.buffer,
byteOffset: value.byteOffset,
byteLength: value.byteLength
};
} else {
transferable[key] = value;
}
});
return transferable;
};
/**
* Returns a unique string identifier for a media initialization
* segment.
*/
var initSegmentId = function initSegmentId(initSegment) {
var byterange = initSegment.byterange || {
length: Infinity,
offset: 0
};
return [byterange.length, byterange.offset, initSegment.resolvedUri].join(',');
};
/**
* Returns a unique string identifier for a media segment key.
*/
var segmentKeyId = function segmentKeyId(key) {
return key.resolvedUri;
};
/**
* utils to help dump binary data to the console
*/
var hexDump = function hexDump(data) {
var bytes = Array.prototype.slice.call(data);
var step = 16;
var result = '';
var hex = void 0;
var ascii = void 0;
for (var j = 0; j < bytes.length / step; j++) {
hex = bytes.slice(j * step, j * step + step).map(formatHexString).join('');
ascii = bytes.slice(j * step, j * step + step).map(formatAsciiString).join('');
result += hex + ' ' + ascii + '\n';
}
return result;
};
var tagDump = function tagDump(_ref) {
var bytes = _ref.bytes;
return hexDump(bytes);
};
var textRanges = function textRanges(ranges) {
var result = '';
var i = void 0;
for (i = 0; i < ranges.length; i++) {
result += textRange(ranges, i) + ' ';
}
return result;
};
var utils = /*#__PURE__*/Object.freeze({
createTransferableMessage: createTransferableMessage,
initSegmentId: initSegmentId,
segmentKeyId: segmentKeyId,
hexDump: hexDump,
tagDump: tagDump,
textRanges: textRanges
});
// TODO handle fmp4 case where the timing info is accurate and doesn't involve transmux
// Add 25% to the segment duration to account for small discrepencies in segment timing.
// 25% was arbitrarily chosen, and may need to be refined over time.
var SEGMENT_END_FUDGE_PERCENT = 0.25;
/**
* Co