Skip to content

Commit

Permalink
Prevent ABR level switching to different codec sets that would break …
Browse files Browse the repository at this point in the history
  • Loading branch information
Rob Walch committed Jan 3, 2021
1 parent 0ba9d71 commit 31cd86c
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 64 deletions.
4 changes: 3 additions & 1 deletion demo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ function loadSelectedStream() {
});

hls.on(Hls.Events.MANIFEST_PARSED, function (eventName, data) {
logStatus('No of quality levels found: ' + hls.levels.length);
logStatus(`${hls.levels.length} quality levels found`);
logStatus('Manifest successfully loaded');
stats = {
levelNb: data.levels.length,
Expand Down Expand Up @@ -1743,6 +1743,8 @@ function appendLog(textElId, message) {
logText += newMessage;
// update
el.text(logText);
const element = el[0];
element.scrollTop = element.scrollHeight - element.clientHeight;
}

function logStatus(message) {
Expand Down
4 changes: 2 additions & 2 deletions demo/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,13 @@ select option {

#statusOut {
height: auto;
max-height: 4em;
max-height: calc((17px * 3) + 19px);
overflow: auto;
}

#errorOut {
height: auto;
max-height: 4em;
max-height: calc((17px * 3) + 19px);
overflow: auto;
}

Expand Down
109 changes: 58 additions & 51 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ class AbrController implements ComponentAPI {
}

// compute next level using ABR logic
let nextABRAutoLevel = this._nextABRAutoLevel;
let nextABRAutoLevel = this.getNextABRAutoLevel();
// if forced auto level has been defined, use it to cap ABR computed quality level
if (forcedAutoLevel !== -1) {
nextABRAutoLevel = Math.min(forcedAutoLevel, nextABRAutoLevel);
Expand All @@ -312,7 +312,7 @@ class AbrController implements ComponentAPI {
return nextABRAutoLevel;
}

get _nextABRAutoLevel() {
private getNextABRAutoLevel() {
const { fragCurrent, partCurrent, hls } = this;
const { maxAutoLevel, config, minAutoLevel, media } = hls;
const currentFragDuration = partCurrent
Expand All @@ -337,7 +337,7 @@ class AbrController implements ComponentAPI {
playbackRate;

// First, look to see if we can find a level matching with our avg bandwidth AND that could also guarantee no rebuffering at all
let bestLevel = this._findBestLevel(
let bestLevel = this.findBestLevel(
avgbw,
minAutoLevel,
maxAutoLevel,
Expand All @@ -347,55 +347,56 @@ class AbrController implements ComponentAPI {
);
if (bestLevel >= 0) {
return bestLevel;
} else {
logger.trace(
'rebuffering expected to happen, lets try to find a quality level minimizing the rebuffering'
);
// not possible to get rid of rebuffering ... let's try to find level that will guarantee less than maxStarvationDelay of rebuffering
// if no matching level found, logic will return 0
let maxStarvationDelay = currentFragDuration
? Math.min(currentFragDuration, config.maxStarvationDelay)
: config.maxStarvationDelay;
let bwFactor = config.abrBandWidthFactor;
let bwUpFactor = config.abrBandWidthUpFactor;

if (!bufferStarvationDelay) {
// in case buffer is empty, let's check if previous fragment was loaded to perform a bitrate test
const bitrateTestDelay = this.bitrateTestDelay;
if (bitrateTestDelay) {
// if it is the case, then we need to adjust our max starvation delay using maxLoadingDelay config value
// max video loading delay used in automatic start level selection :
// in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level +
// the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` )
// cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration
const maxLoadingDelay = currentFragDuration
? Math.min(currentFragDuration, config.maxLoadingDelay)
: config.maxLoadingDelay;
maxStarvationDelay = maxLoadingDelay - bitrateTestDelay;
logger.trace(
`bitrate test took ${Math.round(
1000 * bitrateTestDelay
)}ms, set first fragment max fetchDuration to ${Math.round(
1000 * maxStarvationDelay
)} ms`
);
// don't use conservative factor on bitrate test
bwFactor = bwUpFactor = 1;
}
}
logger.trace(
`${
bufferStarvationDelay ? 'rebuffering expected' : 'buffer is empty'
}, finding optimal quality level`
);
// not possible to get rid of rebuffering ... let's try to find level that will guarantee less than maxStarvationDelay of rebuffering
// if no matching level found, logic will return 0
let maxStarvationDelay = currentFragDuration
? Math.min(currentFragDuration, config.maxStarvationDelay)
: config.maxStarvationDelay;
let bwFactor = config.abrBandWidthFactor;
let bwUpFactor = config.abrBandWidthUpFactor;

if (!bufferStarvationDelay) {
// in case buffer is empty, let's check if previous fragment was loaded to perform a bitrate test
const bitrateTestDelay = this.bitrateTestDelay;
if (bitrateTestDelay) {
// if it is the case, then we need to adjust our max starvation delay using maxLoadingDelay config value
// max video loading delay used in automatic start level selection :
// in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level +
// the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` )
// cap maxLoadingDelay and ensure it is not bigger 'than bitrate test' frag duration
const maxLoadingDelay = currentFragDuration
? Math.min(currentFragDuration, config.maxLoadingDelay)
: config.maxLoadingDelay;
maxStarvationDelay = maxLoadingDelay - bitrateTestDelay;
logger.trace(
`bitrate test took ${Math.round(
1000 * bitrateTestDelay
)}ms, set first fragment max fetchDuration to ${Math.round(
1000 * maxStarvationDelay
)} ms`
);
// don't use conservative factor on bitrate test
bwFactor = bwUpFactor = 1;
}
bestLevel = this._findBestLevel(
avgbw,
minAutoLevel,
maxAutoLevel,
bufferStarvationDelay + maxStarvationDelay,
bwFactor,
bwUpFactor
);
return Math.max(bestLevel, 0);
}
bestLevel = this.findBestLevel(
avgbw,
minAutoLevel,
maxAutoLevel,
bufferStarvationDelay + maxStarvationDelay,
bwFactor,
bwUpFactor
);
return Math.max(bestLevel, 0);
}

private _findBestLevel(
private findBestLevel(
currentBw: number,
minAutoLevel: number,
maxAutoLevel: number,
Expand All @@ -409,7 +410,10 @@ class AbrController implements ComponentAPI {
lastLoadedFragLevel: currentLevel,
} = this;
const { levels } = this.hls;
const live = levels[currentLevel]?.details?.live || false;
const level = levels[currentLevel];
const live = !!level?.details?.live;
const currentCodecSet = level?.codecSet;

const currentFragDuration = partCurrent
? partCurrent.duration
: fragCurrent
Expand All @@ -418,7 +422,10 @@ class AbrController implements ComponentAPI {
for (let i = maxAutoLevel; i >= minAutoLevel; i--) {
const levelInfo = levels[i];

if (!levelInfo) {
if (
!levelInfo ||
(currentCodecSet && levelInfo.codecSet !== currentCodecSet)
) {
continue;
}

Expand Down Expand Up @@ -454,7 +461,7 @@ class AbrController implements ComponentAPI {
adjustedbw > bitrate &&
// fragment fetchDuration unknown OR live stream OR fragment fetchDuration less than max allowed fetch duration, then this level matches
// we don't account for max Fetch Duration for live streams, this is to avoid switching down when near the edge of live sliding window ...
// special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that _findBestLevel will return -1
// special case to support startLevel = -1 (bitrateTest) on live streams : in that case we should not exit loop so that findBestLevel will return -1
(!fetchDuration ||
(live && !this.bitrateTestDelay) ||
fetchDuration < maxFetchDuration)
Expand Down
27 changes: 17 additions & 10 deletions src/types/level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,37 +83,44 @@ export class HlsUrlParameters {
}

export class Level {
public attrs: LevelAttributes;
public audioCodec?: string;
public readonly attrs: LevelAttributes;
public readonly audioCodec: string | undefined;
public readonly bitrate: number;
public readonly codecSet: string;
public readonly height: number;
public readonly id: number;
public readonly name: string | undefined;
public readonly videoCodec: string | undefined;
public readonly width: number;
public readonly unknownCodecs: string[] | undefined;
public audioGroupIds?: string[];
public bitrate: number;
public details?: LevelDetails;
public fragmentError: boolean = false;
public height: number;
public id: number;
public loadError: number = 0;
public loaded?: { bytes: number; duration: number };
public name: string | undefined;
public realBitrate: number = 0;
public textGroupIds?: string[];
public url: string[];
public videoCodec?: string;
public width: number;
public unknownCodecs: string[] | undefined;
private _urlId: number = 0;

constructor(data: LevelParsed) {
this.url = [data.url];
this.attrs = data.attrs;
this.bitrate = data.bitrate;
this.details = data.details;
if (data.details) {
this.details = data.details;
}
this.id = data.id || 0;
this.name = data.name;
this.width = data.width || 0;
this.height = data.height || 0;
this.audioCodec = data.audioCodec;
this.videoCodec = data.videoCodec;
this.unknownCodecs = data.unknownCodecs;
this.codecSet = [data.videoCodec, data.audioCodec]
.filter((c) => c)
.join(',')
.replace(/\.[^.,]+/g, '');
}

get maxBitrate(): number {
Expand Down
1 change: 1 addition & 0 deletions tests/unit/controller/level-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ describe('LevelController', function () {
audioCodec: undefined,
audioGroupIds: undefined,
bitrate: 246440,
codecSet: '',
details: data.levels[1].details,
fragmentError: false,
height: 0,
Expand Down

0 comments on commit 31cd86c

Please sign in to comment.