# Source: streaming/models/FragmentModel.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.
     */

    import FactoryMaker from '../../core/FactoryMaker';
    import FragmentRequest from '../vo/FragmentRequest';

    const FRAGMENT_MODEL_LOADING = 'loading';
    const FRAGMENT_MODEL_EXECUTED = 'executed';
    const FRAGMENT_MODEL_CANCELED = 'canceled';
    const FRAGMENT_MODEL_FAILED = 'failed';

    function FragmentModel(config) {

        config = config || {};
        const eventBus = config.eventBus;
        const events = config.events;
        const dashMetrics = config.dashMetrics;
        const fragmentLoader = config.fragmentLoader;
        const debug = config.debug;
        const streamInfo = config.streamInfo;
        const type = config.type;

        let instance,
            logger,
            executedRequests,
            loadingRequests;

        function setup() {
            logger = debug.getLogger(instance);
            resetInitialSettings();
            eventBus.on(events.LOADING_COMPLETED, onLoadingCompleted, instance);
            eventBus.on(events.LOADING_DATA_PROGRESS, onLoadingInProgress, instance);
            eventBus.on(events.LOADING_ABANDONED, onLoadingAborted, instance);
        }

        function getStreamId() {
            return streamInfo.id;
        }

        function getType() {
            return type;
        }

        function isFragmentLoaded(request) {
            const isEqualComplete = function (req1, req2) {
                return ((req1.action === FragmentRequest.ACTION_COMPLETE) && (req1.action === req2.action));
            };

            const isEqualMedia = function (req1, req2) {
                return !isNaN(req1.index) && (req1.startTime === req2.startTime) && (req1.adaptationIndex === req2.adaptationIndex) && (req1.type === req2.type);
            };

            const isEqualInit = function (req1, req2) {
                return isNaN(req1.index) && isNaN(req2.index) && (req1.quality === req2.quality);
            };

            const check = function (requests) {
                let isLoaded = false;

                requests.some(req => {
                    if (isEqualMedia(request, req) || isEqualInit(request, req) || isEqualComplete(request, req)) {
                        isLoaded = true;
                        return isLoaded;
                    }
                });
                return isLoaded;
            };

            if (!request) {
                return false;
            }

            return check(executedRequests);
        }

        function isFragmentLoadedOrPending(request) {
            let isLoaded = false;
            let i = 0;
            let req;

            // First, check if the fragment has already been loaded
            isLoaded = isFragmentLoaded(request);

            // Then, check if the fragment is about to be loeaded
            if (!isLoaded) {
                for (i = 0; i < loadingRequests.length; i++) {
                    req = loadingRequests[i];
                    if ((request.url === req.url) && (request.startTime === req.startTime)) {
                        isLoaded = true;
                    }
                }
            }

            return isLoaded;
        }

        /**
         *
         * Gets an array of {@link FragmentRequest} objects
         *
         * @param {Object} filter The object with properties by which the method filters the requests to be returned.
         *  the only mandatory property is state, which must be a value from
         *  other properties should match the properties of {@link FragmentRequest}. E.g.:
         *  getRequests({state: FragmentModel.FRAGMENT_MODEL_EXECUTED, quality: 0}) - returns
         *  all the requests from executedRequests array where requests.quality = filter.quality
         *
         * @returns {Array}
         * @memberof FragmentModel#
         */
        function getRequests(filter) {
            const states = filter ? filter.state instanceof Array ? filter.state : [filter.state] : [];

            let filteredRequests = [];
            states.forEach(state => {
                const requests = getRequestsForState(state);
                filteredRequests = filteredRequests.concat(filterRequests(requests, filter));
            });

            return filteredRequests;
        }

        function getRequestThreshold(req) {
            return isNaN(req.duration) ? 0.25 : Math.min(req.duration / 8, 0.5);
        }

        function removeExecutedRequestsBeforeTime(time) {
            executedRequests = executedRequests.filter(req => {
                const threshold = getRequestThreshold(req);
                return isNaN(req.startTime) || (time !== undefined ? req.startTime >= time - threshold : false);
            });
        }

        function removeExecutedRequestsAfterTime(time) {
            executedRequests = executedRequests.filter(req => {
                return isNaN(req.startTime) || (time !== undefined ? req.startTime < time : false);
            });
        }

        function removeExecutedRequestsInTimeRange(start, end) {
            if (end <= start + 0.5) {
                return;
            }

            executedRequests = executedRequests.filter(req => {
                const threshold = getRequestThreshold(req);
                return (isNaN(req.startTime) || req.startTime >= (end - threshold)) ||
                    (isNaN(req.duration) || (req.startTime + req.duration) <= (start + threshold));
            });
        }

        // Remove requests that are not "represented" by any of buffered ranges
        function syncExecutedRequestsWithBufferedRange(bufferedRanges, streamDuration) {
            if (!bufferedRanges || bufferedRanges.length === 0) {
                removeExecutedRequestsBeforeTime();
                return;
            }

            let start = 0;
            for (let i = 0, ln = bufferedRanges.length; i < ln; i++) {
                removeExecutedRequestsInTimeRange(start, bufferedRanges.start(i));
                start = bufferedRanges.end(i);
            }
            if (streamDuration > 0) {
                removeExecutedRequestsInTimeRange(start, streamDuration);
            }
        }

        function abortRequests() {
            logger.debug('abort requests');
            fragmentLoader.abort();
            loadingRequests = [];
        }

        function executeRequest(request) {
            switch (request.action) {
                case FragmentRequest.ACTION_DOWNLOAD:
                    addSchedulingInfoMetrics(request, FRAGMENT_MODEL_LOADING);
                    loadingRequests.push(request);
                    loadCurrentFragment(request);
                    break;
                default:
                    logger.warn('Unknown request action.');
            }
        }

        function loadCurrentFragment(request) {
            eventBus.trigger(events.FRAGMENT_LOADING_STARTED,
                { request: request },
                { streamId: streamInfo.id, mediaType: type }
            );
            fragmentLoader.load(request);
        }

        function getRequestForTime(arr, time, threshold) {
            // loop through the executed requests and pick the one for which the playback interval matches the given time
            const lastIdx = arr.length - 1;
            for (let i = lastIdx; i >= 0; i--) {
                const req = arr[i];
                const start = req.startTime;
                const end = start + req.duration;
                threshold = !isNaN(threshold) ? threshold : getRequestThreshold(req);
                if ((!isNaN(start) && !isNaN(end) && ((time + threshold) >= start) && ((time - threshold) < end)) || (isNaN(start) && isNaN(time))) {
                    return req;
                }
            }
            return null;
        }

        function filterRequests(arr, filter) {
            // for time use a specific filtration function
            if (filter.hasOwnProperty('time')) {
                return [getRequestForTime(arr, filter.time, filter.threshold)];
            }

            return arr.filter(request => {
                for (const prop in filter) {
                    if (prop === 'state') continue;
                    if (filter.hasOwnProperty(prop) && request[prop] != filter[prop]) return false;
                }

                return true;
            });
        }

        function getRequestsForState(state) {
            let requests;
            switch (state) {
                case FRAGMENT_MODEL_LOADING:
                    requests = loadingRequests;
                    break;
                case FRAGMENT_MODEL_EXECUTED:
                    requests = executedRequests;
                    break;
                default:
                    requests = [];
            }
            return requests;
        }

        function addSchedulingInfoMetrics(request, state) {
            dashMetrics.addSchedulingInfo(request, state);
            dashMetrics.addRequestsQueue(request.mediaType, loadingRequests, executedRequests);
        }

        function onLoadingCompleted(e) {
            if (e.sender !== fragmentLoader) return;

            loadingRequests.splice(loadingRequests.indexOf(e.request), 1);

            if (e.response && !e.error) {
                executedRequests.push(e.request);
            }

            addSchedulingInfoMetrics(e.request, e.error ? FRAGMENT_MODEL_FAILED : FRAGMENT_MODEL_EXECUTED);

            eventBus.trigger(events.FRAGMENT_LOADING_COMPLETED,
                {
                    request: e.request,
                    response: e.response,
                    error: e.error,
                    sender: this
                },
                { streamId: streamInfo.id, mediaType: type }
            );
        }

        function onLoadingInProgress(e) {
            if (e.sender !== fragmentLoader) return;

            eventBus.trigger(events.FRAGMENT_LOADING_PROGRESS,
                {
                    request: e.request,
                    response: e.response,
                    error: e.error,
                    sender: this
                },
                { streamId: streamInfo.id, mediaType: type }
            );
        }

        function onLoadingAborted(e) {
            if (e.sender !== fragmentLoader) return;

            eventBus.trigger(events.FRAGMENT_LOADING_ABANDONED,
                { request: e.request },
                { streamId: streamInfo.id, mediaType: type }
            );
        }

        function resetInitialSettings() {
            executedRequests = [];
            loadingRequests = [];
        }

        function reset() {
            eventBus.off(events.LOADING_COMPLETED, onLoadingCompleted, this);
            eventBus.off(events.LOADING_DATA_PROGRESS, onLoadingInProgress, this);
            eventBus.off(events.LOADING_ABANDONED, onLoadingAborted, this);

            if (fragmentLoader) {
                fragmentLoader.reset();
            }
            resetInitialSettings();
        }

        function addExecutedRequest(request) {
            executedRequests.push(request);
        }

        instance = {
            getStreamId,
            getType,
            getRequests,
            isFragmentLoaded,
            isFragmentLoadedOrPending,
            removeExecutedRequestsBeforeTime,
            removeExecutedRequestsAfterTime,
            syncExecutedRequestsWithBufferedRange,
            abortRequests,
            executeRequest,
            reset,
            resetInitialSettings,
            addExecutedRequest
        };

        setup();
        return instance;
    }

    FragmentModel.__dashjs_factory_name = 'FragmentModel';
    const factory = FactoryMaker.getClassFactory(FragmentModel);
    factory.FRAGMENT_MODEL_LOADING = FRAGMENT_MODEL_LOADING;
    factory.FRAGMENT_MODEL_EXECUTED = FRAGMENT_MODEL_EXECUTED;
    factory.FRAGMENT_MODEL_CANCELED = FRAGMENT_MODEL_CANCELED;
    factory.FRAGMENT_MODEL_FAILED = FRAGMENT_MODEL_FAILED;
    FactoryMaker.updateClassFactory(FragmentModel.__dashjs_factory_name, factory);
    export default factory;

#### Search results