UNPKG

@videojs/http-streaming

Version:

Play back HLS and DASH with Video.js, even where it's not natively supported

1,567 lines (1,320 loc) 803 kB
/*! @name @videojs/http-streaming @version 2.6.0 @license Apache-2.0 */ 'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _assertThisInitialized = require('@babel/runtime/helpers/assertThisInitialized'); var _inheritsLoose = require('@babel/runtime/helpers/inheritsLoose'); var document = require('global/document'); var window = require('global/window'); var _resolveUrl = require('@videojs/vhs-utils/cjs/resolve-url.js'); var videojs = require('video.js'); var m3u8Parser = require('m3u8-parser'); var mediaTypes_js = require('@videojs/vhs-utils/cjs/media-types.js'); var mpdParser = require('mpd-parser'); var parseSidx = require('mux.js/lib/tools/parse-sidx'); var id3Helpers = require('@videojs/vhs-utils/cjs/id3-helpers'); var containers = require('@videojs/vhs-utils/cjs/containers'); var byteHelpers = require('@videojs/vhs-utils/cjs/byte-helpers'); var tsInspector = require('mux.js/lib/tools/ts-inspector.js'); var clock = require('mux.js/lib/utils/clock'); var mp4probe = require('mux.js/lib/mp4/probe'); var codecs_js = require('@videojs/vhs-utils/cjs/codecs.js'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var _assertThisInitialized__default = /*#__PURE__*/_interopDefaultLegacy(_assertThisInitialized); var _inheritsLoose__default = /*#__PURE__*/_interopDefaultLegacy(_inheritsLoose); var document__default = /*#__PURE__*/_interopDefaultLegacy(document); var window__default = /*#__PURE__*/_interopDefaultLegacy(window); var _resolveUrl__default = /*#__PURE__*/_interopDefaultLegacy(_resolveUrl); var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs); var parseSidx__default = /*#__PURE__*/_interopDefaultLegacy(parseSidx); var tsInspector__default = /*#__PURE__*/_interopDefaultLegacy(tsInspector); var mp4probe__default = /*#__PURE__*/_interopDefaultLegacy(mp4probe); /** * @file resolve-url.js - Handling how URLs are resolved and manipulated */ var resolveUrl = _resolveUrl__default['default']; /** * 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__default['default'].log.debug) { return videojs__default['default'].log.debug.bind(videojs__default['default'], 'VHS:', source + " >"); } return function () {}; }; var log = videojs__default['default'].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 m3u8Parser.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__default['default'].location.href, resolvedUri: window__default['default'].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__default['default'].mergeOptions, EventTarget = videojs__default['default'].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__default['default'](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__default['default'].clearTimeout(this.mediaUpdateTimeout); this.mediaUpdateTimeout = window__default['default'].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__default['default'].clearTimeout(this.mediaUpdateTimeout); window__default['default'].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__default['default'].clearTimeout(this.finalRenditionTimeout); if (shouldDelay) { var delay = playlist.targetDuration / 2 * 1000 || 5 * 1000; this.finalRenditionTimeout = window__default['default'].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__default['default'].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__default['default'].clearTimeout(this.mediaUpdateTimeout); var media = this.media(); if (shouldDelay) { var delay = media ? media.targetDuration / 2 * 1000 : 5 * 1000; this.mediaUpdateTimeout = window__default['default'].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__default['default'].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__default['default'].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__default['default'].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__default['default'].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__default['default'].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__default['default'].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__default['default'].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__default['default'].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__default['default'].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 t