@videojs/http-streaming
Version:
Play back HLS and DASH with Video.js, even where it's not natively supported
1,597 lines (1,342 loc) • 797 kB
JavaScript
/*! @name @videojs/http-streaming @version 2.6.0 @license Apache-2.0 */
import _assertThisInitialized from '@babel/runtime/helpers/assertThisInitialized';
import _inheritsLoose from '@babel/runtime/helpers/inheritsLoose';
import document from 'global/document';
import window from 'global/window';
import _resolveUrl from '@videojs/vhs-utils/es/resolve-url.js';
import videojs from 'video.js';
import { Parser } from 'm3u8-parser';
import { simpleTypeFromSourceType } from '@videojs/vhs-utils/es/media-types.js';
export { simpleTypeFromSourceType } from '@videojs/vhs-utils/es/media-types.js';
import { parseUTCTiming, parse, addSidxSegmentsToPlaylist } from 'mpd-parser';
import parseSidx from 'mux.js/lib/tools/parse-sidx';
import { getId3Offset } from '@videojs/vhs-utils/es/id3-helpers';
import { detectContainerForBytes, isLikelyFmp4MediaSegment } from '@videojs/vhs-utils/es/containers';
import { concatTypedArrays, stringToBytes, toUint8 } from '@videojs/vhs-utils/es/byte-helpers';
import tsInspector from 'mux.js/lib/tools/ts-inspector.js';
import { ONE_SECOND_IN_TS } from 'mux.js/lib/utils/clock';
import mp4probe from 'mux.js/lib/mp4/probe';
import { translateLegacyCodec, codecsFromDefault, parseCodecs, getMimeForCodec, DEFAULT_VIDEO_CODEC, DEFAULT_AUDIO_CODEC, browserSupportsCodec, muxerSupportsCodec, isAudioCodec, isVideoCodec } from '@videojs/vhs-utils/es/codecs.js';
/**
* @file resolve-url.js - Handling how URLs are resolved and manipulated
*/
var resolveUrl = _resolveUrl;
/**
* 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 && req.responseURL && url !== req.responseURL) {
return req.responseURL;
}
return url;
};
var logger = function logger(source) {
if (videojs.log.debug) {
return videojs.log.debug.bind(videojs, 'VHS:', source + " >");
}
return function () {};
};
var log = videojs.log;
var createPlaylistID = function createPlaylistID(index, uri) {
return index + "-" + uri;
};
/**
* Parses a given m3u8 playlist
*
* @param {string} manifestString
* The downloaded manifest string
* @param {Object[]} [customTagParsers]
* An array of custom tag parsers for the m3u8-parser instance
* @param {Object[]} [customTagMappers]
* An array of custom tag mappers for the m3u8-parser instance
* @return {Object}
* The manifest object
*/
var parseManifest = function parseManifest(_ref) {
var onwarn = _ref.onwarn,
oninfo = _ref.oninfo,
manifestString = _ref.manifestString,
_ref$customTagParsers = _ref.customTagParsers,
customTagParsers = _ref$customTagParsers === void 0 ? [] : _ref$customTagParsers,
_ref$customTagMappers = _ref.customTagMappers,
customTagMappers = _ref$customTagMappers === void 0 ? [] : _ref$customTagMappers;
var parser = new Parser();
if (onwarn) {
parser.on('warn', onwarn);
}
if (oninfo) {
parser.on('info', oninfo);
}
customTagParsers.forEach(function (customParser) {
return parser.addParser(customParser);
});
customTagMappers.forEach(function (mapper) {
return parser.addTagMapper(mapper);
});
parser.push(manifestString);
parser.end();
return parser.manifest;
};
/**
* 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);
}
}
});
};
/**
* Adds properties and attributes to the playlist to keep consistent functionality for
* playlists throughout VHS.
*
* @param {Object} config
* Arguments object
* @param {Object} config.playlist
* The media playlist
* @param {string} [config.uri]
* The uri to the media playlist (if media playlist is not from within a master
* playlist)
* @param {string} id
* ID to use for the playlist
*/
var setupMediaPlaylist = function setupMediaPlaylist(_ref2) {
var playlist = _ref2.playlist,
uri = _ref2.uri,
id = _ref2.id;
playlist.id = id;
if (uri) {
// For media playlists, m3u8-parser does not have access to a URI, as HLS media
// playlists do not contain their own source URI, but one is needed for consistency in
// VHS.
playlist.uri = uri;
} // For HLS master playlists, even though certain attributes MUST be defined, the
// stream may still be played without them.
// For HLS media playlists, m3u8-parser does not attach an attributes object to the
// manifest.
//
// To avoid undefined reference errors through the project, and make the code easier
// to write/read, add an empty attributes object for these cases.
playlist.attributes = playlist.attributes || {};
};
/**
* Adds ID, resolvedUri, and attributes properties to each playlist of the master, where
* necessary. In addition, creates playlist IDs for each playlist and adds playlist ID to
* playlist references to the playlists array.
*
* @param {Object} master
* The master playlist
*/
var setupMediaPlaylists = function setupMediaPlaylists(master) {
var i = master.playlists.length;
while (i--) {
var playlist = master.playlists[i];
setupMediaPlaylist({
playlist: playlist,
id: createPlaylistID(i, playlist.uri)
});
playlist.resolvedUri = resolveUrl(master.uri, playlist.uri);
master.playlists[playlist.id] = playlist; // URI reference added for backwards compatibility
master.playlists[playlist.uri] = playlist; // Although the spec states an #EXT-X-STREAM-INF tag MUST have a BANDWIDTH attribute,
// the stream can be played without it. Although an attributes property may have been
// added to the playlist to prevent undefined references, issue a warning to fix the
// manifest.
if (!playlist.attributes.BANDWIDTH) {
log.warn('Invalid playlist STREAM-INF detected. Missing BANDWIDTH attribute.');
}
}
};
/**
* Adds resolvedUri properties to each media group.
*
* @param {Object} master
* The master playlist
*/
var resolveMediaGroupUris = function resolveMediaGroupUris(master) {
forEachMediaGroup(master, function (properties) {
if (properties.uri) {
properties.resolvedUri = resolveUrl(master.uri, properties.uri);
}
});
};
/**
* Creates a master playlist wrapper to insert a sole media playlist into.
*
* @param {Object} media
* Media playlist
* @param {string} uri
* The media URI
*
* @return {Object}
* Master playlist
*/
var masterForMedia = function masterForMedia(media, uri) {
var id = createPlaylistID(0, uri);
var master = {
mediaGroups: {
'AUDIO': {},
'VIDEO': {},
'CLOSED-CAPTIONS': {},
'SUBTITLES': {}
},
uri: window.location.href,
resolvedUri: window.location.href,
playlists: [{
uri: uri,
id: id,
resolvedUri: uri,
// 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: {}
}]
}; // set up ID reference
master.playlists[id] = master.playlists[0]; // URI reference added for backwards compatibility
master.playlists[uri] = master.playlists[0];
return master;
};
/**
* Does an in-place update of the master manifest to add updated playlist URI references
* as well as other properties needed by VHS that aren't included by the parser.
*
* @param {Object} master
* Master manifest object
* @param {string} uri
* The source URI
*/
var addPropertiesToMaster = function addPropertiesToMaster(master, uri) {
master.uri = uri;
for (var i = 0; i < master.playlists.length; i++) {
if (!master.playlists[i].uri) {
// Set up phony URIs for the playlists since playlists are referenced by their URIs
// throughout VHS, but some formats (e.g., DASH) don't have external URIs
// TODO: consider adding dummy URIs in mpd-parser
var phonyUri = "placeholder-uri-" + i;
master.playlists[i].uri = phonyUri;
}
}
forEachMediaGroup(master, function (properties, mediaType, groupKey, labelKey) {
if (!properties.playlists || !properties.playlists.length || properties.playlists[0].uri) {
return;
} // Set up phony URIs for the media group playlists since playlists are referenced by
// their URIs throughout VHS, but some formats (e.g., DASH) don't have external URIs
var phonyUri = "placeholder-uri-" + mediaType + "-" + groupKey + "-" + labelKey;
var id = createPlaylistID(0, phonyUri);
properties.playlists[0].uri = phonyUri;
properties.playlists[0].id = id; // setup ID and URI references (URI for backwards compatibility)
master.playlists[id] = properties.playlists[0];
master.playlists[phonyUri] = properties.playlists[0];
});
setupMediaPlaylists(master);
resolveMediaGroupUris(master);
};
var mergeOptions = videojs.mergeOptions,
EventTarget = videojs.EventTarget;
/**
* 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;
};
/**
* 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;
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|Object} src url or object of manifest
* @param {boolean} withCredentials the withCredentials xhr option
* @class
*/
var PlaylistLoader = /*#__PURE__*/function (_EventTarget) {
_inheritsLoose(PlaylistLoader, _EventTarget);
function PlaylistLoader(src, vhs, options) {
var _this;
if (options === void 0) {
options = {};
}
_this = _EventTarget.call(this) || this;
if (!src) {
throw new Error('A non-empty playlist URL or object is required');
}
_this.logger_ = logger('PlaylistLoader');
var _options = options,
_options$withCredenti = _options.withCredentials,
withCredentials = _options$withCredenti === void 0 ? false : _options$withCredenti,
_options$handleManife = _options.handleManifestRedirects,
handleManifestRedirects = _options$handleManife === void 0 ? false : _options$handleManife;
_this.src = src;
_this.vhs_ = vhs;
_this.withCredentials = withCredentials;
_this.handleManifestRedirects = handleManifestRedirects;
var vhsOptions = vhs.options_;
_this.customTagParsers = vhsOptions && vhsOptions.customTagParsers || [];
_this.customTagMappers = vhsOptions && vhsOptions.customTagMappers || []; // 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.vhs_.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({
playlistString: _this.request.responseText,
url: _this.media().uri,
id: _this.media().id
});
});
});
return _this;
}
var _proto = PlaylistLoader.prototype;
_proto.playlistRequestError = 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.
*
* @param {string} [playlistString]
* Playlist string (if playlistObject is not provided)
* @param {Object} [playlistObject]
* Playlist object (if playlistString is not provided)
* @param {string} url
* URL of playlist
* @param {string} id
* ID to use for playlist
*/
;
_proto.haveMetadata = function haveMetadata(_ref) {
var _this2 = this;
var playlistString = _ref.playlistString,
playlistObject = _ref.playlistObject,
url = _ref.url,
id = _ref.id;
// any in-flight request is now finished
this.request = null;
this.state = 'HAVE_METADATA';
var playlist = playlistObject || parseManifest({
onwarn: function onwarn(_ref2) {
var message = _ref2.message;
return _this2.logger_("m3u8-parser warn for " + id + ": " + message);
},
oninfo: function oninfo(_ref3) {
var message = _ref3.message;
return _this2.logger_("m3u8-parser info for " + id + ": " + message);
},
manifestString: playlistString,
customTagParsers: this.customTagParsers,
customTagMappers: this.customTagMappers
});
playlist.lastRequest = Date.now();
setupMediaPlaylist({
playlist: playlist,
uri: url,
id: id
}); // merge this playlist into the master
var update = updateMaster(this.master, playlist);
this.targetDuration = playlist.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.clearTimeout(this.mediaUpdateTimeout);
this.mediaUpdateTimeout = window.setTimeout(function () {
_this2.trigger('mediaupdatetimeout');
}, refreshDelay(this.media(), !!update));
}
this.trigger('loadedplaylist');
}
/**
* Abort any outstanding work and clean up.
*/
;
_proto.dispose = function dispose() {
this.trigger('dispose');
this.stopRequest();
window.clearTimeout(this.mediaUpdateTimeout);
window.clearTimeout(this.finalRenditionTimeout);
this.off();
};
_proto.stopRequest = 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=} shouldDelay whether we should delay the request by half target duration
*
* @return {Playlist} the current loaded media
*/
;
_proto.media = function media(playlist, shouldDelay) {
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.clearTimeout(this.finalRenditionTimeout);
if (shouldDelay) {
var delay = playlist.targetDuration / 2 * 1000 || 5 * 1000;
this.finalRenditionTimeout = window.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 || // handle the case of a playlist object (e.g., if using vhs-json with a resolved
// media playlist or, for the case of demuxed audio, a resolved audio media group)
playlist.endList && playlist.segments.length) {
// 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');
if (startingState === 'HAVE_MASTER') {
// The initial playlist was a master manifest, and the first media selected was
// also provided (in the form of a resolved playlist object) as part of the
// source object (rather than just a URL). Therefore, since the media playlist
// doesn't need to be requested, loadedmetadata won't trigger as part of the
// normal flow, and needs an explicit trigger here.
this.trigger('loadedmetadata');
} else {
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.vhs_.xhr({
uri: playlist.resolvedUri,
withCredentials: this.withCredentials
}, function (error, req) {
// disposed
if (!_this3.request) {
return;
}
playlist.lastRequest = Date.now();
playlist.resolvedUri = resolveManifestRedirect(_this3.handleManifestRedirects, playlist.resolvedUri, req);
if (error) {
return _this3.playlistRequestError(_this3.request, playlist, startingState);
}
_this3.haveMetadata({
playlistString: req.responseText,
url: playlist.uri,
id: 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
*/
;
_proto.pause = function pause() {
this.stopRequest();
window.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
*/
;
_proto.load = function load(shouldDelay) {
var _this4 = this;
window.clearTimeout(this.mediaUpdateTimeout);
var media = this.media();
if (shouldDelay) {
var delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000;
this.mediaUpdateTimeout = window.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
*/
;
_proto.start = function start() {
var _this5 = this;
this.started = true;
if (typeof this.src === 'object') {
// in the case of an entirely constructed manifest object (meaning there's no actual
// manifest on a server), default the uri to the page's href
if (!this.src.uri) {
this.src.uri = window.location.href;
} // resolvedUri is added on internally after the initial request. Since there's no
// request for pre-resolved manifests, add on resolvedUri here.
this.src.resolvedUri = this.src.uri; // Since a manifest object was passed in as the source (instead of a URL), the first
// request can be skipped (since the top level of the manifest, at a minimum, is
// already available as a parsed manifest object). However, if the manifest object
// represents a master playlist, some media playlists may need to be resolved before
// the starting segment list is available. Therefore, go directly to setup of the
// initial playlist, and let the normal flow continue from there.
//
// Note that the call to setup is asynchronous, as other sections of VHS may assume
// that the first request is asynchronous.
setTimeout(function () {
_this5.setupInitialPlaylist(_this5.src);
}, 0);
return;
} // request the specified URL
this.request = this.vhs_.xhr({
uri: this.src,
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.src + ".",
responseText: req.responseText,
// MEDIA_ERR_NETWORK
code: 2
};
if (_this5.state === 'HAVE_NOTHING') {
_this5.started = false;
}
return _this5.trigger('error');
}
_this5.src = resolveManifestRedirect(_this5.handleManifestRedirects, _this5.src, req);
var manifest = parseManifest({
manifestString: req.responseText,
customTagParsers: _this5.customTagParsers,
customTagMappers: _this5.customTagMappers
});
_this5.setupInitialPlaylist(manifest);
});
};
_proto.srcUri = function srcUri() {
return typeof this.src === 'string' ? this.src : this.src.uri;
}
/**
* Given a manifest object that's either a master or media playlist, trigger the proper
* events and set the state of the playlist loader.
*
* If the manifest object represents a master playlist, `loadedplaylist` will be
* triggered to allow listeners to select a playlist. If none is selected, the loader
* will default to the first one in the playlists array.
*
* If the manifest object represents a media playlist, `loadedplaylist` will be
* triggered followed by `loadedmetadata`, as the only available playlist is loaded.
*
* In the case of a media playlist, a master playlist object wrapper with one playlist
* will be created so that all logic can handle playlists in the same fashion (as an
* assumed manifest object schema).
*
* @param {Object} manifest
* The parsed manifest object
*/
;
_proto.setupInitialPlaylist = function setupInitialPlaylist(manifest) {
this.state = 'HAVE_MASTER';
if (manifest.playlists) {
this.master = manifest;
addPropertiesToMaster(this.master, this.srcUri()); // If the initial master playlist has playlists wtih segments already resolved,
// then resolve URIs in advance, as they are usually done after a playlist request,
// which may not happen if the playlist is resolved.
manifest.playlists.forEach(function (playlist) {
if (playlist.segments) {
playlist.segments.forEach(function (segment) {
resolveSegmentUris(segment, playlist.resolvedUri);
});
}
});
this.trigger('loadedplaylist');
if (!this.request) {
// no media playlist was specifically selected so start
// from the first listed one
this.media(this.master.playlists[0]);
}
return;
} // In order to support media playlists passed in as vhs-json, the case where the uri
// is not provided as part of the manifest should be considered, and an appropriate
// default used.
var uri = this.srcUri() || window.location.href;
this.master = masterForMedia(manifest, uri);
this.haveMetadata({
playlistObject: manifest,
url: uri,
id: this.master.playlists[0].id
});
this.trigger('loadedmetadata');
};
return PlaylistLoader;
}(EventTarget);
/**
* ranges
*
* Utilities for working with TimeRanges.
*
*/
var TIME_FUDGE_FACTOR = 1 / 30; // Comparisons between time values such as current time and the end of the buffered range
// can be misleading because of precision differences or when the current media has poorly
// aligned audio and video, which can cause values to be slightly off from what you would
// expect. This value is what we consider to be safe to use in such comparisons to account
// for these scenarios.
var SAFE_TIME_DELTA = TIME_FUDGE_FACTOR * 3;
var filterRanges = function filterRanges(timeRanges, predicate) {
var results = [];
var i;
if (timeRanges && timeRanges.length) {
// Search for ranges that match the predicate
for (i = 0; i < timeRanges.length; i++) {
if (predicate(timeRanges.start(i), timeRanges.end(i))) {
results.push([timeRanges.start(i), timeRanges.end(i)]);
}
}
}
return videojs.createTimeRanges(results);
};
/**
* Attempts to find the buffered TimeRange that contains the specified
* time.
*
* @param {TimeRanges} buffered - the TimeRanges object to query
* @param {number} time - the time to filter on.
* @return {TimeRanges} a new TimeRanges object
*/
var findRange = function findRange(buffered, time) {
return filterRanges(buffered, function (start, end) {
return start - SAFE_TIME_DELTA <= time && end + SAFE_TIME_DELTA >= time;
});
};
/**
* Returns the TimeRanges that begin later than the specified time.
*
* @param {TimeRanges} timeRanges - the TimeRanges object to query
* @param {number} time - the time to filter on.
* @return {TimeRanges} a new TimeRanges object.
*/
var findNextRange = function findNextRange(timeRanges, time) {
return filterRanges(timeRanges, function (start) {
return start - TIME_FUDGE_FACTOR >= time;
});
};
/**
* Returns gaps within a list of TimeRanges
*
* @param {TimeRanges} buffered - the TimeRanges object
* @return {TimeRanges} a TimeRanges object of gaps
*/
var findGaps = function findGaps(buffered) {
if (buffered.length < 2) {
return videojs.createTimeRanges();
}
var ranges = [];
for (var i = 1; i < buffered.length; i++) {
var start = buffered.end(i - 1);
var end = buffered.start(i);
ranges.push([start, end]);
}
return videojs.createTimeRanges(ranges);
};
/**
* Calculate the intersection of two TimeRanges
*
* @param {TimeRanges} bufferA
* @param {TimeRanges} bufferB
* @return {TimeRanges} The interesection of `bufferA` with `bufferB`
*/
var bufferIntersection = function bufferIntersection(bufferA, bufferB) {
var start = null;
var end = null;
var arity = 0;
var extents = [];
var ranges = [];
if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) {
return videojs.createTimeRange();
} // Handle the case where we have both buffers and create an
// intersection of the two
var count = bufferA.length; // A) Gather up all start and end times
while (count--) {
extents.push({
time: bufferA.start(count),
type: 'start'
});
extents.push({
time: bufferA.end(count),
type: 'end'
});
}
count = bufferB.length;
while (count--) {
extents.push({
time: bufferB.start(count),
type: 'start'
});
extents.push({
time: bufferB.end(count),
type: 'end'
});
} // B) Sort them by time
extents.sort(function (a, b) {
return a.time - b.time;
}); // C) Go along one by one incrementing arity for start and decrementing
// arity for ends
for (count = 0; count < extents.length; count++) {
if (extents[count].type === 'start') {
arity++; // D) If arity is ever incremented to 2 we are entering an
// overlapping range
if (arity === 2) {
start = extents[count].time;
}
} else if (extents[count].type === 'end') {
arity--; // E) If arity is ever decremented to 1 we leaving an
// overlapping range
if (arity === 1) {
end = extents[count].time;
}
} // F) Record overlapping ranges
if (start !== null && end !== null) {
ranges.push([start, end]);
start = null;
end = null;
}
}
return videojs.createTimeRanges(ranges);
};
/**
* Gets a human readable string for a TimeRange
*
* @param {TimeRange} range
* @return {string} a human readable string
*/
var printableRange = function printableRange(range) {
var strArr = [];
if (!range || !range.length) {
return '';
}
for (var i = 0; i < range.length; i++) {
strArr.push(range.start(i) + ' => ' + range.end(i));
}
return strArr.join(', ');
};
/**
* Calculates the amount of time left in seconds until the player hits the end of the
* buffer and causes a rebuffer
*
* @param {TimeRange} buffered
* The state of the buffer
* @param {Numnber} currentTime
* The current time of the player
* @param {number} playbackRate
* The current playback rate of the player. Defaults to 1.
* @return {number}
* Time until the player has to start rebuffering in seconds.
* @function timeUntilRebuffer
*/
var timeUntilRebuffer = function timeUntilRebuffer(buffered, currentTime, playbackRate) {
if (playbackRate === void 0) {
playbackRate = 1;
}
var bufferedEnd = buffered.length ? buffered.end(buffered.length - 1) : 0;
return (bufferedEnd - currentTime) / playbackRate;
};
/**
* Converts a TimeRanges object into an array representation
*
* @param {TimeRanges} timeRanges
* @return {Array}
*/
var timeRangesToArray = function timeRangesToArray(timeRanges) {
var timeRangesList = [];
for (var i = 0; i < timeRanges.length; i++) {
timeRangesList.push({
start: timeRanges.start(i),
end: timeRanges.end(i)
});
}
return timeRangesList;
};
/**
* Determines if two time range objects are different.
*
* @param {TimeRange} a
* the first time range object to check
*
* @param {TimeRange} b
* the second time range object to check
*
* @return {Boolean}
* Whether the time range objects differ
*/
var isRangeDifferent = function isRangeDifferent(a, b) {
// same object
if (a === b) {
return false;
} // one or the other is undefined
if (!a && b || !b && a) {
return true;
} // length is different
if (a.length !== b.length) {
return true;
} // see if any start/end pair is different
for (var i = 0; i < a.length; i++) {
if (a.start(i) !== b.start(i) || a.end(i) !== b.end(i)) {
return true;
}
} // if the length and every pair is the same
// this is the same time range
return false;
};
/**
* @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;
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) {
if (typeof endSequence === 'undefined') {
endSequence = playlist.mediaSequence + playlist.segments.length;
}
if (endSequence < playlist.mediaSequence) {
return 0;
} // do a backward walk to estimate the duration
var 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
var 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.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.
* @return {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);
};
/**
* 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;
var segment;
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 += segment.duration + TIME_FUDGE_FACTOR;
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 -= segment.duration + TIME_FUDGE_FACTOR;
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