UNPKG

rx-player

Version:
572 lines (532 loc) 17.3 kB
/** * Copyright 2015 CANAL+ Group * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /** * This file contains functions helping with TimeRanges management. * * For simplicity/performance reasons, many of those work with a simplified * "Range" object, which is an object with two keys: * - start {Number} * - end {Number} * * Those two corresponds to what is returned by the start and end methods of a * TimeRanges Object. * * You can convert from TimeRanges to Range object(s) with the getRange/ * convertToRanges methods. */ // Factor for rounding errors const EPSILON = 1 / 60; interface IRange { start: number; end: number; } /** * Check equality with a tolerance of EPSILON. * Used for various functions with this sort of tolerance regarding the * start/end of contiguous ranges. * @param {Number} a * @param {Number} b * @returns {Boolean} */ function nearlyEqual(a: number, b: number): boolean { return Math.abs(a - b) < EPSILON; } /** * Construct a new range which will have, as start/end, the min/max * of both the range given, and the given bitrate. * @param {Object} range1 * @param {Object} range2 * @returns {Object} */ function createRangeUnion(range1: IRange, range2: IRange): IRange { const start = Math.min(range1.start, range2.start); const end = Math.max(range1.end, range2.end); return { start, end }; } /** * Clean array ranges from "empty" ranges. * That is, range objects which have their start equal to their end. * /!\ Mutate the array of ranges. * @param {Array<Object>} ranges * @returns {Array<Object>} */ function removeEmptyRanges(ranges: IRange[]): IRange[] { for (let index = 0; index < ranges.length; index++) { const range = ranges[index]; if (range.start === range.end) { ranges.splice(index--, 1); } } return ranges; } /** * /!\ Mutate the array of ranges. * @param {Array<Object>} ranges * @returns {Array<Object>} */ function mergeContiguousRanges(ranges: IRange[]): IRange[] { for (let index = 1; index < ranges.length; index++) { const prevRange = ranges[index - 1]; const currRange = ranges[index]; if (areRangesNearlyContiguous(prevRange, currRange)) { const unionRange = createRangeUnion(prevRange, currRange); ranges.splice(--index, 2, unionRange); } } return ranges; } /** * True if range1 is considered _after_ range2. * @param {Object} range1 * @param {Object} range2 * @returns {Boolean} */ function isAfter(range1: IRange, range2: IRange): boolean { return range1.start >= range2.end; } /** * True if range1 is considered _before_ range2. * @param {Object} range1 * @param {Object} range2 * @returns {Boolean} */ function isBefore(range1: IRange, range2: IRange): boolean { return range1.end <= range2.start; } /** * Returns true if the time given can be considered as part of any of the given * ranges. * @param {Array.<Object>} ranges * @param {number} time * @returns {boolean} */ function isTimeInRanges(ranges: IRange[], time: number): boolean { for (let i = 0; i < ranges.length; i++) { if (isTimeInRange(ranges[i], time)) { return true; } } return false; } /** * Returns true if the time given can be considered as part of the given range. * @param {Object} range1 * @param {Number} Time * @returns {Boolean} */ function isTimeInRange({ start, end }: IRange, time: number): boolean { return start <= time && time < end; } /** * Returns true if the two ranges given are overlapping. * @param {Object} range1 * @param {Object} range2 * @returns {Boolean} */ function areRangesOverlapping(range1: IRange, range2: IRange): boolean { return ( isTimeInRange(range1, range2.start) || (range1.start < range2.end && range2.end < range1.end) || isTimeInRange(range2, range1.start) ); } /** * Returns true if the two ranges given can be considered contiguous. * @param {Object} range1 * @param {Object} range2 * @returns {Boolean} */ function areRangesNearlyContiguous(range1: IRange, range2: IRange): boolean { return nearlyEqual(range2.start, range1.end) || nearlyEqual(range2.end, range1.start); } /** * Convert from a TimeRanges object to an array of Ranges. * @param {TimeRanges} timeRanges * @returns {Array.<Object>} */ function convertToRanges(timeRanges: TimeRanges): IRange[] { const ranges: IRange[] = []; for (let i = 0; i < timeRanges.length; i++) { ranges.push({ start: timeRanges.start(i), end: timeRanges.end(i) }); } return ranges; } /** * Get range object of a specific time in a TimeRanges object. * @param {TimeRanges} timeRanges * @returns {Object} */ function getBufferedTimeRange(timeRanges: TimeRanges, time: number): IRange | null { for (let i = timeRanges.length - 1; i >= 0; i--) { const start = timeRanges.start(i); if (time >= start) { const end = timeRanges.end(i); if (time < end) { return { start, end }; } } } return null; } /** * Get range object of a specific time in a `IRange` object. * @param {Array.<Object>} ranges * @returns {Object} */ function getRange(ranges: IRange[], time: number): IRange | null { for (let i = ranges.length - 1; i >= 0; i--) { const start = ranges[i].start; if (time >= start) { const end = ranges[i].end; if (time < end) { return ranges[i]; } } } return null; } /** * Get gap from a specific time until the start of the next Range. * @param {TimeRanges} timeRanges * @param {Number} time * @returns {Number} */ function getNextBufferedTimeRangeGap(timeRanges: TimeRanges, time: number): number { const len = timeRanges.length; for (let i = 0; i < len; i++) { const start = timeRanges.start(i); if (time < start) { return start - time; } } return Infinity; } /** * @param {TimeRanges} timeRanges * @param {Number} time * @returns {Object} - Object with two properties: * - outerRanges {Array.<Object>}: every ranges which does not contain the * given time. * - innerRange {Object|null}: the range which contain the given time. */ function getInnerAndOuterRangesFromBufferedTimeRanges( timeRanges: TimeRanges, time: number, ): { innerRange: IRange | null; outerRanges: IRange[] } { let innerRange: IRange | null = null; const outerRanges: IRange[] = []; for (let i = 0; i < timeRanges.length; i++) { const start = timeRanges.start(i); const end = timeRanges.end(i); if (time < start || time >= end) { outerRanges.push({ start, end }); } else { innerRange = { start, end }; } } return { outerRanges, innerRange }; } /** * @param {Array.<Object>} ranges * @param {Number} time * @returns {Object} - Object with two properties: * - outerRanges {Array.<Object>}: every ranges which does not contain the * given time. * - innerRange {Object|null}: the range which contain the given time. */ function getInnerAndOuterRanges( ranges: IRange[], time: number, ): { innerRange: IRange | null; outerRanges: IRange[] } { let innerRange: IRange | null = null; const outerRanges: IRange[] = []; for (let i = 0; i < ranges.length; i++) { const start = ranges[i].start; const end = ranges[i].end; if (time < start || time >= end) { outerRanges.push({ start, end }); } else { innerRange = { start, end }; } } return { outerRanges, innerRange }; } /** * Get "size" (difference between end and start) of the TimeRange containing the * given time. 0 if the range is not found. * @param {TimeRanges} timeRanges * @param {Number} currentTime * @returns {Number} */ function getSizeOfBufferedTimeRange(timeRanges: TimeRanges, currentTime: number): number { const range = getBufferedTimeRange(timeRanges, currentTime); return range !== null ? range.end - range.start : 0; } /** * Get "currently played" (difference between time given and start) of the * range containing the given time. 0 if the range is not found. * @param {TimeRanges} timeRanges * @param {Number} currentTime * @returns {Number} */ function getPlayedSizeOfBufferedTimeRange( timeRanges: TimeRanges, currentTime: number, ): number { const range = getBufferedTimeRange(timeRanges, currentTime); return range !== null ? currentTime - range.start : 0; } /** * Get "left to play" (difference between end and time given) of the range * containing the given time. Infinity if the range is not found. * @param {TimeRanges} timeRanges * @param {Number} currentTime * @returns {Number} */ function getLeftSizeOfBufferedTimeRange( timeRanges: TimeRanges, currentTime: number, ): number { const range = getBufferedTimeRange(timeRanges, currentTime); return range !== null ? range.end - currentTime : Infinity; } /** * Get "size" (difference between end and start) of the range containing the * given time. 0 if the range is not found. * @param {Array.<Object>} ranges * @param {Number} currentTime * @returns {Number} */ function getSizeOfRange(ranges: IRange[], currentTime: number): number { const range = getRange(ranges, currentTime); return range !== null ? range.end - range.start : 0; } /** * Get "currently played" (difference between time given and start) of the * range containing the given time. 0 if the range is not found. * @param {Array.<Object>} ranges * @param {Number} currentTime * @returns {Number} */ function getPlayedSizeOfRange(ranges: IRange[], currentTime: number): number { const range = getRange(ranges, currentTime); return range !== null ? currentTime - range.start : 0; } /** * Get "left to play" (difference between end and time given) of the range * containing the given time. Infinity if the range is not found. * @param {Array.<Object>} ranges * @param {Number} currentTime * @returns {Number} */ function getLeftSizeOfRange(ranges: IRange[], currentTime: number): number { const range = getRange(ranges, currentTime); return range !== null ? range.end - currentTime : Infinity; } /** * Insert a range object into an array of ranges objects, at the right place. * /!\ Mutate the array of ranges. * @param {Array.<Object>} ranges * @param {Object} rangeToAddArg * @returns {Array.<Object>} */ function insertInto(ranges: IRange[], rangeToAddArg: IRange): IRange[] { if (rangeToAddArg.start === rangeToAddArg.end) { return ranges; } let rangeToAdd: IRange = rangeToAddArg; // For each present range check if we need to: // - In case we are overlapping or contiguous: // - if added range has the same bitrate as the overlapped or // contiguous one, we can merge themcurrentRange // - if added range has a different bitrate we need to insert it // in place // - Need to insert in place, we we are completely, not overlapping // and not contiguous in between two ranges. let index = 0; for (; index < ranges.length; index++) { const range = ranges[index]; const overlapping = areRangesOverlapping(rangeToAdd, range); const contiguous = areRangesNearlyContiguous(rangeToAdd, range); // We assume ranges are ordered and two ranges can not be // completely overlapping. if (overlapping || contiguous) { rangeToAdd = createRangeUnion(rangeToAdd, range); ranges.splice(index--, 1); } else { // Check the case for which there is no more to do if (index === 0) { if (isBefore(rangeToAdd, ranges[0])) { // First index, and we are completely before that range (and // not contiguous, nor overlapping). We just need to be // inserted here. break; } } else { if (isBefore(ranges[index - 1], rangeToAdd) && isBefore(rangeToAdd, range)) { // We are exactly after the current previous range, and // before the current range, while not overlapping with none // of them. Insert here. break; } } } } // Now that we are sure we don't overlap with any range, just add it. ranges.splice(index, 0, rangeToAdd); return mergeContiguousRanges(removeEmptyRanges(ranges)); } /** * Returns range, from a range objects array overlapping with a range given * in argument. null if none is found. * @param {Object} range * @param {Array.<Object>} ranges * @returns {Array.<Object>} */ function findOverlappingRanges(range: IRange, ranges: IRange[]): IRange[] { const resultingRanges: IRange[] = []; for (let i = 0; i < ranges.length; i++) { if (areRangesOverlapping(range, ranges[i])) { resultingRanges.push(ranges[i]); } } return resultingRanges; } /** * Returns only the intersection between the two ranges, from the first * ranges argument given. * @param {Array.<Range>} ranges1 * @param {Array.<Range>} ranges2 * @returns {Array.<Range>} */ function keepRangeIntersection(ranges1: IRange[], ranges2: IRange[]): IRange[] { const result: IRange[] = []; for (let i = 0; i < ranges1.length; i++) { const range = ranges1[i]; const overlappingRanges = findOverlappingRanges(range, ranges2); if (overlappingRanges.length > 0) { for (let j = 0; j < overlappingRanges.length; j++) { const overlappingRange = overlappingRanges[j]; result.push({ start: Math.max(range.start, overlappingRange.start), end: Math.min(range.end, overlappingRange.end), }); } } } return result; } /** * Exclude from the `baseRanges` everything that is in `rangesToExclude`. * Example: * * Let's say we have the following base ranges: * |==========| |===============| |======| |==========| * * From which we want to "exclude" the following ranges: * |=========| |==| |===| |=====| * * We will obtain the first ranges from which we remove the second ranges: * ----------------------------------------------------------------------- * |==========| |===============| |======| |==========| * |=========| |==| |===| |=====| * _______________________________________________________________________ * | * | * V * ----------------------------------------------------------------------- * |==| |======| |==| |====| |==========| * ----------------------------------------------------------------------- * * @param {Array.<Object} baseRanges * @param {Array.<Object} rangesToExclude * @return {Array.<Object>} */ function excludeFromRanges(baseRanges: IRange[], rangesToExclude: IRange[]): IRange[] { const result: IRange[] = []; // For every range in `baseRanges`, find overlapping ranges with // `rangesToExclude` and remove them. for (let i = 0; i < baseRanges.length; i++) { const range = baseRanges[i]; const intersections: IRange[] = []; const overlappingRanges = findOverlappingRanges(range, rangesToExclude); if (overlappingRanges.length > 0) { for (let j = 0; j < overlappingRanges.length; j++) { const overlappingRange = overlappingRanges[j]; intersections.push({ start: Math.max(range.start, overlappingRange.start), end: Math.min(range.end, overlappingRange.end), }); } } if (intersections.length === 0) { result.push(range); } else { let lastStart: number = range.start; for (let j = 0; j < intersections.length; j++) { if (intersections[j].start > lastStart) { result.push({ start: lastStart, end: intersections[j].start }); } lastStart = intersections[j].end; } if (lastStart < range.end) { result.push({ start: lastStart, end: range.end }); } } } return result; } /** * Returns `true` if the given `time` is available in the TimeRanges object * given. * Returns `false` otherwise. * @param {TimeRanges} ranges * @param {Number} time * @returns {boolean} */ function isTimeInTimeRanges(ranges: TimeRanges, time: number): boolean { for (let i = 0; i < ranges.length; i++) { if (ranges.start(i) <= time && time < ranges.end(i)) { return true; } } return false; } export type { IRange }; export { convertToRanges, excludeFromRanges, getInnerAndOuterRangesFromBufferedTimeRanges, getInnerAndOuterRanges, getLeftSizeOfRange, getNextBufferedTimeRangeGap, getSizeOfBufferedTimeRange, getPlayedSizeOfBufferedTimeRange, getLeftSizeOfBufferedTimeRange, getPlayedSizeOfRange, getBufferedTimeRange, getRange, getSizeOfRange, insertInto, isAfter, isBefore, isTimeInTimeRanges, isTimeInRange, isTimeInRanges, keepRangeIntersection, mergeContiguousRanges, removeEmptyRanges, };