/**
* 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 ABRRulesCollection from '../rules/abr/ABRRulesCollection';
import Constants from '../constants/Constants';
import MetricsConstants from '../constants/MetricsConstants';
import BitrateInfo from '../vo/BitrateInfo';
import FragmentModel from '../models/FragmentModel';
import EventBus from '../../core/EventBus';
import Events from '../../core/events/Events';
import FactoryMaker from '../../core/FactoryMaker';
import RulesContext from '../rules/RulesContext';
import SwitchRequest from '../rules/SwitchRequest';
import SwitchRequestHistory from '../rules/SwitchRequestHistory';
import DroppedFramesHistory from '../rules/DroppedFramesHistory';
import ThroughputHistory from '../rules/ThroughputHistory';
import Debug from '../../core/Debug';
import {HTTPRequest} from '../vo/metrics/HTTPRequest';
import {checkInteger} from '../utils/SupervisorTools';
import MediaPlayerEvents from '../MediaPlayerEvents';
const DEFAULT_VIDEO_BITRATE = 1000;
const DEFAULT_AUDIO_BITRATE = 100;
const QUALITY_DEFAULT = 0;
function AbrController() {
const context = this.context;
const debug = Debug(context).getInstance();
const eventBus = EventBus(context).getInstance();
let instance,
logger,
abrRulesCollection,
streamController,
topQualities,
qualityDict,
streamProcessorDict,
abandonmentStateDict,
abandonmentTimeout,
windowResizeEventCalled,
elementWidth,
elementHeight,
adapter,
videoModel,
mediaPlayerModel,
customParametersModel,
cmsdModel,
domStorage,
playbackIndex,
switchHistoryDict,
droppedFramesHistory,
throughputHistory,
isUsingBufferOccupancyAbrDict,
isUsingL2AAbrDict,
isUsingLoLPAbrDict,
dashMetrics,
settings;
function setup() {
logger = debug.getLogger(instance);
resetInitialSettings();
}
/**
* Initialize everything that is not Stream specific. We only have one instance of the ABR Controller for all periods.
*/
function initialize() {
droppedFramesHistory = DroppedFramesHistory(context).create();
throughputHistory = ThroughputHistory(context).create({
settings
});
abrRulesCollection = ABRRulesCollection(context).create({
dashMetrics,
customParametersModel,
mediaPlayerModel,
settings
});
abrRulesCollection.initialize();
eventBus.on(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, _onQualityChangeRendered, instance);
eventBus.on(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance);
eventBus.on(Events.LOADING_PROGRESS, _onFragmentLoadProgress, instance);
}
/**
* Whenever a StreamProcessor is created it is added to the list of streamProcessorDict
* In addition, the corresponding objects for this object and its stream id are created
* @param {object} type
* @param {object} streamProcessor
*/
function registerStreamType(type, streamProcessor) {
const streamId = streamProcessor.getStreamInfo().id;
if (!streamProcessorDict[streamId]) {
streamProcessorDict[streamId] = {};
}
if (!switchHistoryDict[streamId]) {
switchHistoryDict[streamId] = {};
}
if (!abandonmentStateDict[streamId]) {
abandonmentStateDict[streamId] = {};
}
switchHistoryDict[streamId][type] = SwitchRequestHistory(context).create();
streamProcessorDict[streamId][type] = streamProcessor;
abandonmentStateDict[streamId][type] = {};
abandonmentStateDict[streamId][type].state = MetricsConstants.ALLOW_LOAD;
_initializeAbrStrategy(type);
if (type === Constants.VIDEO) {
setElementSize();
}
}
function _initializeAbrStrategy(type) {
const strategy = settings.get().streaming.abr.ABRStrategy;
if (strategy === Constants.ABR_STRATEGY_L2A) {
isUsingBufferOccupancyAbrDict[type] = false;
isUsingLoLPAbrDict[type] = false;
isUsingL2AAbrDict[type] = true;
} else if (strategy === Constants.ABR_STRATEGY_LoLP) {
isUsingBufferOccupancyAbrDict[type] = false;
isUsingLoLPAbrDict[type] = true;
isUsingL2AAbrDict[type] = false;
} else if (strategy === Constants.ABR_STRATEGY_BOLA) {
isUsingBufferOccupancyAbrDict[type] = true;
isUsingLoLPAbrDict[type] = false;
isUsingL2AAbrDict[type] = false;
} else if (strategy === Constants.ABR_STRATEGY_THROUGHPUT) {
isUsingBufferOccupancyAbrDict[type] = false;
isUsingLoLPAbrDict[type] = false;
isUsingL2AAbrDict[type] = false;
} else if (strategy === Constants.ABR_STRATEGY_DYNAMIC) {
isUsingBufferOccupancyAbrDict[type] = isUsingBufferOccupancyAbrDict && isUsingBufferOccupancyAbrDict[type] ? isUsingBufferOccupancyAbrDict[type] : false;
isUsingLoLPAbrDict[type] = false;
isUsingL2AAbrDict[type] = false;
}
}
function unRegisterStreamType(streamId, type) {
try {
if (streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
delete streamProcessorDict[streamId][type];
}
if (switchHistoryDict[streamId] && switchHistoryDict[streamId][type]) {
delete switchHistoryDict[streamId][type];
}
if (abandonmentStateDict[streamId] && abandonmentStateDict[streamId][type]) {
delete abandonmentStateDict[streamId][type];
}
} catch (e) {
}
}
function resetInitialSettings() {
topQualities = {};
qualityDict = {};
abandonmentStateDict = {};
streamProcessorDict = {};
switchHistoryDict = {};
isUsingBufferOccupancyAbrDict = {};
isUsingL2AAbrDict = {};
isUsingLoLPAbrDict = {};
if (windowResizeEventCalled === undefined) {
windowResizeEventCalled = false;
}
if (droppedFramesHistory) {
droppedFramesHistory.reset();
}
playbackIndex = undefined;
droppedFramesHistory = undefined;
throughputHistory = undefined;
clearTimeout(abandonmentTimeout);
abandonmentTimeout = null;
}
function reset() {
resetInitialSettings();
eventBus.off(Events.LOADING_PROGRESS, _onFragmentLoadProgress, instance);
eventBus.off(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, _onQualityChangeRendered, instance);
eventBus.off(MediaPlayerEvents.METRIC_ADDED, _onMetricAdded, instance);
if (abrRulesCollection) {
abrRulesCollection.reset();
}
}
function setConfig(config) {
if (!config) return;
if (config.streamController) {
streamController = config.streamController;
}
if (config.domStorage) {
domStorage = config.domStorage;
}
if (config.mediaPlayerModel) {
mediaPlayerModel = config.mediaPlayerModel;
}
if (config.customParametersModel) {
customParametersModel = config.customParametersModel;
}
if (config.cmsdModel) {
cmsdModel = config.cmsdModel
}
if (config.dashMetrics) {
dashMetrics = config.dashMetrics;
}
if (config.adapter) {
adapter = config.adapter;
}
if (config.videoModel) {
videoModel = config.videoModel;
}
if (config.settings) {
settings = config.settings;
}
}
function checkConfig() {
if (!domStorage || !domStorage.hasOwnProperty('getSavedBitrateSettings')) {
throw new Error(Constants.MISSING_CONFIG_ERROR);
}
}
/**
* While fragment loading is in progress we check if we might need to abort the request
* @param {object} e
* @private
*/
function _onFragmentLoadProgress(e) {
const type = e.request.mediaType;
const streamId = e.streamId;
if (!type || !streamId || !streamProcessorDict[streamId] || !settings.get().streaming.abr.autoSwitchBitrate[type]) {
return;
}
const streamProcessor = streamProcessorDict[streamId][type];
if (!streamProcessor) {
return;
}
const rulesContext = RulesContext(context).create({
abrController: instance,
streamProcessor: streamProcessor,
currentRequest: e.request,
useBufferOccupancyABR: isUsingBufferOccupancyAbrDict[type],
useL2AABR: isUsingL2AAbrDict[type],
useLoLPABR: isUsingLoLPAbrDict[type],
videoModel
});
const switchRequest = abrRulesCollection.shouldAbandonFragment(rulesContext, streamId);
if (switchRequest.quality > SwitchRequest.NO_CHANGE) {
const fragmentModel = streamProcessor.getFragmentModel();
const request = fragmentModel.getRequests({
state: FragmentModel.FRAGMENT_MODEL_LOADING,
index: e.request.index
})[0];
if (request) {
abandonmentStateDict[streamId][type].state = MetricsConstants.ABANDON_LOAD;
switchHistoryDict[streamId][type].reset();
switchHistoryDict[streamId][type].push({
oldValue: getQualityFor(type, streamId),
newValue: switchRequest.quality,
confidence: 1,
reason: switchRequest.reason
});
setPlaybackQuality(type, streamController.getActiveStreamInfo(), switchRequest.quality, switchRequest.reason);
clearTimeout(abandonmentTimeout);
abandonmentTimeout = setTimeout(
() => {
abandonmentStateDict[streamId][type].state = MetricsConstants.ALLOW_LOAD;
abandonmentTimeout = null;
},
settings.get().streaming.abandonLoadTimeout
);
}
}
}
/**
* Update dropped frames history when the quality was changed
* @param {object} e
* @private
*/
function _onQualityChangeRendered(e) {
if (e.mediaType === Constants.VIDEO) {
if (playbackIndex !== undefined) {
droppedFramesHistory.push(e.streamId, playbackIndex, videoModel.getPlaybackQuality());
}
playbackIndex = e.newQuality;
}
}
/**
* When the buffer level is updated we check if we need to change the ABR strategy
* @param e
* @private
*/
function _onMetricAdded(e) {
if (e.metric === MetricsConstants.HTTP_REQUEST && e.value && e.value.type === HTTPRequest.MEDIA_SEGMENT_TYPE && (e.mediaType === Constants.AUDIO || e.mediaType === Constants.VIDEO)) {
throughputHistory.push(e.mediaType, e.value, settings.get().streaming.abr.useDeadTimeLatency);
}
if (e.metric === MetricsConstants.BUFFER_LEVEL && (e.mediaType === Constants.AUDIO || e.mediaType === Constants.VIDEO)) {
_updateAbrStrategy(e.mediaType, 0.001 * e.value.level);
}
}
/**
* Returns the highest possible index taking limitations like maxBitrate, representationRatio and portal size into account.
* @param {string} type
* @param {string} streamId
* @return {undefined|number}
*/
function getMaxAllowedIndexFor(type, streamId) {
try {
let idx;
topQualities[streamId] = topQualities[streamId] || {};
if (!topQualities[streamId].hasOwnProperty(type)) {
topQualities[streamId][type] = 0;
}
idx = _checkMaxBitrate(type, streamId);
idx = _checkMaxRepresentationRatio(idx, type, streamId);
idx = _checkPortalSize(idx, type, streamId);
// Apply maximum suggested bitrate from CMSD headers if enabled
if (settings.get().streaming.cmsd.enabled && settings.get().streaming.cmsd.abr.applyMb) {
idx = _checkCmsdMaxBitrate(idx, type, streamId);
}
return idx;
} catch (e) {
return undefined
}
}
/**
* Returns the minimum allowed index. We consider thresholds defined in the settings, i.e. minBitrate for the corresponding media type.
* @param {string} type
* @param {string} streamId
* @return {undefined|number}
*/
function getMinAllowedIndexFor(type, streamId) {
try {
return _getMinIndexBasedOnBitrateFor(type, streamId);
} catch (e) {
return undefined
}
}
/**
* Returns the maximum allowed index.
* @param {string} type
* @param {string} streamId
* @return {undefined|number}
*/
function _getMaxIndexBasedOnBitrateFor(type, streamId) {
try {
const maxBitrate = mediaPlayerModel.getAbrBitrateParameter('maxBitrate', type);
if (maxBitrate > -1) {
return getQualityForBitrate(streamProcessorDict[streamId][type].getMediaInfo(), maxBitrate, streamId);
} else {
return undefined;
}
} catch (e) {
return undefined
}
}
/**
* Returns the minimum allowed index.
* @param {string} type
* @param {string} streamId
* @return {undefined|number}
*/
function _getMinIndexBasedOnBitrateFor(type, streamId) {
try {
const minBitrate = mediaPlayerModel.getAbrBitrateParameter('minBitrate', type);
if (minBitrate > -1) {
const mediaInfo = streamProcessorDict[streamId][type].getMediaInfo();
const bitrateList = getBitrateList(mediaInfo);
// This returns the quality index <= for the given bitrate
let minIdx = getQualityForBitrate(mediaInfo, minBitrate, streamId);
if (bitrateList[minIdx] && minIdx < bitrateList.length - 1 && bitrateList[minIdx].bitrate < minBitrate * 1000) {
minIdx++; // Go to the next bitrate
}
return minIdx;
} else {
return undefined;
}
} catch (e) {
return undefined;
}
}
/**
* Returns the maximum possible index
* @param type
* @param streamId
* @return {number|*}
*/
function _checkMaxBitrate(type, streamId) {
let idx = topQualities[streamId][type];
let newIdx = idx;
if (!streamProcessorDict[streamId] || !streamProcessorDict[streamId][type]) {
return newIdx;
}
const minIdx = getMinAllowedIndexFor(type, streamId);
if (minIdx !== undefined) {
newIdx = Math.max(idx, minIdx);
}
const maxIdx = _getMaxIndexBasedOnBitrateFor(type, streamId);
if (maxIdx !== undefined) {
newIdx = Math.min(newIdx, maxIdx);
}
return newIdx;
}
/**
* Returns the maximum possible index from CMSD model
* @param type
* @param streamId
* @return {number|*}
*/
function _checkCmsdMaxBitrate(idx, type, streamId) {
// Check CMSD max suggested bitrate only for video segments
if (type !== 'video') {
return idx;
}
// Get max suggested bitrate
let maxCmsdBitrate = cmsdModel.getMaxBitrate(type);
if (maxCmsdBitrate < 0) {
return idx;
}
// Substract audio bitrate
const audioBitrate = _getBitrateInfoForQuality(streamId, 'audio', getQualityFor('audio', streamId));
maxCmsdBitrate -= audioBitrate ? (audioBitrate.bitrate / 1000) : 0;
const maxIdx = getQualityForBitrate(streamProcessorDict[streamId][type].getMediaInfo(), maxCmsdBitrate, streamId);
logger.debug('Stream ID: ' + streamId + ' [' + type + '] Apply max bit rate from CMSD: ' + maxCmsdBitrate);
return Math.min(idx, maxIdx);
}
/**
* Returns the maximum index according to maximum representation ratio
* @param idx
* @param type
* @param streamId
* @return {number|*}
* @private
*/
function _checkMaxRepresentationRatio(idx, type, streamId) {
let maxIdx = topQualities[streamId][type]
const maxRepresentationRatio = settings.get().streaming.abr.maxRepresentationRatio[type];
if (isNaN(maxRepresentationRatio) || maxRepresentationRatio >= 1 || maxRepresentationRatio < 0) {
return idx;
}
return Math.min(idx, Math.round(maxIdx * maxRepresentationRatio));
}
/**
* Returns the maximum index according to the portal size
* @param idx
* @param type
* @param streamId
* @return {number|*}
* @private
*/
function _checkPortalSize(idx, type, streamId) {
if (type !== Constants.VIDEO || !settings.get().streaming.abr.limitBitrateByPortal || !streamProcessorDict[streamId] || !streamProcessorDict[streamId][type]) {
return idx;
}
if (!windowResizeEventCalled) {
setElementSize();
}
const streamInfo = streamProcessorDict[streamId][type].getStreamInfo();
const representation = adapter.getAdaptationForType(streamInfo.index, type, streamInfo).Representation_asArray;
let newIdx = idx;
if (elementWidth > 0 && elementHeight > 0) {
while (
newIdx > 0 &&
representation[newIdx] &&
elementWidth < representation[newIdx].width &&
elementWidth - representation[newIdx - 1].width < representation[newIdx].width - elementWidth) {
newIdx = newIdx - 1;
}
// Make sure that in case of multiple representation elements have same
// resolution, every such element is included
while (newIdx < representation.length - 1 && representation[newIdx].width === representation[newIdx + 1].width) {
newIdx = newIdx + 1;
}
}
return newIdx;
}
/**
* Gets top BitrateInfo for the player
* @param {string} type - 'video' or 'audio' are the type options.
* @param {string} streamId - Id of the stream
* @returns {BitrateInfo | null}
*/
function getTopBitrateInfoFor(type, streamId = null) {
if (!streamId) {
streamId = streamController.getActiveStreamInfo().id;
}
if (type && streamProcessorDict && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
const idx = getMaxAllowedIndexFor(type, streamId);
const bitrates = getBitrateList(streamProcessorDict[streamId][type].getMediaInfo());
return bitrates[idx] ? bitrates[idx] : null;
}
return null;
}
/**
* Returns the initial bitrate for a specific media type and stream id
* @param {string} type
* @param {string} streamId
* @returns {number} A value of the initial bitrate, kbps
* @memberof AbrController#
*/
function getInitialBitrateFor(type, streamId) {
checkConfig();
if (type === Constants.TEXT) {
return NaN;
}
const savedBitrate = domStorage.getSavedBitrateSettings(type);
let configBitrate = mediaPlayerModel.getAbrBitrateParameter('initialBitrate', type);
let configRatio = settings.get().streaming.abr.initialRepresentationRatio[type];
if (configBitrate === -1) {
if (configRatio > -1) {
const streamInfo = streamProcessorDict[streamId][type].getStreamInfo();
const representation = adapter.getAdaptationForType(streamInfo.index, type, streamInfo).Representation_asArray;
if (Array.isArray(representation)) {
const repIdx = Math.max(Math.round(representation.length * configRatio) - 1, 0);
configBitrate = representation[repIdx].bandwidth / 1000;
} else {
configBitrate = 0;
}
} else if (!isNaN(savedBitrate)) {
configBitrate = savedBitrate;
} else {
configBitrate = (type === Constants.VIDEO) ? DEFAULT_VIDEO_BITRATE : DEFAULT_AUDIO_BITRATE;
}
}
return configBitrate;
}
/**
* This function is called by the scheduleControllers to check if the quality should be changed.
* Consider this the main entry point for the ABR decision logic
* @param {string} type
* @param {string} streamId
*/
function checkPlaybackQuality(type, streamId) {
try {
if (!type || !streamProcessorDict || !streamProcessorDict[streamId] || !streamProcessorDict[streamId][type]) {
return false;
}
if (droppedFramesHistory) {
const playbackQuality = videoModel.getPlaybackQuality();
if (playbackQuality) {
droppedFramesHistory.push(streamId, playbackIndex, playbackQuality);
}
}
// ABR is turned off, do nothing
if (!settings.get().streaming.abr.autoSwitchBitrate[type]) {
return false;
}
const oldQuality = getQualityFor(type, streamId);
const rulesContext = RulesContext(context).create({
abrController: instance,
switchHistory: switchHistoryDict[streamId][type],
droppedFramesHistory: droppedFramesHistory,
streamProcessor: streamProcessorDict[streamId][type],
currentValue: oldQuality,
useBufferOccupancyABR: isUsingBufferOccupancyAbrDict[type],
useL2AABR: isUsingL2AAbrDict[type],
useLoLPABR: isUsingLoLPAbrDict[type],
videoModel
});
const minIdx = getMinAllowedIndexFor(type, streamId);
const maxIdx = getMaxAllowedIndexFor(type, streamId);
const switchRequest = abrRulesCollection.getMaxQuality(rulesContext);
let newQuality = switchRequest.quality;
if (minIdx !== undefined && ((newQuality > SwitchRequest.NO_CHANGE) ? newQuality : oldQuality) < minIdx) {
newQuality = minIdx;
}
if (newQuality > maxIdx) {
newQuality = maxIdx;
}
switchHistoryDict[streamId][type].push({ oldValue: oldQuality, newValue: newQuality });
if (newQuality > SwitchRequest.NO_CHANGE && newQuality !== oldQuality && (abandonmentStateDict[streamId][type].state === MetricsConstants.ALLOW_LOAD || newQuality < oldQuality)) {
_changeQuality(type, oldQuality, newQuality, maxIdx, switchRequest.reason, streamId);
return true;
}
return false;
} catch (e) {
return false;
}
}
/**
* Returns the current quality for a specific media type and a specific streamId
* @param {string} type
* @param {string} streamId
* @return {number|*}
*/
function getQualityFor(type, streamId = null) {
try {
if (!streamId) {
streamId = streamController.getActiveStreamInfo().id;
}
if (type && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
let quality;
if (streamId) {
qualityDict[streamId] = qualityDict[streamId] || {};
if (!qualityDict[streamId].hasOwnProperty(type)) {
qualityDict[streamId][type] = QUALITY_DEFAULT;
}
quality = qualityDict[streamId][type];
return quality;
}
}
return QUALITY_DEFAULT;
} catch (e) {
return QUALITY_DEFAULT;
}
}
/**
* Sets the new playback quality. Starts from index 0.
* If the index of the new quality is the same as the old one changeQuality will not be called.
* @param {string} type
* @param {object} streamInfo
* @param {number} newQuality
* @param {string} reason
*/
function setPlaybackQuality(type, streamInfo, newQuality, reason = null) {
if (!streamInfo || !streamInfo.id || !type) {
return;
}
const streamId = streamInfo.id;
const oldQuality = getQualityFor(type, streamId);
checkInteger(newQuality);
const topQualityIdx = getMaxAllowedIndexFor(type, streamId);
if (newQuality !== oldQuality && newQuality >= 0 && newQuality <= topQualityIdx) {
_changeQuality(type, oldQuality, newQuality, topQualityIdx, reason, streamId);
}
}
/**
*
* @param {string} streamId
* @param {type} type
* @return {*|null}
*/
function getAbandonmentStateFor(streamId, type) {
return abandonmentStateDict[streamId] && abandonmentStateDict[streamId][type] ? abandonmentStateDict[streamId][type].state : null;
}
/**
* Changes the internal qualityDict values according to the new quality
* @param {string} type
* @param {number} oldQuality
* @param {number} newQuality
* @param {number} maxIdx
* @param {string} reason
* @param {object} streamId
* @private
*/
function _changeQuality(type, oldQuality, newQuality, maxIdx, reason, streamId) {
if (type && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
const streamInfo = streamProcessorDict[streamId][type].getStreamInfo();
const isDynamic = streamInfo && streamInfo.manifestInfo && streamInfo.manifestInfo.isDynamic;
const bufferLevel = dashMetrics.getCurrentBufferLevel(type);
logger.info('Stream ID: ' + streamId + ' [' + type + '] switch from ' + oldQuality + ' to ' + newQuality + '/' + maxIdx + ' (buffer: ' + bufferLevel + ') ' + (reason ? JSON.stringify(reason) : '.'));
qualityDict[streamId] = qualityDict[streamId] || {};
qualityDict[streamId][type] = newQuality;
const bitrateInfo = _getBitrateInfoForQuality(streamId, type, newQuality);
eventBus.trigger(Events.QUALITY_CHANGE_REQUESTED,
{
oldQuality,
newQuality,
reason,
streamInfo,
bitrateInfo,
maxIdx,
mediaType: type
},
{ streamId: streamInfo.id, mediaType: type }
);
const bitrate = throughputHistory.getAverageThroughput(type, isDynamic);
if (!isNaN(bitrate)) {
domStorage.setSavedBitrateSettings(type, bitrate);
}
}
}
function _getBitrateInfoForQuality(streamId, type, idx) {
if (type && streamProcessorDict && streamProcessorDict[streamId] && streamProcessorDict[streamId][type]) {
const bitrates = getBitrateList(streamProcessorDict[streamId][type].getMediaInfo());
return bitrates[idx] ? bitrates[idx] : null;
}
return null;
}
/**
* @param {MediaInfo} mediaInfo
* @param {number} bitrate A bitrate value, kbps
* @param {String} streamId Period ID
* @param {number|null} latency Expected latency of connection, ms
* @returns {number} A quality index <= for the given bitrate
* @memberof AbrController#
*/
function getQualityForBitrate(mediaInfo, bitrate, streamId, latency = null) {
const voRepresentation = mediaInfo && mediaInfo.type ? streamProcessorDict[streamId][mediaInfo.type].getRepresentationInfo() : null;
if (settings.get().streaming.abr.useDeadTimeLatency && latency && voRepresentation && voRepresentation.fragmentDuration) {
latency = latency / 1000;
const fragmentDuration = voRepresentation.fragmentDuration;
if (latency > fragmentDuration) {
return 0;
} else {
const deadTimeRatio = latency / fragmentDuration;
bitrate = bitrate * (1 - deadTimeRatio);
}
}
const bitrateList = getBitrateList(mediaInfo);
for (let i = bitrateList.length - 1; i >= 0; i--) {
const bitrateInfo = bitrateList[i];
if (bitrate * 1000 >= bitrateInfo.bitrate) {
return i;
}
}
return QUALITY_DEFAULT;
}
/**
* @param {MediaInfo} mediaInfo
* @returns {Array|null} A list of {@link BitrateInfo} objects
* @memberof AbrController#
*/
function getBitrateList(mediaInfo) {
const infoList = [];
if (!mediaInfo || !mediaInfo.bitrateList) return infoList;
const bitrateList = mediaInfo.bitrateList;
const type = mediaInfo.type;
let bitrateInfo;
for (let i = 0, ln = bitrateList.length; i < ln; i++) {
bitrateInfo = new BitrateInfo();
bitrateInfo.mediaType = type;
bitrateInfo.qualityIndex = i;
bitrateInfo.bitrate = bitrateList[i].bandwidth;
bitrateInfo.width = bitrateList[i].width;
bitrateInfo.height = bitrateList[i].height;
bitrateInfo.scanType = bitrateList[i].scanType;
infoList.push(bitrateInfo);
}
return infoList;
}
function _updateAbrStrategy(mediaType, bufferLevel) {
// else ABR_STRATEGY_DYNAMIC
const strategy = settings.get().streaming.abr.ABRStrategy;
if (strategy === Constants.ABR_STRATEGY_DYNAMIC) {
_updateDynamicAbrStrategy(mediaType, bufferLevel);
}
}
function _updateDynamicAbrStrategy(mediaType, bufferLevel) {
try {
const stableBufferTime = mediaPlayerModel.getStableBufferTime();
const switchOnThreshold = stableBufferTime;
const switchOffThreshold = 0.5 * stableBufferTime;
const useBufferABR = isUsingBufferOccupancyAbrDict[mediaType];
const newUseBufferABR = bufferLevel > (useBufferABR ? switchOffThreshold : switchOnThreshold); // use hysteresis to avoid oscillating rules
isUsingBufferOccupancyAbrDict[mediaType] = newUseBufferABR;
if (newUseBufferABR !== useBufferABR) {
if (newUseBufferABR) {
logger.info('[' + mediaType + '] switching from throughput to buffer occupancy ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').');
} else {
logger.info('[' + mediaType + '] switching from buffer occupancy to throughput ABR rule (buffer: ' + bufferLevel.toFixed(3) + ').');
}
}
} catch (e) {
logger.error(e);
}
}
function getThroughputHistory() {
return throughputHistory;
}
function updateTopQualityIndex(mediaInfo) {
const type = mediaInfo.type;
const streamId = mediaInfo.streamInfo.id;
const max = mediaInfo.representationCount - 1;
topQualities[streamId] = topQualities[streamId] || {};
topQualities[streamId][type] = max;
return max;
}
function isPlayingAtTopQuality(streamInfo) {
const streamId = streamInfo ? streamInfo.id : null;
const audioQuality = getQualityFor(Constants.AUDIO, streamId);
const videoQuality = getQualityFor(Constants.VIDEO, streamId);
const isAtTop = (audioQuality === getMaxAllowedIndexFor(Constants.AUDIO, streamId)) &&
(videoQuality === getMaxAllowedIndexFor(Constants.VIDEO, streamId));
return isAtTop;
}
function setWindowResizeEventCalled(value) {
windowResizeEventCalled = value;
}
function setElementSize() {
if (videoModel) {
const hasPixelRatio = settings.get().streaming.abr.usePixelRatioInLimitBitrateByPortal && window.hasOwnProperty('devicePixelRatio');
const pixelRatio = hasPixelRatio ? window.devicePixelRatio : 1;
elementWidth = videoModel.getClientWidth() * pixelRatio;
elementHeight = videoModel.getClientHeight() * pixelRatio;
}
}
function clearDataForStream(streamId) {
if (droppedFramesHistory) {
droppedFramesHistory.clearForStream(streamId);
}
if (streamProcessorDict[streamId]) {
delete streamProcessorDict[streamId];
}
if (switchHistoryDict[streamId]) {
delete switchHistoryDict[streamId];
}
if (abandonmentStateDict[streamId]) {
delete abandonmentStateDict[streamId];
}
}
instance = {
initialize,
isPlayingAtTopQuality,
updateTopQualityIndex,
clearDataForStream,
getThroughputHistory,
getBitrateList,
getQualityForBitrate,
getTopBitrateInfoFor,
getMinAllowedIndexFor,
getMaxAllowedIndexFor,
getInitialBitrateFor,
getQualityFor,
getAbandonmentStateFor,
setPlaybackQuality,
checkPlaybackQuality,
setElementSize,
setWindowResizeEventCalled,
registerStreamType,
unRegisterStreamType,
setConfig,
reset
};
setup();
return instance;
}
AbrController.__dashjs_factory_name = 'AbrController';
const factory = FactoryMaker.getSingletonFactory(AbrController);
factory.QUALITY_DEFAULT = QUALITY_DEFAULT;
FactoryMaker.updateSingletonFactory(AbrController.__dashjs_factory_name, factory);
export default factory;