streaming_utils_BufferRangeUtils.js

/**
 * The copyright in this software is being made available under the BSD License,
 * included below. This software may be subject to other third party and contributor
 * rights, including patent rights, and no such rights are granted under this license.
 *
 * Copyright (c) 2013, Dash Industry Forum.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *  * Redistributions of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation and/or
 *  other materials provided with the distribution.
 *  * Neither the name of Dash Industry Forum nor the names of its
 *  contributors may be used to endorse or promote products derived from this software
 *  without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS AS IS AND ANY
 *  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 *  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 *  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 *  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

const BUFFER_END_THRESHOLD = 0.5;
const DEFAULT_RANGE_TOLERANCE = 0.15;

function isValidTargetTime(time) {
    return (time || time === 0) && !isNaN(time);
}

/**
 * Returns the end of the buffered range that contains targetTime, i.e. the time until which the buffer is continuous.
 * Returns NaN if targetTime is not buffered.
 */
function getContinuousBufferTime(ranges, targetTime) {
    if (!ranges || ranges.length === 0) {
        return NaN;
    }

    let adjustedTime = targetTime;
    let i = 0;

    while (adjustedTime === targetTime && i < ranges.length) {
        const start = ranges.start(i);
        const end = ranges.end(i);

        if (adjustedTime >= start && adjustedTime <= end) {
            adjustedTime = end;
        }

        i += 1;
    }

    return adjustedTime === targetTime ? NaN : adjustedTime;
}

function getRangeBehindForPruning(ranges, targetTime, bufferToKeepBehind, currentTimeRequest) {
    const keepBehind = Number(bufferToKeepBehind);

    if (isNaN(keepBehind)) {
        return null;
    }

    const startOfBuffer = ranges.start(0);

    if (targetTime - startOfBuffer > keepBehind) {
        let rangeEnd = Math.max(0, targetTime - keepBehind);

        if (currentTimeRequest) {
            rangeEnd = Math.min(currentTimeRequest.startTime, rangeEnd);
        }

        if (rangeEnd > 0) {
            return {
                start: startOfBuffer,
                end: rangeEnd
            };
        }
    }

    return null;
}

function getRangeAheadForPruning(ranges, targetTime, options) {
    const endOfLastRange = ranges.end(ranges.length - 1);
    const endOfBuffer = endOfLastRange + BUFFER_END_THRESHOLD;
    const {
        bufferToKeepAhead,
        currentTimeRequest,
        avoidCurrentTimeRangePruning,
        logger
    } = options;

    const continuousBufferTime = getContinuousBufferTime(ranges, targetTime);
    let rangeStart;

    if (!isNaN(continuousBufferTime)) {
        const keepAhead = Number(bufferToKeepAhead);

        if (isNaN(keepAhead)) {
            return null;
        }

        rangeStart = Math.min(continuousBufferTime, targetTime + keepAhead);
    } else {
        rangeStart = targetTime;
    }

    if (rangeStart >= endOfLastRange) {
        return null;
    }

    if (currentTimeRequest) {
        rangeStart = Math.max(currentTimeRequest.startTime + currentTimeRequest.duration, rangeStart);
    }

    if (avoidCurrentTimeRangePruning) {
        for (let i = 0; i < ranges.length; i++) {
            if (ranges.start(i) <= targetTime && targetTime <= ranges.end(i)
                && ranges.start(i) <= rangeStart && rangeStart <= ranges.end(i)) {
                const oldRangeStart = rangeStart;
                rangeStart = i + 1 < ranges.length ? ranges.start(i + 1) : ranges.end(i) + 1;
                if (logger) {
                    logger.debug('Buffered range [' + ranges.start(i) + ', ' + ranges.end(i) + '] overlaps with targetTime ' + targetTime + ' and range to be pruned [' + oldRangeStart + ', ' + endOfBuffer + '], using [' + rangeStart + ', ' + endOfBuffer + '] instead' + ((rangeStart < endOfBuffer) ? '' : ' (no actual pruning)'));
                }
                break;
            }
        }
    }

    if (rangeStart < endOfLastRange) {
        return {
            start: rangeStart,
            end: endOfBuffer
        };
    }

    return null;
}

function getPruningRanges(ranges, seekTime, options) {
    const clearRanges = [];

    if (!ranges || ranges.length === 0) {
        return clearRanges;
    }

    if (!isValidTargetTime(seekTime)) {
        clearRanges.push({
            start: ranges.start(0),
            end: ranges.end(ranges.length - 1) + BUFFER_END_THRESHOLD
        });
        return clearRanges;
    }

    const pruningOptions = options || {};
    const behindPruningRange = getRangeBehindForPruning(
        ranges,
        seekTime,
        pruningOptions.bufferToKeepBehind,
        pruningOptions.currentTimeRequest
    );
    const aheadPruningRange = getRangeAheadForPruning(ranges, seekTime, pruningOptions);

    if (behindPruningRange) {
        clearRanges.push(behindPruningRange);
    }

    if (aheadPruningRange) {
        clearRanges.push(aheadPruningRange);
    }

    return clearRanges;
}

function hasBufferAtTime(ranges, time) {
    if (!ranges || ranges.length === 0) {
        return false;
    }

    for (let i = 0; i < ranges.length; i++) {
        if (time >= ranges.start(i) && time <= ranges.end(i)) {
            return true;
        }
    }

    return false;
}

function getRangeAt(ranges, time, tolerance) {
    let firstStart = null;
    let lastEnd = null;
    const actualTolerance = !isNaN(tolerance) ? tolerance : DEFAULT_RANGE_TOLERANCE;

    if (ranges !== null && ranges !== undefined) {
        for (let i = 0; i < ranges.length; i++) {
            const start = ranges.start(i);
            const end = ranges.end(i);

            if (firstStart === null) {
                const gap = Math.abs(start - time);
                if ((time >= start && time < end) || gap <= actualTolerance) {
                    firstStart = start;
                    lastEnd = end;
                }
            } else if (start - lastEnd <= actualTolerance) {
                lastEnd = end;
            } else {
                break;
            }
        }
    }

    return firstStart === null ? null : {
        start: firstStart,
        end: lastEnd
    };
}

export {
    getContinuousBufferTime,
    getPruningRanges,
    getRangeAt,
    hasBufferAtTime
};