-
Notifications
You must be signed in to change notification settings - Fork 425
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
feat: update ABR algorithm to use moving average and switching conditionally depending on the buffer level #886
Conversation
What I thought would be an easy change is making things a lot trickier. First, I saw the playback watcher kick in a couple of times for "excessive downloading" (#884). Tests likely don't work due to the changes in the default bandwidth estimation and bitrate adaptation algorithms. |
I think that we need to make this behind a switch, maybe we have a separate |
4d28c98
to
61cc0a5
Compare
00a8ee5
to
549713b
Compare
@@ -73,6 +73,10 @@ <h3>Options</h3> | |||
<input id=partial type="checkbox"> | |||
Handle Partial (reloads player) | |||
</label> | |||
<label> | |||
<input id=buffer-water type="checkbox"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adds an option to our debug page to use the new playlist selection logic.
window.videojs.options.vhs = window.videojs.options.vhs || {}; | ||
window.videojs.options.vhs.handlePartialData = event.target.checked; | ||
// reload the player and scripts | ||
stateEls.minified.dispatchEvent(newEvent('change')); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to use this code for the buffer water line option and noticed that it didn't really work in all cases. So I updated them both.
@@ -329,7 +331,7 @@ | |||
sources.dispatchEvent(newEvent('change')); | |||
} | |||
player.on('loadedmetadata', function() { | |||
if (player.vhs) { | |||
if (player.tech_.vhs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was causing a deprecation warning.
src/config.js
Outdated
BUFFER_LOW_WATER_LINE_RATE: 1 | ||
|
||
// TODO: Remove this when useBufferWaterLines is removed | ||
MAX_BUFFER_LOW_WATER_LINE_NEW: 16, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what we should call this, but in order to remain backwards compatible we needed a new config value or a hard coded value in shouldSwitchToMedia_
. Perhaps a better name? I didn't really want to make it that much longer though as the name is already unwieldy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's probably good enough.
@@ -58,31 +60,61 @@ const shouldSwitchToMedia = function({ | |||
return false; | |||
} | |||
|
|||
const sharedLogLine = `allowing switch ${currentPlaylist && currentPlaylist.id || 'null'} -> ${nextPlaylist.id}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added tons of logging throughout this function so that we can track what is going on by turning debug logging on.
@@ -318,7 +352,12 @@ export class MasterPlaylistController extends videojs.EventTarget { | |||
selectedMedia = this.selectPlaylist(); | |||
} | |||
|
|||
if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the one of the changes that isn't behind a feature switch, but I think it is something that we should have been doing. This prevents us from switching aborting a bunch of things and then re-requesting the same playlist and requesting them again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense, we probably shouldn't ever load a separate playlist automatically without going through shouldSwitchToMedia_
@@ -478,6 +517,27 @@ export class MasterPlaylistController extends videojs.EventTarget { | |||
this.tech_.trigger({type: 'usage', name: 'hls-playlist-cue-tags'}); | |||
} | |||
} | |||
|
|||
shouldSwitchToMedia_(nextPlaylist) { | |||
const currentPlaylist = this.masterPlaylistLoader_.media(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We were calling this function the same way everywhere except for nextPlaylist
so I just added it to the mpc object.
src/master-playlist-controller.js
Outdated
this.mainSegmentLoader_.on('progress', () => { | ||
if (this.bufferWaterLineSelector) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sometimes, especially with low bandwidth, or when bandwidth gets slower, we do not switch fast enough by waiting for an entire segment to download.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I debated using this on without the flag, but I think we want to keep the current playlist switching intact wherever possible, so I did not do it.
src/master-playlist-controller.js
Outdated
@@ -542,7 +598,21 @@ export class MasterPlaylistController extends videojs.EventTarget { | |||
this.onEndOfStream(); | |||
}); | |||
|
|||
this.mainSegmentLoader_.on('earlyabort', () => { | |||
this.mainSegmentLoader_.on('earlyabort', (event) => { | |||
if (this.bufferWaterLineSelector) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In segmentLoader
we used to abort the download during an earlyabort
, but that was problematic because with the new switching algorithm at certain buffer values we do not want to force a switch. Ultimately I think that we should move most of the logic for what triggers earlyabort
in here, but doing that in a backwards compatible way wasn't going to be possible without drastic effort.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, now we exclude the current playlist, but we may not switch away from it? Should we only exclude it in this case if we do end up switching away from it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We only temporarily exclude it below so that it isn't selected as the nextPlaylist
in selectPlaylist
. Then we remove the exclusion and make sure that we should switch to the next playlist before actually blacklisting it and switching. Somewhat confusing, I will add some comments here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I totally missed that. That makes sense. Comments would definitely be helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also of note is that we always blacklist (which was the previous behavior) when not using the bufferWaterLineSelector
option.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, that's excpted, thanks.
@@ -1326,22 +1326,22 @@ export default class SegmentLoader extends videojs.EventTarget { | |||
* @return {boolean} True if the request was aborted, false otherwise | |||
* @private | |||
*/ | |||
abortRequestEarly_(stats) { | |||
earlyAbortWhenNeeded_(stats) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this function no longer aborts, the name needed a slight change. Now we only trigger earlyabort
whenever we think that an earlyabort
should happen and let master playlist controller handle a switch if it thinks that it should happen.
@@ -340,7 +340,7 @@ export const LoaderCommonFactory = ({ | |||
}); | |||
|
|||
QUnit.test( | |||
'aborts request at progress events if bandwidth is too low', | |||
'triggers earlyabort at progress events if bandwidth is too low', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test changed as earlyabort no longer aborts directly.
|
||
this.masterPlaylistController.tech_.currentTime = () => currentTime; | ||
this.masterPlaylistController.tech_.buffered = () => videojs.createTimeRanges(buffered); | ||
this.masterPlaylistController.duration = () => duration; | ||
this.masterPlaylistController.selectPlaylist = () => { | ||
return { | ||
id: id++, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was needed as we compare playlist ids now.
} | ||
|
||
// set the bandwidth to that of the desired playlist being sure to scale by | ||
// BANDWIDTH_VARIANCE and add one so the playlist selector does not exclude it | ||
// don't trigger a bandwidthupdate as the bandwidth is artifial | ||
this.bandwidth = | ||
switchCandidate.playlist.attributes.BANDWIDTH * Config.BANDWIDTH_VARIANCE + 1; | ||
this.abort(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
where does the abort happen now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in the earlyabort
handler in master-playlist-controller
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess I don't see it? Unless I'm not understanding things. In MPC only call to segment loader's abort is inside mediachanging
and setCurrentTime
. Unless mediachanging
ends up getting called as part of that and thus we don't need to call it explicitly here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
abort is called when we blacklist, so in the earlyabort
handler we call blacklistCurrentPlaylist
which will call abort and load a new playlist.
src/master-playlist-controller.js
Outdated
|
||
// when switching down, if our buffer is lower than the high water line, | ||
// we can switch down | ||
if (nextBandwidth < currBandwidth && (!bufferWaterLineSelector || forwardBuffer < bufferHighWaterLine)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cool, this works surprisingly well and allows us not to need duplicate this method outright. Though, duplicating the method makes it exceptionally clear what the old and what the new thing is and makes it easy to remove it. I think with the comments and the earlier commit in this PR we can figure it out.
@@ -318,7 +352,12 @@ export class MasterPlaylistController extends videojs.EventTarget { | |||
selectedMedia = this.selectPlaylist(); | |||
} | |||
|
|||
if (!selectedMedia || !this.shouldSwitchToMedia_(selectedMedia)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes sense, we probably shouldn't ever load a separate playlist automatically without going through shouldSwitchToMedia_
src/master-playlist-controller.js
Outdated
@@ -542,7 +598,21 @@ export class MasterPlaylistController extends videojs.EventTarget { | |||
this.onEndOfStream(); | |||
}); | |||
|
|||
this.mainSegmentLoader_.on('earlyabort', () => { | |||
this.mainSegmentLoader_.on('earlyabort', (event) => { | |||
if (this.bufferWaterLineSelector) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, now we exclude the current playlist, but we may not switch away from it? Should we only exclude it in this case if we do end up switching away from it?
src/config.js
Outdated
BUFFER_LOW_WATER_LINE_RATE: 1 | ||
|
||
// TODO: Remove this when useBufferWaterLines is removed | ||
MAX_BUFFER_LOW_WATER_LINE_NEW: 16, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's probably good enough.
…ovingAverage by default with decay of 0.55
If we are switching down, we want to stay with the current rendition if we have lots of buffer left. If we are switching up, we want to stay with the current rendition if there isn't a lot of buffer.
Co-authored-by: Gary Katsevman <git@gkatsev.com>
cb2ed9f
to
db9fdc3
Compare
Before we merge, I think we should change the option from |
… on an interval (#978) This is a followup to #886. There, I noticed that if we get sustained bad network we will time out because we will an opportunity for earlyabort. At first, I tried working around by delaying earlyabort in that case (#966) but as I was getting that ready I kept finding edge cases that we needed to account for. Specifically, the issue is that we only run our ABR algorithm on progress and bandwidthchange events from the segment loader. This means that if the download stalls, we stop run our algorithm and will eventually stall playback. Instead, we should remove earlyabort altogether (f897dde) and set up an interval (currently 250ms) to re-run our ABR algorithm. This means that if the network becomes bad for a sustained period, we will drop down once the buffer allows us but if the network recovers, we will switch up appropriately as well. Also, the interval is stopped while we're paused. Fixes #964.
We want to update our ABR algorithm to be more reliable. We already have a moving average bandwidth estimator, but we weren't using it, so, we're switching to use it with a value of 0.55 for the decay factor.
For the bitrate adaptation, we want to start switching conditionally depending on the buffer level. If the buffer level is high, and we're switching down, we want to stick with our current rendition. We also want to stick with the current rendition if we are switching up and the buffer level is low.