UNPKG

@videojs/http-streaming

Version:

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

1,630 lines (1,365 loc) 895 kB
/*! @name @videojs/http-streaming @version 2.14.3 @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$1 = require('global/window'); var _resolveUrl = require('@videojs/vhs-utils/cjs/resolve-url.js'); var videojs = require('video.js'); var _extends = require('@babel/runtime/helpers/extends'); var m3u8Parser = require('m3u8-parser'); var codecs_js = require('@videojs/vhs-utils/cjs/codecs.js'); var mediaTypes_js = require('@videojs/vhs-utils/cjs/media-types.js'); var byteHelpers = require('@videojs/vhs-utils/cjs/byte-helpers'); 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 clock = require('mux.js/lib/utils/clock'); 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$1); var _resolveUrl__default = /*#__PURE__*/_interopDefaultLegacy(_resolveUrl); var videojs__default = /*#__PURE__*/_interopDefaultLegacy(videojs); var _extends__default = /*#__PURE__*/_interopDefaultLegacy(_extends); var parseSidx__default = /*#__PURE__*/_interopDefaultLegacy(parseSidx); /** * @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 () {}; }; /** * 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; }; var lastBufferedEnd = function lastBufferedEnd(a) { if (!a || !a.length || !a.end) { return; } return a.end(a.length - 1); }; /** * A utility function to add up the amount of time in a timeRange * after a specified startTime. * ie:[[0, 10], [20, 40], [50, 60]] with a startTime 0 * would return 40 as there are 40s seconds after 0 in the timeRange * * @param {TimeRange} range * The range to check against * @param {number} startTime * The time in the time range that you should start counting from * * @return {number} * The number of seconds in the buffer passed the specified time. */ var timeAheadOf = function timeAheadOf(range, startTime) { var time = 0; if (!range || !range.length) { return time; } for (var i = 0; i < range.length; i++) { var start = range.start(i); var end = range.end(i); // startTime is after this range entirely if (startTime > end) { continue; } // startTime is within this range if (startTime > start && startTime <= end) { time += end - startTime; continue; } // startTime is before this range. time += end - start; } return time; }; /** * @file playlist.js * * Playlist related utilities. */ var createTimeRange = videojs__default["default"].createTimeRange; /** * Get the duration of a segment, with special cases for * llhls segments that do not have a duration yet. * * @param {Object} playlist * the playlist that the segment belongs to. * @param {Object} segment * the segment to get a duration for. * * @return {number} * the segment duration */ var segmentDurationWithParts = function segmentDurationWithParts(playlist, segment) { // if this isn't a preload segment // then we will have a segment duration that is accurate. if (!segment.preload) { return segment.duration; } // otherwise we have to add up parts and preload hints // to get an up to date duration. var result = 0; (segment.parts || []).forEach(function (p) { result += p.duration; }); // for preload hints we have to use partTargetDuration // as they won't even have a duration yet. (segment.preloadHints || []).forEach(function (p) { if (p.type === 'PART') { result += playlist.partTargetDuration; } }); return result; }; /** * A function to get a combined list of parts and segments with durations * and indexes. * * @param {Playlist} playlist the playlist to get the list for. * * @return {Array} The part/segment list. */ var getPartsAndSegments = function getPartsAndSegments(playlist) { return (playlist.segments || []).reduce(function (acc, segment, si) { if (segment.parts) { segment.parts.forEach(function (part, pi) { acc.push({ duration: part.duration, segmentIndex: si, partIndex: pi, part: part, segment: segment }); }); } else { acc.push({ duration: segment.duration, segmentIndex: si, partIndex: null, segment: segment, part: null }); } return acc; }, []); }; var getLastParts = function getLastParts(media) { var lastSegment = media.segments && media.segments.length && media.segments[media.segments.length - 1]; return lastSegment && lastSegment.parts || []; }; var getKnownPartCount = function getKnownPartCount(_ref) { var preloadSegment = _ref.preloadSegment; if (!preloadSegment) { return; } var parts = preloadSegment.parts, preloadHints = preloadSegment.preloadHints; var partCount = (preloadHints || []).reduce(function (count, hint) { return count + (hint.type === 'PART' ? 1 : 0); }, 0); partCount += parts && parts.length ? parts.length : 0; return partCount; }; /** * Get the number of seconds to delay from the end of a * live playlist. * * @param {Playlist} master the master playlist * @param {Playlist} media the media playlist * @return {number} the hold back in seconds. */ var liveEdgeDelay = function liveEdgeDelay(master, media) { if (media.endList) { return 0; } // dash suggestedPresentationDelay trumps everything if (master && master.suggestedPresentationDelay) { return master.suggestedPresentationDelay; } var hasParts = getLastParts(media).length > 0; // look for "part" delays from ll-hls first if (hasParts && media.serverControl && media.serverControl.partHoldBack) { return media.serverControl.partHoldBack; } else if (hasParts && media.partTargetDuration) { return media.partTargetDuration * 3; // finally look for full segment delays } else if (media.serverControl && media.serverControl.holdBack) { return media.serverControl.holdBack; } else if (media.targetDuration) { return media.targetDuration * 3; } return 0; }; /** * 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 += segmentDurationWithParts(playlist, segment); 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 += segmentDurationWithParts(playlist, segment); 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 {Array} options.durationList list to iterate over for durations. * @param {number} options.defaultDuration duration to use for elements before or after the durationList * @param {number} options.startIndex partsAndSegments index to start * @param {number} options.endIndex partsAndSegments index to end. * @return {number} the number of seconds between startIndex and endIndex */ var sumDurations = function sumDurations(_ref2) { var defaultDuration = _ref2.defaultDuration, durationList = _ref2.durationList, startIndex = _ref2.startIndex, endIndex = _ref2.endIndex; var durations = 0; if (startIndex > endIndex) { var _ref3 = [endIndex, startIndex]; startIndex = _ref3[0]; endIndex = _ref3[1]; } if (startIndex < 0) { for (var i = startIndex; i < Math.min(0, endIndex); i++) { durations += defaultDuration; } startIndex = 0; } for (var _i = startIndex; _i < endIndex; _i++) { durations += durationList[_i].duration; } return durations; }; /** * 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 lastSegmentEndTime = intervalDuration(playlist, playlist.mediaSequence + playlist.segments.length, expired); if (useSafeLiveEnd) { liveEdgePadding = typeof liveEdgePadding === 'number' ? liveEdgePadding : liveEdgeDelay(null, playlist); lastSegmentEndTime -= liveEdgePadding; } // don't return a time less than zero return Math.max(0, lastSegmentEndTime); }; /** * 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} options.playlist the media playlist to query * @param {number} options.currentTime The number of seconds since the earliest * possible position to determine the containing segment for * @param {number} options.startTime the time when the segment/part starts * @param {number} options.startingSegmentIndex the segment index to start looking at. * @param {number?} [options.startingPartIndex] the part index to look at within the segment. * * @return {Object} an object with partIndex, segmentIndex, and startTime. */ var getMediaInfoForTime = function getMediaInfoForTime(_ref4) { var playlist = _ref4.playlist, currentTime = _ref4.currentTime, startingSegmentIndex = _ref4.startingSegmentIndex, startingPartIndex = _ref4.startingPartIndex, startTime = _ref4.startTime, experimentalExactManifestTimings = _ref4.experimentalExactManifestTimings; var time = currentTime - startTime; var partsAndSegments = getPartsAndSegments(playlist); var startIndex = 0; for (var i = 0; i < partsAndSegments.length; i++) { var partAndSegment = partsAndSegments[i]; if (startingSegmentIndex !== partAndSegment.segmentIndex) { continue; } // skip this if part index does not match. if (typeof startingPartIndex === 'number' && typeof partAndSegment.partIndex === 'number' && startingPartIndex !== partAndSegment.partIndex) { continue; } startIndex = i; break; } 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 (var _i2 = startIndex - 1; _i2 >= 0; _i2--) { var _partAndSegment = partsAndSegments[_i2]; time += _partAndSegment.duration; if (experimentalExactManifestTimings) { if (time < 0) { continue; } } else if (time + TIME_FUDGE_FACTOR <= 0) { continue; } return { partIndex: _partAndSegment.partIndex, segmentIndex: _partAndSegment.segmentIndex, startTime: startTime - sumDurations({ defaultDuration: playlist.targetDuration, durationList: partsAndSegments, startIndex: startIndex, endIndex: _i2 }) }; } } // We were unable to find a good segment within the playlist // so select the first segment return { partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 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 (var _i3 = startIndex; _i3 < 0; _i3++) { time -= playlist.targetDuration; if (time < 0) { return { partIndex: partsAndSegments[0] && partsAndSegments[0].partIndex || null, segmentIndex: partsAndSegments[0] && partsAndSegments[0].segmentIndex || 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 (var _i4 = startIndex; _i4 < partsAndSegments.length; _i4++) { var _partAndSegment2 = partsAndSegments[_i4]; time -= _partAndSegment2.duration; if (experimentalExactManifestTimings) { if (time > 0) { continue; } } else if (time - TIME_FUDGE_FACTOR >= 0) { continue; } return { partIndex: _partAndSegment2.partIndex, segmentIndex: _partAndSegment2.segmentIndex, startTime: startTime + sumDurations({ defaultDuration: playlist.targetDuration, durationList: partsAndSegments, startIndex: startIndex, endIndex: _i4 }) }; } // We are out of possible candidates so load the last one... return { segmentIndex: partsAndSegments[partsAndSegments.length - 1].segmentIndex, partIndex: partsAndSegments[partsAndSegments.length - 1].partIndex, 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; }; /** * 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, bytesReceived) { if (bytesReceived === void 0) { bytesReceived = 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; }; var playlistMatch = function playlistMatch(a, b) { // both playlits are null // or only one playlist is non-null // no match if (!a && !b || !a && b || a && !b) { return false; } // playlist objects are the same, match if (a === b) { return true; } // first try to use id as it should be the most // accurate if (a.id && b.id && a.id === b.id) { return true; } // next try to use reslovedUri as it should be the // second most accurate. if (a.resolvedUri && b.resolvedUri && a.resolvedUri === b.resolvedUri) { return true; } // finally try to use uri as it should be accurate // but might miss a few cases for relative uris if (a.uri && b.uri && a.uri === b.uri) { return true; } return false; }; var someAudioVariant = function someAudioVariant(master, callback) { var AUDIO = master && master.mediaGroups && master.mediaGroups.AUDIO || {}; var found = false; for (var groupName in AUDIO) { for (var label in AUDIO[groupName]) { found = callback(AUDIO[groupName][label]); if (found) { break; } } if (found) { break; } } return !!found; }; var isAudioOnly = function isAudioOnly(master) { // we are audio only if we have no main playlists but do // have media group playlists. if (!master || !master.playlists || !master.playlists.length) { // without audio variants or playlists this // is not an audio only master. var found = someAudioVariant(master, function (variant) { return variant.playlists && variant.playlists.length || variant.uri; }); return found; } // if every playlist has only an audio codec it is audio only var _loop = function _loop(i) { var playlist = master.playlists[i]; var CODECS = playlist.attributes && playlist.attributes.CODECS; // all codecs are audio, this is an audio playlist. if (CODECS && CODECS.split(',').every(function (c) { return codecs_js.isAudioCodec(c); })) { return "continue"; } // playlist is in an audio group it is audio only var found = someAudioVariant(master, function (variant) { return playlistMatch(playlist, variant); }); if (found) { return "continue"; } // if we make it here this playlist isn't audio and we // are not audio only return { v: false }; }; for (var i = 0; i < master.playlists.length; i++) { var _ret = _loop(i); if (_ret === "continue") continue; if (typeof _ret === "object") return _ret.v; } // if we make it past every playlist without returning, then // this is an audio only playlist. return true; }; // exports var Playlist = { liveEdgeDelay: liveEdgeDelay, duration: duration, seekable: seekable, getMediaInfoForTime: getMediaInfoForTime, isEnabled: isEnabled, isDisabled: isDisabled, isBlacklisted: isBlacklisted, isIncompatible: isIncompatible, playlistEnd: playlistEnd, isAes: isAes, hasAttribute: hasAttribute, estimateSegmentRequestTime: estimateSegmentRequestTime, isLowestEnabledRendition: isLowestEnabledRendition, isAudioOnly: isAudioOnly, playlistMatch: playlistMatch, segmentDurationWithParts: segmentDurationWithParts }; var log = videojs__default["default"].log; var createPlaylistID = function createPlaylistID(index, uri) { return index + "-" + uri; }; /** * Parses a given m3u8 playlist * * @param {Function} [onwarn] * a function to call when the parser triggers a warning event. * @param {Function} [oninfo] * a function to call when the parser triggers an info event. * @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 * @param {boolean} [experimentalLLHLS=false] * Whether to keep ll-hls features in the manifest after parsing. * @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, experimentalLLHLS = _ref.experimentalLLHLS; 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(); var manifest = parser.manifest; // remove llhls features from the parsed manifest // if we don't want llhls support. if (!experimentalLLHLS) { ['preloadSegment', 'skip', 'serverControl', 'renditionReports', 'partInf', 'partTargetDuration'].forEach(function (k) { if (manifest.hasOwnProperty(k)) { delete manifest[k]; } }); if (manifest.segments) { manifest.segments.forEach(function (segment) { ['parts', 'preloadHints'].forEach(function (k) { if (segment.hasOwnProperty(k)) { delete segment[k]; } }); }); } } if (!manifest.targetDuration) { var targetDuration = 10; if (manifest.segments && manifest.segments.length) { targetDuration = manifest.segments.reduce(function (acc, s) { return Math.max(acc, s.duration); }, 0); } if (onwarn) { onwarn("manifest has no targetDuration defaulting to " + targetDuration); } manifest.targetDuration = targetDuration; } var parts = getLastParts(manifest); if (parts.length && !manifest.partTargetDuration) { var partTargetDuration = parts.reduce(function (acc, p) { return Math.max(acc, p.duration); }, 0); if (onwarn) { onwarn("manifest has no partTargetDuration defaulting to " + partTargetDuration); log.error('LL-HLS manifest has parts but lacks required #EXT-X-PART-INF:PART-TARGET value. See https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis-09#section-4.4.3.7. Playback is not guaranteed.'); } manifest.partTargetDuration = partTargetDuration; } return 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) { if (!master.mediaGroups) { return; } ['AUDIO', 'SUBTITLES'].forEach(function (mediaType) { if (!master.mediaGroups[mediaType]) { return; } 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; playlist.playlistErrors_ = 0; 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; } } var audioOnlyMaster = isAudioOnly(master); forEachMediaGroup(master, function (properties, mediaType, groupKey, labelKey) { var groupId = "placeholder-uri-" + mediaType + "-" + groupKey + "-" + labelKey; // add a playlist array under properties if (!properties.playlists || !properties.playlists.length) { // If the manifest is audio only and this media group does not have a uri, check // if the media group is located in the main list of playlists. If it is, don't add // placeholder properties as it shouldn't be considered an alternate audio track. if (audioOnlyMaster && mediaType === 'AUDIO' && !properties.uri) { for (var _i = 0; _i < master.playlists.length; _i++) { var p = master.playlists[_i]; if (p.attributes && p.attributes.AUDIO && p.attributes.AUDIO === groupKey) { return; } } } properties.playlists = [_extends__default["default"]({}, properties)]; } properties.playlists.forEach(function (p, i) { var id = createPlaylistID(i, groupId); if (p.uri) { p.resolvedUri = p.resolvedUri || resolveUrl(master.uri, p.uri); } else { // DEPRECATED, this has been added to prevent a breaking change. // previously we only ever had a single media group playlist, so // we mark the first playlist uri without prepending the index as we used to // ideally we would do all of the playlists the same way. p.uri = i === 0 ? groupId : id; // don't resolve a placeholder uri to an absolute url, just use // the placeholder again p.resolvedUri = p.uri; } p.id = p.id || id; // add an empty attributes object, all playlists are // expected to have this. p.attributes = p.attributes || {}; // setup ID and URI references (URI for backwards compatibility) master.playlists[p.id] = p; master.playlists[p.uri] = p; }); }); setupMediaPlaylists(master); resolveMediaGroupUris(master); }; var mergeOptions$2 = videojs__default["default"].mergeOptions, EventTarget$1 = videojs__default["default"].EventTarget; var addLLHLSQueryDirectives = function addLLHLSQueryDirectives(uri, media) { if (media.endList || !media.serverControl) { return uri; } var parameters = {}; if (media.serverControl.canBlockReload) { var preloadSegment = media.preloadSegment; // next msn is a zero based value, length is not. var nextMSN = media.mediaSequence + media.segments.length; // If preload segment has parts then it is likely // that we are going to request a part of that preload segment. // the logic below is used to determine that. if (preloadSegment) { var parts = preloadSegment.parts || []; // _HLS_part is a zero based index var nextPart = getKnownPartCount(media) - 1; // if nextPart is > -1 and not equal to just the // length of parts, then we know we had part preload hints // and we need to add the _HLS_part= query if (nextPart > -1 && nextPart !== parts.length - 1) { // add existing parts to our preload hints // eslint-disable-next-line parameters._HLS_part = nextPart; } // this if statement makes sure that we request the msn // of the preload segment if: // 1. the preload segment had parts (and was not yet a full segment) // but was added to our segments array // 2. the preload segment had preload hints for parts that are not in // the manifest yet. // in all other cases we want the segment after the preload segment // which will be given by using media.segments.length because it is 1 based // rather than 0 based. if (nextPart > -1 || parts.length) { nextMSN--; } } // add _HLS_msn= in front of any _HLS_part query // eslint-disable-next-line parameters._HLS_msn = nextMSN; } if (media.serverControl && media.serverControl.canSkipUntil) { // add _HLS_skip= infront of all other queries. // eslint-disable-next-line parameters._HLS_skip = media.serverControl.canSkipDateranges ? 'v2' : 'YES'; } if (Object.keys(parameters).length) { var parsedUri = new window__default["default"].URL(uri); ['_HLS_skip', '_HLS_msn', '_HLS_part'].forEach(function (name) { if (!parameters.hasOwnProperty(name)) { return; } parsedUri.searchParams.set(name, parameters[name]); }); uri = parsedUri.toString(); } return uri; }; /** * Returns a new segment object with properties and * the parts array merged. * * @param {Object} a the old segment * @param {Object} b the new segment * * @return {Object} the merged segment */ var updateSegment = function updateSegment(a, b) { if (!a) { return b; } var result = mergeOptions$2(a, b); // if only the old segment has preload hints // and the new one does not, remove preload hints. if (a.preloadHints && !b.preloadHints) { delete result.preloadHints; } // if only the old segment has parts // then the parts are no longer valid if (a.parts && !b.parts) { delete result.parts; // if both segments have parts // copy part propeties from the old segment // to the new one. } else if (a.parts && b.parts) { for (var i = 0; i < b.parts.length; i++) { if (a.parts && a.parts[i]) { result.parts[i] = mergeOptions$2(a.parts[i], b.parts[i]); } } } // set skipped to false for segments that have // have had information merged from the old segment. if (!a.skipped && b.skipped) { result.skipped = false; } // set preload to false for segments that have // had information added in the new segment.