@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
JavaScript
/*! @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.