Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

split ThroughputHistory out of ThroughputRule, update InsufficientBufferRule #2003

Merged
merged 9 commits into from
Jun 16, 2017
2 changes: 1 addition & 1 deletion src/dash/controllers/RepresentationController.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ function RepresentationController() {
voAvailableRepresentations = updateRepresentations(voAdaptation);

if (realAdaptation === null && type !== 'fragmentedText') {
averageThroughput = abrController.getAverageThroughput(type);
averageThroughput = abrController.getThroughputHistory().getAverageThroughput(type);
bitrate = averageThroughput || abrController.getInitialBitrateFor(type, streamInfo);
quality = abrController.getQualityForBitrate(streamProcessor.getMediaInfo(), bitrate);
} else {
Expand Down
28 changes: 18 additions & 10 deletions src/streaming/controllers/AbrController.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ import RulesContext from '../rules/RulesContext.js';
import SwitchRequest from '../rules/SwitchRequest.js';
import SwitchRequestHistory from '../rules/SwitchRequestHistory.js';
import DroppedFramesHistory from '../rules/DroppedFramesHistory.js';
import ThroughputHistory from '../rules/ThroughputHistory.js';
import MetricsModel from '../models/MetricsModel.js';
import {HTTPRequest} from '../vo/metrics/HTTPRequest';
import DashMetrics from '../../dash/DashMetrics.js';
import Debug from '../../core/Debug';

Expand All @@ -70,7 +72,6 @@ function AbrController() {
qualityDict,
bitrateDict,
ratioDict,
averageThroughputDict,
streamProcessorDict,
abandonmentStateDict,
abandonmentTimeout,
Expand All @@ -87,6 +88,7 @@ function AbrController() {
playbackIndex,
switchHistoryDict,
droppedFramesHistory,
throughputHistory,
metricsModel,
dashMetrics;

Expand All @@ -112,6 +114,10 @@ function AbrController() {
droppedFramesHistory = DroppedFramesHistory(context).create();
setElementSize();
}
eventBus.on(MediaPlayerEvents.METRIC_ADDED, onMetricAdded, this);
throughputHistory = ThroughputHistory().create({
mediaPlayerModel: mediaPlayerModel
});
}

function createAbrRulesCollection() {
Expand All @@ -131,7 +137,6 @@ function AbrController() {
qualityDict = {};
bitrateDict = {};
ratioDict = {};
averageThroughputDict = {};
abandonmentStateDict = {};
streamProcessorDict = {};
switchHistoryDict = {};
Expand All @@ -142,8 +147,10 @@ function AbrController() {
}
eventBus.off(Events.LOADING_PROGRESS, onFragmentLoadProgress, this);
eventBus.off(MediaPlayerEvents.QUALITY_CHANGE_RENDERED, onQualityChangeRendered, this);
eventBus.off(MediaPlayerEvents.METRIC_ADDED, onMetricAdded, this);
playbackIndex = undefined;
droppedFramesHistory = undefined;
throughputHistory = undefined;
clearTimeout(abandonmentTimeout);
abandonmentTimeout = null;
if (abrRulesCollection) {
Expand All @@ -169,6 +176,12 @@ function AbrController() {
}
}

function onMetricAdded(e) {
if (e.metric === 'HttpList' && e.value && e.value.type === HTTPRequest.MEDIA_SEGMENT_TYPE && (e.mediaType === 'audio' || e.mediaType === 'video')) {
throughputHistory.push(e.mediaType, e.value);
}
}

function getTopQualityIndexFor(type, id) {
let idx;
topQualities[id] = topQualities[id] || {};
Expand Down Expand Up @@ -448,12 +461,8 @@ function AbrController() {
return isBufferRich;
}

function setAverageThroughput(type, value) {
averageThroughputDict[type] = value;
}

function getAverageThroughput(type) {
return averageThroughputDict[type];
function getThroughputHistory() {
return throughputHistory;
}

function updateTopQualityIndex(mediaInfo) {
Expand Down Expand Up @@ -624,7 +633,7 @@ function AbrController() {
instance = {
isPlayingAtTopQuality: isPlayingAtTopQuality,
updateTopQualityIndex: updateTopQualityIndex,
getAverageThroughput: getAverageThroughput,
getThroughputHistory: getThroughputHistory,
getBitrateList: getBitrateList,
getQualityForBitrate: getQualityForBitrate,
getMaxAllowedBitrateFor: getMaxAllowedBitrateFor,
Expand All @@ -648,7 +657,6 @@ function AbrController() {
setAbandonmentStateFor: setAbandonmentStateFor,
setPlaybackQuality: setPlaybackQuality,
checkPlaybackQuality: checkPlaybackQuality,
setAverageThroughput: setAverageThroughput,
getTopQualityIndexFor: getTopQualityIndexFor,
setElementSize: setElementSize,
setWindowResizeEventCalled: setWindowResizeEventCalled,
Expand Down
186 changes: 186 additions & 0 deletions src/streaming/rules/ThroughputHistory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/**
* 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) 2017, 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.js';

// throughput generally stored in kbit/s
// latency generally stored in ms

function ThroughputHistory(config) {

const MAX_MEASUREMENTS_TO_KEEP = 20;
const AVERAGE_THROUGHPUT_SAMPLE_AMOUNT_LIVE = 3;
const AVERAGE_THROUGHPUT_SAMPLE_AMOUNT_VOD = 4;
const AVERAGE_LATENCY_SAMPLE_AMOUNT = 4;
const CACHE_LOAD_THRESHOLD_VIDEO = 50;
const CACHE_LOAD_THRESHOLD_AUDIO = 5;
const THROUGHPUT_DECREASE_SCALE = 1.3;
const THROUGHPUT_INCREASE_SCALE = 1.3;

const mediaPlayerModel = config.mediaPlayerModel;

let throughputDict,
latencyDict;

function setup() {
throughputDict = {};
latencyDict = {};
}

function isCachedResponse(mediaType, latencyMs, downloadTimeMs) {
if (mediaType === 'video') {
return downloadTimeMs < CACHE_LOAD_THRESHOLD_VIDEO;
} else if (mediaType === 'audio') {
return downloadTimeMs < CACHE_LOAD_THRESHOLD_AUDIO;
}
}

function push(mediaType, httpRequest) {
if (!httpRequest.trace || !httpRequest.trace.length) {
return;
}

const latencyTimeInMilliseconds = (httpRequest.tresponse.getTime() - httpRequest.trequest.getTime()) || 1;
const downloadTimeInMilliseconds = (httpRequest._tfinish.getTime() - httpRequest.tresponse.getTime()) || 1; //Make sure never 0 we divide by this value. Avoid infinity!
const downloadBytes = httpRequest.trace.reduce((a, b) => a + b.b[0], 0);
let throughput = Math.round((8 * downloadBytes) / downloadTimeInMilliseconds); // bits/ms = kbits/s

throughputDict[mediaType] = throughputDict[mediaType] || [];
latencyDict[mediaType] = latencyDict[mediaType] || [];

if (isCachedResponse(mediaType, latencyTimeInMilliseconds, downloadTimeInMilliseconds)) {
if (throughputDict[mediaType].length > 0 && !throughputDict[mediaType].hasCachedEntries) {
// already have some entries which are not cached entries
// prevent cached fragment loads from skewing the average values
return;
} else { // have no entries || have cached entries
// no uncached entries yet, rely on cached entries, set allowance for ABR rules
throughput /= 1000;
throughputDict[mediaType].hasCachedEntries = true;
}
} else if (throughputDict[mediaType] && throughputDict[mediaType].hasCachedEntries) {
// if we are here then we have some entries already, but they are cached, and now we have a new uncached entry
throughputDict[mediaType] = [];
latencyDict[mediaType] = [];
}

throughputDict[mediaType].push(throughput);
if (throughputDict[mediaType].length > MAX_MEASUREMENTS_TO_KEEP) {
throughputDict[mediaType].shift();
}

latencyDict[mediaType].push(latencyTimeInMilliseconds);
if (latencyDict[mediaType].length > MAX_MEASUREMENTS_TO_KEEP) {
latencyDict[mediaType].shift();
}
}

function getSampleSize(isThroughput, mediaType, isLive) {
let arr;
let sampleSize;

if (isThroughput) {
arr = throughputDict[mediaType];
sampleSize = isLive ? AVERAGE_THROUGHPUT_SAMPLE_AMOUNT_LIVE : AVERAGE_THROUGHPUT_SAMPLE_AMOUNT_VOD;
} else {
arr = latencyDict[mediaType];
sampleSize = AVERAGE_LATENCY_SAMPLE_AMOUNT;
}

if (!arr) {
sampleSize = 0;
} else if (sampleSize >= arr.length) {
sampleSize = arr.length;
} else if (isThroughput) {
// if throughput samples vary a lot, average over a wider sample
for (let i = 1; i < sampleSize; ++i) {
let ratio = arr[-i] / arr[-i - 1];
if (ratio >= THROUGHPUT_INCREASE_SCALE || ratio <= 1 / THROUGHPUT_DECREASE_SCALE) {
sampleSize += 1;
if (sampleSize === arr.length) { // cannot increase sampleSize beyond arr.length
break;
}
}
}
}

return sampleSize;
}

function getAverage(isThroughput, mediaType, isDynamic) {
let sampleSize = getSampleSize(isThroughput, mediaType, isDynamic);
let dict = isThroughput ? throughputDict : latencyDict;
let arr = dict[mediaType];

if (sampleSize === 0 || !arr || arr.length === 0) {
return NaN;
}

arr = arr.slice(-sampleSize); // still works if sampleSize too large
// arr.length >= 1
return arr.reduce((total, elem) => total + elem) / arr.length;
}

function getAverageThroughput(mediaType, isDynamic) {
return getAverage(true, mediaType, isDynamic);
}

function getSafeAverageThroughput(mediaType, isDynamic) {
let average = getAverageThroughput(mediaType, isDynamic);
if (!isNaN(average)) {
average *= mediaPlayerModel.getBandwidthSafetyFactor();
}
return average;
}

function getAverageLatency(mediaType) {
return getAverage(false, mediaType);
}

function reset() {
setup();
}

const instance = {
push: push,
getAverageThroughput: getAverageThroughput,
getSafeAverageThroughput: getSafeAverageThroughput,
getAverageLatency: getAverageLatency,
reset: reset
};

setup();
return instance;
}

ThroughputHistory.__dashjs_factory_name = 'ThroughputHistory';
let factory = FactoryMaker.getClassFactory(ThroughputHistory);
export default factory;
8 changes: 4 additions & 4 deletions src/streaming/rules/abr/ABRRulesCollection.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ function ABRRulesCollection(config) {
qualitySwitchRules.push(
ThroughputRule(context).create({
metricsModel: metricsModel,
dashMetrics: dashMetrics,
mediaPlayerModel: mediaPlayerModel
dashMetrics: dashMetrics
})
);
qualitySwitchRules.push(
InsufficientBufferRule(context).create({
metricsModel: metricsModel
metricsModel: metricsModel,
dashMetrics: dashMetrics
})
);
qualitySwitchRules.push(
Expand Down Expand Up @@ -217,4 +217,4 @@ factory.QUALITY_SWITCH_RULES = QUALITY_SWITCH_RULES;
factory.ABANDON_FRAGMENT_RULES = ABANDON_FRAGMENT_RULES;
FactoryMaker.updateSingletonFactory(ABRRulesCollection.__dashjs_factory_name, factory);

export default factory;
export default factory;
Loading