dash_DashMetrics.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 Constants from '../streaming/constants/Constants';
import { HTTPRequest } from '../streaming/vo/metrics/HTTPRequest';
import FactoryMaker from '../core/FactoryMaker';
import MetricsConstants from '../streaming/constants/MetricsConstants';
import Round10 from './utils/Round10';
import MetricsModel from '../streaming/models/MetricsModel';
import Utils from '../core/Utils';
import {
    PlayList,
    PlayListTrace
} from '../streaming/vo/metrics/PlayList';

/**
 * @module DashMetrics
 * @description The DashMetrics module can be accessed using the MediaPlayer API getDashMetrics()
 * @param {object} config
 */

function DashMetrics(config) {

    config = config || {};

    const context = this.context;
    let instance,
        playListTraceMetricsClosed,
        playListTraceMetrics,
        playListMetrics;

    let metricsModel = config.metricsModel;

    function setup() {
        metricsModel = metricsModel || MetricsModel(context).getInstance({settings: config.settings});
        resetInitialSettings();
    }

    function resetInitialSettings() {
        playListTraceMetricsClosed = true;
        playListTraceMetrics = null;
        playListMetrics = null;
    }

    /**
     * Returns the latest Representation switch for a given media type
     * @param {MediaType} mediaType
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getCurrentRepresentationSwitch(mediaType) {
        const metrics = metricsModel.getMetricsFor(mediaType, true);
        return getCurrent(metrics, MetricsConstants.TRACK_SWITCH);
    }

    /**
     * @param {MediaType} mediaType
     * @param {Date} t time of the switch event
     * @param {Date} mt media presentation time
     * @param {string} to id of representation
     * @param {string} lto if present, subrepresentation reference
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addRepresentationSwitch(mediaType, t, mt, to, lto) {
        metricsModel.addRepresentationSwitch(mediaType, t, mt, to, lto);
    }

    /**
     * Returns the current buffer state for a given media type
     * @param {MediaType} mediaType
     * @returns {number}
     * @memberof module:DashMetrics
     * @instance
     */
    function getCurrentBufferState(mediaType) {
        const metrics = metricsModel.getMetricsFor(mediaType, true);
        return getCurrent(metrics, MetricsConstants.BUFFER_STATE);
    }

    /**
     * Returns the current buffer level for a given media type
     * @param {MediaType} mediaType
     * @returns {number}
     * @memberof module:DashMetrics
     * @instance
     */
    function getCurrentBufferLevel(mediaType) {
        const metrics = metricsModel.getMetricsFor(mediaType, true);
        const metric = getCurrent(metrics, MetricsConstants.BUFFER_LEVEL);

        if (metric) {
            return Round10.round10(metric.level / 1000, -3);
        }

        return 0;
    }

    /**
     * @param {MediaType} mediaType
     * @param {number} t
     * @param {number} level
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addBufferLevel(mediaType, t, level) {
        metricsModel.addBufferLevel(mediaType, t, level);
    }

    /**
     * @param {MediaType} mediaType
     * @param {string} state
     * @param {number} target
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addBufferState(mediaType, state, target) {
        metricsModel.addBufferState(mediaType, state, target);
    }

    /**
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function clearAllCurrentMetrics () {
        metricsModel.clearAllCurrentMetrics();
    }

    /**
     * Returns the latest HTTP request for a given media type
     * @param {MediaType} mediaType
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getCurrentHttpRequest(mediaType) {
        const metrics = metricsModel.getMetricsFor(mediaType, true);

        if (!metrics) {
            return null;
        }

        const httpList = metrics.HttpList;
        let currentHttpList = null;

        let httpListLastIndex;

        if (!httpList || httpList.length <= 0) {
            return null;
        }

        httpListLastIndex = httpList.length - 1;

        while (httpListLastIndex >= 0) {
            if (httpList[httpListLastIndex].responsecode) {
                currentHttpList = httpList[httpListLastIndex];
                break;
            }
            httpListLastIndex--;
        }
        return currentHttpList;
    }

    /**
     * Returns all HTTP requests for a given media type
     * @param {MediaType} mediaType
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getHttpRequests(mediaType) {
        const metrics = metricsModel.getMetricsFor(mediaType, true);
        if (!metrics) {
            return [];
        }

        return !!metrics.HttpList ? metrics.HttpList : [];
    }

    /**
     * @param {MediaType} mediaType
     * @param {Array} loadingRequests
     * @param {Array} executedRequests
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addRequestsQueue(mediaType, loadingRequests, executedRequests) {
        metricsModel.addRequestsQueue(mediaType, loadingRequests, executedRequests);
    }

    /**
     * Returns the latest metrics for a given metric list and specific metric name
     * @param {MetricsList} metrics
     * @param {string} metricName
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getCurrent(metrics, metricName) {
        if (!metrics) {
            return null;
        }
        const list = metrics[metricName];
        return (!list || list.length === 0) ? null : list[list.length - 1];
    }

    /**
     * Returns the number of dropped frames
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getCurrentDroppedFrames() {
        const metrics = metricsModel.getMetricsFor(Constants.VIDEO, true);
        return getCurrent(metrics, MetricsConstants.DROPPED_FRAMES);
    }

    /**
     * @param {number} quality
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addDroppedFrames(quality) {
        metricsModel.addDroppedFrames(Constants.VIDEO, quality);
    }

    /**
     * Returns the current scheduling info for a given media type
     * @param {MediaType} mediaType
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getCurrentSchedulingInfo(mediaType) {
        const metrics = metricsModel.getMetricsFor(mediaType, true);
        return getCurrent(metrics, MetricsConstants.SCHEDULING_INFO);
    }

    /**
     * @param {object} request
     * @param {string} state
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addSchedulingInfo(request, state) {
        metricsModel.addSchedulingInfo(
            request.mediaType,
            new Date(),
            request.type,
            request.startTime,
            request.availabilityStartTime,
            request.duration,
            request.quality,
            request.range,
            state);
    }

    /**
     * Returns the current manifest update information
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getCurrentManifestUpdate() {
        const streamMetrics = metricsModel.getMetricsFor(Constants.STREAM);
        return getCurrent(streamMetrics, MetricsConstants.MANIFEST_UPDATE);
    }

    /**
     * @param {object} updatedFields fields to be updated
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function updateManifestUpdateInfo(updatedFields) {
        const manifestUpdate = this.getCurrentManifestUpdate();
        metricsModel.updateManifestUpdateInfo(manifestUpdate, updatedFields);
    }

    /**
     * @param {object} streamInfo
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addManifestUpdateStreamInfo(streamInfo) {
        if (streamInfo) {
            const manifestUpdate = this.getCurrentManifestUpdate();
            metricsModel.addManifestUpdateStreamInfo(manifestUpdate, streamInfo.id, streamInfo.index, streamInfo.start, streamInfo.duration);
        }
    }

    /**
     * @param {object} request
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addManifestUpdate(request) {
        metricsModel.addManifestUpdate(Constants.STREAM, request.type, request.requestStartDate, request.requestEndDate);
    }

    /**
     * @param {object} request
     * @param {string} responseURL
     * @param {number} responseStatus
     * @param {object} responseHeaders
     * @param {object} traces
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addHttpRequest(request, responseURL, responseStatus, responseHeaders, traces, cmsd) {
        metricsModel.addHttpRequest(request.mediaType,
            null,
            request.type,
            request.url,
            request.quality,
            responseURL,
            request.serviceLocation || null,
            request.range || null,
            request.requestStartDate,
            request.firstByteDate,
            request.requestEndDate,
            responseStatus,
            request.duration,
            responseHeaders,
            traces,
            request.fileLoaderType,
            cmsd);
    }

    /**
     * @param {object} representation
     * @param {MediaType} mediaType
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addManifestUpdateRepresentationInfo(representation, mediaType) {
        if (representation) {
            const manifestUpdateInfo = this.getCurrentManifestUpdate();
            metricsModel.addManifestUpdateRepresentationInfo(manifestUpdateInfo, representation.id, representation.index, representation.streamIndex, mediaType, representation.presentationTimeOffset, representation.startNumber, representation.fragmentInfoType);
        }
    }

    /**
     * Returns the current DVR window
     * @param {MediaType} mediaType
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getCurrentDVRInfo(mediaType) {
        const metrics = mediaType ? metricsModel.getMetricsFor(mediaType, true) :
            metricsModel.getMetricsFor(Constants.VIDEO, true) || metricsModel.getMetricsFor(Constants.AUDIO, true);
        return getCurrent(metrics, MetricsConstants.DVR_INFO);
    }

    /**
     * @param {MediaType} mediaType
     * @param {Date} currentTime time of the switch event
     * @param {object} mpd mpd reference
     * @param {object} range range of the dvr info
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addDVRInfo(mediaType, currentTime, mpd, range) {
        metricsModel.addDVRInfo(mediaType, currentTime, mpd, range);
    }

    /**
     * Returns the value for a specific request headers used in the latest MPD request
     * @param {string} id
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getLatestMPDRequestHeaderValueByID(id) {
        if (!id) {
            return null;
        }

        let headers = {};
        let httpRequestList,
            httpRequest,
            i;

        httpRequestList = getHttpRequests(Constants.STREAM);

        for (i = httpRequestList.length - 1; i >= 0; i--) {
            httpRequest = httpRequestList[i];

            if (httpRequest.type === HTTPRequest.MPD_TYPE) {
                headers = Utils.parseHttpHeaders(httpRequest._responseHeaders);
                break;
            }
        }

        const value = headers[id.toLowerCase()];
        return value === undefined ? null : value;
    }

    /**
     * Returns the value for a specific request headers used in the latest fragment request
     * @param {MediaType} mediaType
     * @param {string} id
     * @returns {*}
     * @memberof module:DashMetrics
     * @instance
     */
    function getLatestFragmentRequestHeaderValueByID(mediaType, id) {
        if (!id) {
            return null;
        }

        let headers = {};
        let httpRequest = getCurrentHttpRequest(mediaType);
        if (httpRequest) {
            headers = Utils.parseHttpHeaders(httpRequest._responseHeaders);
        }

        const value = headers[id.toLowerCase()];
        return value === undefined ? null : value;
    }

    /**
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addPlayList() {
        if (playListMetrics) {
            metricsModel.addPlayList(playListMetrics);
            playListMetrics = null;
        }
    }

    /**
     * Create a new playlist metric
     * @param {number} mediaStartTime
     * @param {string} startReason
     * @ignore
     */
    function createPlaylistMetrics(mediaStartTime, startReason) {
        playListMetrics = new PlayList();

        playListMetrics.start = new Date();
        playListMetrics.mstart = mediaStartTime;
        playListMetrics.starttype = startReason;
    }

    /**
     * Create a playlist trace metric
     * @param {number} representationId
     * @param {number} mediaStartTime
     * @param {number} speed
     * @ignore
     */
    function createPlaylistTraceMetrics(representationId, mediaStartTime, speed) {
        if (playListTraceMetricsClosed === true ) {
            playListTraceMetricsClosed = false;
            playListTraceMetrics = new PlayListTrace();

            playListTraceMetrics.representationid = representationId;
            playListTraceMetrics.start = new Date();
            playListTraceMetrics.mstart = mediaStartTime;
            playListTraceMetrics.playbackspeed = speed !== null ? speed.toString() : null;
        }
    }

    /**
     * Update existing playlist trace metric
     * @param {object} traceToUpdate
     * @ignore
     */
    function updatePlayListTraceMetrics(traceToUpdate) {
        if (playListTraceMetrics) {
            for (let field in playListTraceMetrics) {
                playListTraceMetrics[field] = traceToUpdate[field];
            }
        }
    }

    /**
     * Push a new playlist trace metric
     * @param endTime
     * @param reason
     * @ignore
     */
    function pushPlayListTraceMetrics(endTime, reason) {
        if (playListTraceMetricsClosed === false && playListMetrics && playListTraceMetrics && playListTraceMetrics.start) {
            const startTime = playListTraceMetrics.start;
            const duration = endTime.getTime() - startTime.getTime();
            playListTraceMetrics.duration = duration;
            playListTraceMetrics.stopreason = reason;
            playListMetrics.trace.push(playListTraceMetrics);
            playListTraceMetricsClosed = true;
        }
    }

    /**
     * @param {object} errors
     * @memberof module:DashMetrics
     * @instance
     * @ignore
     */
    function addDVBErrors(errors) {
        metricsModel.addDVBErrors(errors);
    }

    instance = {
        getCurrentRepresentationSwitch,
        getCurrentBufferState,
        getCurrentBufferLevel,
        getCurrentHttpRequest,
        getHttpRequests,
        getCurrentDroppedFrames,
        getCurrentSchedulingInfo,
        getCurrentDVRInfo,
        getCurrentManifestUpdate,
        getLatestFragmentRequestHeaderValueByID,
        getLatestMPDRequestHeaderValueByID,
        addRepresentationSwitch,
        addDVRInfo,
        updateManifestUpdateInfo,
        addManifestUpdateStreamInfo,
        addManifestUpdateRepresentationInfo,
        addManifestUpdate,
        addHttpRequest,
        addSchedulingInfo,
        addRequestsQueue,
        addBufferLevel,
        addBufferState,
        addDroppedFrames,
        addPlayList,
        addDVBErrors,
        createPlaylistMetrics,
        createPlaylistTraceMetrics,
        updatePlayListTraceMetrics,
        pushPlayListTraceMetrics,
        clearAllCurrentMetrics
    };

    setup();

    return instance;
}

DashMetrics.__dashjs_factory_name = 'DashMetrics';
export default FactoryMaker.getSingletonFactory(DashMetrics);