-
Notifications
You must be signed in to change notification settings - Fork 793
Abr Improvements #1176
Abr Improvements #1176
Conversation
Good thing git can now squash and merge.. This might be the ugliest commit list in the history of videojs! 😆 |
src/master-playlist-controller.js
Outdated
buffered.end(buffered.length - 1) - this.tech_.currentTime() : 0; | ||
|
||
const currentTime = this.tech_.currentTime(); | ||
const initial = Config.BUFFER_LOW_WATER_LINE; |
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 should make getter functions on MPC for low water line and goal buffer length, and pass the GBL one down to the segment loader to use
src/media-segment-request.js
Outdated
|
||
// record the time that we receive the first byte of data | ||
if (!segment.stats.firstByteRoundTripTime && segment.stats.bytesReceived) { | ||
segment.stats.firstByteRoundTripTime = Date.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.
firstByteRoundTripTime
is probably not the most accurate property name
@@ -814,6 +817,134 @@ QUnit.test('re-triggers bandwidthupdate events on the tech', function(assert) { | |||
assert.equal(bandwidthupdateEvents, 2, 'triggered bandwidthupdate'); | |||
}); | |||
|
|||
// TODO: Add test for ignoring low waterline in LIVE playback |
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.
let's do this todo
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.
Still need to review tests. Not reviewing network simulator files.
src/master-playlist-controller.js
Outdated
@@ -548,7 +550,30 @@ export class MasterPlaylistController extends videojs.EventTarget { | |||
this.mainSegmentLoader_.on('bandwidthupdate', () => { | |||
// figure out what stream the next segment should be downloaded from |
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 comment is a bit extraneous now
src/master-playlist-controller.js
Outdated
|
||
const bufferLowWaterLine = this.bufferLowWaterLine(); | ||
|
||
// we want to switch down to lower resolutions quickly to continue playback, but |
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 at least for this top piece, we can move it above the relevant lines below.
// switch down to lower resolutions quickly to continue playback
nextPlaylist.attributes.BANDWIDTH < currentPlaylist.attributes.BANDWIDTH ||
// ensure we have some buffer before we switch up to prevent us running out of
// buffer while loading a higher rendition
forwardBuffer >= bufferLowWaterLine) {
src/segment-loader.js
Outdated
* updated to trigger a playlist switch. | ||
* | ||
* @param {Object} stats | ||
* Object continaing stats about the request timing and size |
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.
containing
src/segment-loader.js
Outdated
this.duration_(), | ||
this.currentTimeline_, | ||
currentTime); | ||
// There is no sync point for this playlist, so switching to it will require a |
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.
If there is no
src/segment-loader.js
Outdated
} | ||
|
||
// set the bandwidth to that of the desired playlist | ||
// (Being sure to scale by BANDWIDTH_VARIANCE and add one so the |
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.
Can remove parenthesis
@@ -776,7 +898,7 @@ export default class SegmentLoader extends videojs.EventTarget { | |||
seekable.start(0) < currentTime) { | |||
removeToTime = seekable.start(0); | |||
} else { | |||
removeToTime = currentTime - 60; | |||
removeToTime = currentTime - 30; |
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.
Comment for 1 minute is out of date (above). And we should probably mention that we have to keep it lower than max LWL. Though we should probably calculate based on LWL, particularly if people set their own values.
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 maybe we should calculate based on GBL. The reason we lowered this was we increased GBL from 30 -> 60, so we decreased the trim. Maybe we should have a rule of only having 90 (or something similar, while also having some minimum trim incase someone sets GBL very high, we still want to trim content) seconds buffered max and depending on GBL, trim the remaining?
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.
Makes sense.
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.
deciding to go back to 60 second trim. Comment in code above stating 150mb
should actually be 150MB
which is ~5 minutes of playback, so we should be safe keeping 60 sec of back buffer
src/videojs-contrib-hls.js
Outdated
@@ -58,6 +58,98 @@ Object.defineProperty(Hls, 'GOAL_BUFFER_LENGTH', { | |||
} | |||
}); | |||
|
|||
Object.defineProperty(Hls, 'MAX_GOAL_BUFFER_LENGTH', { |
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.
A lot of these are simply copies. We should have a method that generates them, and make all of them one line function calls.
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 would also add that you could shove the property string 'MAX_GOAL_BUFFER_LENGTH' into a variable somewhere.
If these functions are all similar enough you could probably dump them all in an array and run through it to define these objects.
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.
Should we make them all have the same constraints (e.g. positive integer) and remove the low waterline constraints for being below GBL? We can just make sure to check for valid numbers whenever we use them and make no assumptions about what the numbers are, which we already do I believe
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 since the variable name is only used in one spot, it might make more sense to keep it as a string inline. If we end up using it in more than one spot, then we should consider making a constant for it.
I think we can remove the various constraints for now. Most people should realize that setting a LWL above GBL just won't work. If people run into trouble with them in the future, then we can add in the config specific constraints again.
src/config.js
Outdated
// How much of the buffer must be filled before we consider upswitching | ||
BUFFER_LOW_WATER_LINE: 0, | ||
MAX_BUFFER_LOW_WATER_LINE: 30, | ||
BUFFER_LOW_WATER_RATE: 1 |
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.
For consistency, maybe BUFFER_LOW_WATER_LINE_RATE
?
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.
should GOAL_BUFFER_RATE
be updated to GOAL_BUFFER_LENGTH_RATE
then?
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.
Makes sense to me
* | ||
* @return {Number} Desired forward buffer length in seconds | ||
*/ | ||
goalBufferLength() { |
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.
There is some duplication of this function in the test suite. You could:
1.Take currentTime in as a variable into the function and use that for testing OR
2. Take currentTime in and set it to a default value, then change it/set it in the test.
This would also let you write a couple of tiny unit tests to check the internal logic of this function if you wanted to.
You could probably do the same for bufferLowWaterLine if you wanted to test it similarly.
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 test suite i decided to set this.goalBufferLength = MasterPlaylistController.prototype.goalBufferLength.bind(this)
to eliminate the duplication. How are you with that approach?
src/playlist-selectors.js
Outdated
let resolutionBestVariantList = []; | ||
|
||
stableSort(sortedPlaylists, comparePlaylistBandwidth); | ||
const simpleSelector = function(master, playerBandwidth, playerWidth, playerHeight) { |
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.
Given that I am a big n00b it would be nice to have this function commented, at the moment I'm guessing it selects the proper rendition based on the criteria that's passed in.
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.
Yea it selects the best rendition available given the bandwidth and player size constraints
let haveResolution = bandwidthPlaylistReps.filter((rep) => rep.width && rep.height); | ||
|
||
// sort variants by resolution | ||
stableSort(haveResolution, (left, right) => left.width - right.width); |
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 seems to be sorting and then filtering it, is there any reason not to sort the final output from whatever filters you've applied?
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.
One reason is that we reuse haveResolution
in different filters to get different values, so it's probably better to sort once then filter twice than filter twice and sort twice. We only ever sort twice, once by bandwidth and once by resolution and I think its easier to reason about the filters when they are working on sorted lists
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.
Ah, makes sense.
* @private | ||
*/ | ||
abortRequestEarly_(stats) { | ||
if (this.hls_.tech_.paused() || |
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.
Anyway to break this almost 100 line function down into a couple of smaller functions?
Maybe some of the "ifs" could be moved elsewhere? Or some of the logic in the sort functions?
src/segment-loader.js
Outdated
return; | ||
} | ||
|
||
if (this.abortRequestEarly_(simpleSegment.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.
Could just move this condition up into the if statement above.
@@ -46,7 +46,7 @@ export const syncPointStrategies = [ | |||
{ | |||
name: 'Segment', | |||
run: (syncController, playlist, duration, currentTimeline, currentTime) => { | |||
let segments = playlist.segments; | |||
let segments = playlist.segments || []; |
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.
Could do something like let { segments = [] } = playlist;
src/videojs-contrib-hls.js
Outdated
@@ -58,6 +58,98 @@ Object.defineProperty(Hls, 'GOAL_BUFFER_LENGTH', { | |||
} | |||
}); | |||
|
|||
Object.defineProperty(Hls, 'MAX_GOAL_BUFFER_LENGTH', { |
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 would also add that you could shove the property string 'MAX_GOAL_BUFFER_LENGTH' into a variable somewhere.
If these functions are all similar enough you could probably dump them all in an array and run through it to define these objects.
src/videojs-contrib-hls.js
Outdated
this.options_.bandwidth = 4194304; | ||
// only use Android for mobile because iOS does not support MSE (and uses | ||
// native HLS) | ||
this.options_.bandwidth = videojs.browser.IS_ANDROID ? 500000 : 4194304; |
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.
Would be neat to store these constants in a couple of variables.
test/loader-common.js
Outdated
playbackRate: () => this.playbackRate | ||
} | ||
}; | ||
this.goalBufferLength = () => { |
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 function I mentioned before that you may be able to avoid duplicating here.
loader.playlist(playlist1, xhrOptions); | ||
loader.load(); | ||
|
||
this.clock.tick(1); |
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.
If the order of each test here is unimportant (one test doesn't rely on another) you could break it up into a few smaller/more specific tests. It might make it easier to debug if one specific case fails but the rest work.
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 is actually just one test, order is very important here. There is only one segment request in flight for this test, each dispatchEvent
call is mocking a progress event for the xhr request. using the value on loaded
within the event and the number of clock tick
s that have passed since the request has been made is what is being used for the estimated bandwidth
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.
Gotcha, that makes sense.
return false; | ||
} | ||
|
||
const switchCandidate = minRebufferMaxBandwidthSelector({ |
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.
😎
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.
😎
… of downloading the segment in a reasonable time
|
||
const size = segmentDuration * playlist.attributes.BANDWIDTH; | ||
|
||
return (size - (bytesReceived * 8)) / bandwidth; |
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.
Should this be bitsReceived
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.
No, thats why we multiply by 8 to convert to bits
return false; | ||
} | ||
|
||
const switchCandidate = minRebufferMaxBandwidthSelector({ |
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.
😎
src/segment-loader.js
Outdated
@@ -898,7 +883,7 @@ export default class SegmentLoader extends videojs.EventTarget { | |||
seekable.start(0) < currentTime) { | |||
removeToTime = seekable.start(0); | |||
} else { | |||
removeToTime = currentTime - 30; | |||
removeToTime = currentTime - 60; |
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 may be too high.
src/videojs-contrib-hls.js
Outdated
} | ||
}); | ||
if (typeof value !== 'number' || value < 0) { | ||
videojs.log.warn(`value passed to Hls.${prop} must be a positive number or 0`); |
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.
Might be easier to read as must be greater than or equal to 0
src/videojs-contrib-hls.js
Outdated
@@ -374,7 +279,8 @@ class HlsHandler extends Component { | |||
if (typeof this.options_.bandwidth !== 'number') { | |||
// only use Android for mobile because iOS does not support MSE (and uses | |||
// native HLS) | |||
this.options_.bandwidth = videojs.browser.IS_ANDROID ? 500000 : 4194304; | |||
this.options_.bandwidth = | |||
videojs.browser.IS_ANDROID ? BANDWIDTH_MOBILE : BANDWIDTH_BROADBAND; |
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.
Might be easier read/more precise as INITIAL_BANDWIDTH_MOBILE
and INITIAL_BANDWIDTH_DESKTOP
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.
You just like making staying under 90 chars per line difficult
edit: this fits perfectly
src/playlist-selectors.js
Outdated
}); | ||
|
||
const noRebufferingPlaylists = rebufferingEstimates.filter( | ||
(estimate) => estimate.rebufferingImpact < 0); |
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.
Should this be <=?
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.
Would be good to add tests for:
goalBufferLength()
bufferLowWaterLine()
firstBytesReceivedAt
added to pending segment objectminRebufferMaxBandwidthSelector
hasAttribute
estimateSegmentRequestTime
timeUntilRebuffer
assert.equal(this.env.log.warn.calls, 2, 'logged two warnings'); | ||
|
||
assert.equal(Config.GOAL_BUFFER_LENGTH, 30, 'default'); | ||
}); | ||
|
||
QUnit.test('MAX_GOAL_BUFFER_LENGTH get warning', function(assert) { |
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.
Although I'm usually for non shared tests, these are similar enough that it might be worth abstracting the test logic, and also checking the warning messages.
this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate'); | ||
assert.equal(mediaChanges.length, | ||
1, | ||
'changes media when no buffer and equal bandwidth playlist'); |
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 don't think that a change should happen when the bandwidths are equal (either by how the code works or by what we should do).
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.
It looks like it succeeds here because currentTime === 0
and initial BUFFER_LOW_WATER_LINE
is 0, so the check forwardBuffer >= bufferLowWaterLine
is true
.
this.masterPlaylistController.mainSegmentLoader_.trigger('bandwidthupdate'); | ||
assert.equal(mediaChanges.length, | ||
1, | ||
'changes media when sufficient forward buffer and higher ' + |
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.
Might be worth having another for forward buffer equal to dynamic LWL
3, | ||
'changes live media when sufficient forward buffer and higher ' + | ||
'bandwidth playlist'); | ||
}); |
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.
Would be good to have some tests for always switching when duration < LWL.
Description
This PR contains the following improvements to the ABR algorithm:
bandwidth
value of0.0625 MB/s
on Android devicesBUFFER_LOW_WATER_LINE
BUFFER_LOW_WATER_LINE
andGOAL_BUFFER_LENGTH
from0 - > 30
and30 -> 60
respectively during the first 30 seconds of playbackprogress
event shows that network conditions are not fast enough to complete the request without causing rebufferingRequirements Checklist