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

Add level suppression logic #1536

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions demo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,8 @@ function loadSelectedStream() {
{this.sumLevelParsingMs = parsingDuration;}

stats.levelParsed++;
stats.levelParsingUs = Math.round(1000*this.sumLevelParsingMs / stats.levelParsed);
stats.levelParsingUs = Math.round(1000 * this.sumLevelParsingMs / stats.levelParsed);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome, thanks for the lint fixes ;)

stats.levelsSuppressed = hls.levelSuppression._levelsSuppressed;
console.log('parsing level duration :' + stats.levelParsingUs + 'us,count:' + stats.levelParsed);
events.load.push(event);
refreshCanvas();
Expand Down Expand Up @@ -934,11 +935,12 @@ function updateLevelInfo() {
}

let button_template = '<button type="button" class="btn btn-sm ';
let button_enabled = 'btn-primary" ';
let button_enabled = 'btn-primary" ';
let button_disabled = 'btn-success" ';
let button_suppressed = 'btn-warning" ';

let html1 = button_template;
if(hls.autoLevelEnabled) {
if (hls.autoLevelEnabled) {
html1 += button_enabled;
} else {
html1 += button_disabled;
Expand Down Expand Up @@ -973,32 +975,37 @@ function updateLevelInfo() {

html4 += 'onclick="hls.nextLevel=-1">auto</button>';

for (let i=0; i < hls.levels.length; i++) {
for (let i = 0; i < hls.levels.length; i++) {
html1 += button_template;
if(hls.currentLevel === i) {
if (hls.currentLevel === i) {
html1 += button_enabled;
}
else if (hls.levelSuppression._levelsSuppressed[i]) {
html1 += button_suppressed;
} else {
html1 += button_disabled;
}

let levelName = i, label = level2label(i);
if(label) {
if (label) {
levelName += '(' + level2label(i) + ')';
}

html1 += 'onclick="hls.currentLevel=' + i + '">' + levelName + '</button>';

html2 += button_template;
if(hls.loadLevel === i) {
if (hls.loadLevel === i) {
html2 += button_enabled;
} else if (hls.levelSuppression._levelsSuppressed[i]) {
html2 += button_suppressed;
} else {
html2 += button_disabled;
}

html2 += 'onclick="hls.loadLevel=' + i + '">' + levelName + '</button>';

html3 += button_template;
if(hls.autoLevelCapping === i) {
if (hls.autoLevelCapping === i) {
html3 += button_enabled;
} else {
html3 += button_disabled;
Expand All @@ -1007,8 +1014,10 @@ function updateLevelInfo() {
html3 += 'onclick="levelCapping=hls.autoLevelCapping=' + i + ';updateLevelInfo();updatePermalink();">' + levelName + '</button>';

html4 += button_template;
if(hls.nextLevel === i) {
if (hls.nextLevel === i) {
html4 += button_enabled;
} else if (hls.levelSuppression._levelsSuppressed[i]) {
html4 += button_suppressed;
} else {
html4 += button_disabled;
}
Expand Down
8 changes: 8 additions & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
- [`liveDurationInfinity`](#livedurationinfinity)
- [`enableWorker`](#enableworker)
- [`enableSoftwareAES`](#enablesoftwareaes)
- [`enableLevelSuppression`](#enableLevelSuppression)
- [`startLevel`](#startlevel)
- [`fragLoadingTimeOut` / `manifestLoadingTimeOut` / `levelLoadingTimeOut`](#fragloadingtimeout--manifestloadingtimeout--levelloadingtimeout)
- [`fragLoadingMaxRetry` / `manifestLoadingMaxRetry` / `levelLoadingMaxRetry`](#fragloadingmaxretry--manifestloadingmaxretry--levelloadingmaxretry)
Expand Down Expand Up @@ -310,6 +311,7 @@ Configuration parameters could be provided to hls.js upon instantiation of `Hls`
liveMaxLatencyDurationCount: 10,
enableWorker: true,
enableSoftwareAES: true,
enableLevelSuppression: false,
manifestLoadingTimeOut: 10000,
manifestLoadingMaxRetry: 1,
manifestLoadingRetryDelay: 500,
Expand Down Expand Up @@ -552,6 +554,12 @@ Enable WebWorker (if available on browser) for TS demuxing/MP4 remuxing, to impr

Enable to use JavaScript version AES decryption for fallback of WebCrypto API.

### `enableLevelSuppression`

(default: `false`)

Enable to use Level Suppression. When enabled, levels will be suppressed for a given timeout (config.levelLoadingMaxRetryTimeout) when attempting to switch.

### `startLevel`

(default: `undefined`)
Expand Down
1 change: 1 addition & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export var hlsDefaultConfig = {
maxMaxBufferLength: 600, // used by stream-controller
enableWorker: true, // used by demuxer
enableSoftwareAES: true, // used by decrypter
enableLevelSuppression: false, // used by hls, level-controller
manifestLoadingTimeOut: 10000, // used by playlist-loader
manifestLoadingMaxRetry: 1, // used by playlist-loader
manifestLoadingRetryDelay: 1000, // used by playlist-loader
Expand Down
5 changes: 5 additions & 0 deletions src/controller/level-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,11 @@ export default class LevelController extends EventHandler {
} else {
// Search for available level
if (this.manualLevelIndex === -1) {
//mark problematic level as suppressed
if (this.hls.config.enableLevelSuppression) {
this.hls.levelSuppression.set(levelIndex, this.hls.config.levelLoadingMaxRetryTimeout);
logger.warn(`level controller, ${levelIndex} has been suppressed for ${this.hls.config.levelLoadingMaxRetryTimeout}`);
}
// When lowest level has been reached, let's start hunt from the top
nextLevel = (levelIndex === 0) ? this._levels.length - 1 : levelIndex - 1;
logger.warn(`level controller, ${errorDetails}: switch to ${nextLevel}`);
Expand Down
47 changes: 43 additions & 4 deletions src/hls.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import ID3TrackController from './controller/id3-track-controller';
import {isSupported} from './helper/is-supported';
import {logger, enableLogs} from './utils/logger';
import EventEmitter from 'events';
import {hlsDefaultConfig} from './config';
import {FragmentTracker} from './helper/fragment-tracker';
import { hlsDefaultConfig } from './config';
import { FragmentTracker } from './helper/fragment-tracker';
import LevelSuppression from './utils/level-suppression';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like us to use the word Restriction :)

which is more widely used in this context, rather than suppression.

What we really also would want would that this manages general constraints, like min/maximum resolution or bitrate that could be set as well. Thinking a bit ahead, but I think this all belongs together and should be put under a more wider-thought umbrella.

Copy link
Collaborator

@tchakabam tchakabam Feb 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw, as I had forgotten, we already have the whole min/max resolution stuff covered in cap-controller. So this is where all your additional features here should be implemented. See my last comment on this PR ;)


// polyfill for IE11
require('string.prototype.endswith');
Expand Down Expand Up @@ -76,6 +77,7 @@ export default class Hls {
enableLogs(config.debug);
this.config = config;
this._autoLevelCapping = -1;
this.levelSuppression = new LevelSuppression();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplest solution here would be, make this a member of our ABR-controller instance rather than a member of Hls itself.

you can then still easily access it to set your stuff.

// observer setup
var observer = this.observer = new EventEmitter();
observer.trigger = function trigger (event, ...data) {
Expand Down Expand Up @@ -154,6 +156,7 @@ export default class Hls {
this.url = null;
this.observer.removeAllListeners();
this._autoLevelCapping = -1;
this.levelSuppression = null;
}

attachMedia(media) {
Expand Down Expand Up @@ -334,15 +337,51 @@ export default class Hls {
get nextAutoLevel() {
const hls = this;
// ensure next auto level is between min and max auto level
return Math.min(Math.max(hls.abrController.nextAutoLevel,hls.minAutoLevel),hls.maxAutoLevel);
let level = Math.min(Math.max(hls.abrController.nextAutoLevel, hls.minAutoLevel), hls.maxAutoLevel);

if (hls.config.enableLevelSuppression && (hls.currentLevel !== level)) {
return this.getAppropriateLevel(level);
} else {
return level;
}

}

// this setter is used to force next auto level
// this is useful to force a switch down in auto mode : in case of load error on level N, hls.js can set nextAutoLevel to N-1 for example)
// forced value is valid for one fragment. upon succesful frag loading at forced level, this value will be resetted to -1 by ABR controller
set nextAutoLevel(nextLevel) {
const hls = this;
hls.abrController.nextAutoLevel = Math.max(hls.minAutoLevel,nextLevel);

let level = Math.max(hls.minAutoLevel, nextLevel);

if (hls.config.enableLevelSuppression) {
hls.abrController.nextAutoLevel = this.getAppropriateLevel(level);
} else {
hls.abrController.nextAutoLevel = level;
}
}

getAppropriateLevel(level) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of adding a method on our lib API, this logic should be localized to ABR controller's interface

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's confusing that this method is a getter by name, but seems to do much more ...


//to avoid extra checks, return level if not suppressed
if (!this.levelSuppression.isSuppressed(level)) return level;

//reset if all levels have been suppressed, continute to suppress last problematic level
if (this.levelSuppression.isAllSuppressed(this.minAutoLevel, this.maxAutoLevel)) {
//reset
this.levelSuppression = new LevelSuppression();
//suppress problematic level
this.levelSuppression.set(this.streamController.levelLastLoaded, this.config.levelLoadingMaxRetryTimeout);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it feels like this shouldn't be done on-access, but at some other moment

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once we moved levelRestrictions to ABR-controller, we need listen to an event that passes on this.streamController.levelLastLoaded. Please check if StreamController emits an event when this gets updated.

Then the logic here that does set, can be executed on the event instead of on-access of the level index that you want to output here :)

Copy link
Collaborator

@tchakabam tchakabam Feb 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update: CapController has already restrictedLevels as a member, so no need to add anything to ABR ctrl :)

}


//find non-suppressed level
while (this.levelSuppression.isSuppressed(level)) {
level = (level === 0) ? this.levels.length - 1 : level - 1;
}

return level;
}

/** get alternate audio tracks list from playlist **/
Expand Down
36 changes: 36 additions & 0 deletions src/utils/level-suppression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class LevelSuppression {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's call it LevelRestrictions

put this class into the ABR-controller module/file.

that's the only place you should need it (no need to even export it for now, ABR-controller file is still small)

constructor() {
this._levelsSuppressed = {};
}

isSuppressed(level) {

let expiration = this._levelsSuppressed[level];

if (Date.now() < expiration) {
return true;
}

if (this._levelsSuppressed[level]) {
delete this._levelsSuppressed[level];
}

return false;
}

isAllSuppressed(min, max) {
for (var i = min; i <= max; i++) {
if (!this.isSuppressed(i)) {
return false;
}
}
return true;
}

set(level, ttl) {
this._levelsSuppressed[level] = ttl + Date.now();
}

}

export default LevelSuppression;
125 changes: 125 additions & 0 deletions tests/unit/utils/level-suppression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for writing a unit test for this 👍

*
*
*
*/

const assert = require('assert');
const sinon = require('sinon');
import LevelSuppression from '../../../src/utils/level-suppression';
import Hls from '../../../src/hls';

describe('Level suppression logic', function () {


let levelSuppressionTimeout = 10000;


describe('level-suppression', function () {

let levelSuppression;
let clock;

beforeEach(function () {
clock = sinon.useFakeTimers();
});

afterEach(function () {
clock.restore();
});

it('suppresses some levels', function () {
levelSuppression = new LevelSuppression();
levelSuppression.set(2, levelSuppressionTimeout);
levelSuppression.set(3, levelSuppressionTimeout);
levelSuppression.set(4, levelSuppressionTimeout);

assert.strictEqual(levelSuppression.isSuppressed(0), false);
assert.strictEqual(levelSuppression.isSuppressed(1), false);
assert.strictEqual(levelSuppression.isSuppressed(2), true);
assert.strictEqual(levelSuppression.isSuppressed(3), true);
assert.strictEqual(levelSuppression.isSuppressed(4), true);
});

it('suppresses all levels', function () {
let levels = [0, 1, 2, 3, 4];
levelSuppression = new LevelSuppression();

levels.forEach((level) => {
levelSuppression.set(level, levelSuppressionTimeout)
});

assert.equal(levelSuppression.isAllSuppressed(0, levels.length - 1), true);
});

it('is not suppressed when timeout is exceeded', function () {

levelSuppression = new LevelSuppression();

levelSuppression.set(2, levelSuppressionTimeout);
levelSuppression.set(3, levelSuppressionTimeout);
levelSuppression.set(4, levelSuppressionTimeout);

clock.tick(levelSuppressionTimeout); //advance clock by length of timeout

assert.strictEqual(levelSuppression.isSuppressed(2), false);
assert.strictEqual(levelSuppression.isSuppressed(3), false);
assert.strictEqual(levelSuppression.isSuppressed(4), false);
});
});

describe('getAppropriateLevel', function () {
let hls;

beforeEach(function () {
hls = new Hls();
hls.levelController._levels = [
{ bitrate: 105000, name: "144", details: { totalduration: 4, fragments: [{}] } },
{ bitrate: 246440, name: "240", details: { totalduration: 10, fragments: [{}] } },
{ bitrate: 460560, name: "380", details: { totalduration: 10, fragments: [{}] } },
{ bitrate: 836280, name: "480", details: { totalduration: 10, fragments: [{}] } },
{ bitrate: 2149280, name: "720", details: { totalduration: 10, fragments: [{}] } },
{ bitrate: 6221600, name: "1080", details: { totalduration: 10, fragments: [{}] } }
];
});

afterEach(function () {
hls = null;
});

it('returns the next non-suppressed level when one level is suppressed', function () {

let currentLevel = 4;

// suppress the fourth level
hls.levelSuppression.set(currentLevel, hls.config.levelLoadingMaxRetryTimeout);

assert.strictEqual(hls.getAppropriateLevel(currentLevel), 3);
});

it('returns the next non-suppressed level when multiple levels are suppressed', function () {

hls.levelSuppression.set(4, hls.config.levelLoadingMaxRetryTimeout);
hls.levelSuppression.set(5, hls.config.levelLoadingMaxRetryTimeout);

let nonSuppressedLevel = hls.getAppropriateLevel(5);

assert.strictEqual(nonSuppressedLevel, 3);
});

it('returns the last level if all levels are suppressed', function () {

//last level hls loaded
hls.streamController.levelLastLoaded = 0;

//suppress all levels
hls.levelController._levels.forEach((level, levelIndex) => {
hls.levelSuppression.set(levelIndex, hls.config.levelLoadingMaxRetryTimeout)
});

assert.strictEqual(hls.getAppropriateLevel(hls.abrController.nextAutoLevel), hls.levelController._levels.length - 1);
});

})

});