Skip to content
This repository has been archived by the owner on Jan 12, 2019. It is now read-only.

Abr Improvements #1176

Merged
merged 50 commits into from
Jul 6, 2017
Merged

Abr Improvements #1176

merged 50 commits into from
Jul 6, 2017

Conversation

mjneil
Copy link
Contributor

@mjneil mjneil commented Jun 21, 2017

Description

This PR contains the following improvements to the ABR algorithm:

  • Use a starting bandwidth value of 0.0625 MB/s on Android devices
  • Do not allow an up-switch in quality until a certain amount of forward buffer has been filled, BUFFER_LOW_WATER_LINE
  • Dynamically increase the BUFFER_LOW_WATER_LINE and GOAL_BUFFER_LENGTH from 0 - > 30 and 30 -> 60 respectively during the first 30 seconds of playback
  • Abort segment requests before completion if bandwidth reported by the XHR progress event shows that network conditions are not fast enough to complete the request without causing rebuffering

Requirements Checklist

  • Feature implemented / Bug fixed
  • If necessary, more likely in a feature request than a bug fix
  • Reviewed by Two Core Contributors

@imbcmdth
Copy link
Member

Good thing git can now squash and merge.. This might be the ugliest commit list in the history of videojs! 😆

buffered.end(buffered.length - 1) - this.tech_.currentTime() : 0;

const currentTime = this.tech_.currentTime();
const initial = Config.BUFFER_LOW_WATER_LINE;
Copy link
Contributor Author

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


// record the time that we receive the first byte of data
if (!segment.stats.firstByteRoundTripTime && segment.stats.bytesReceived) {
segment.stats.firstByteRoundTripTime = Date.now();
Copy link
Contributor Author

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
Copy link
Contributor Author

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

Copy link
Contributor

@gesinger gesinger left a 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.

@@ -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
Copy link
Contributor

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


const bufferLowWaterLine = this.bufferLowWaterLine();

// we want to switch down to lower resolutions quickly to continue playback, but
Copy link
Contributor

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) {

* updated to trigger a playlist switch.
*
* @param {Object} stats
* Object continaing stats about the request timing and size
Copy link
Contributor

Choose a reason for hiding this comment

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

containing

this.duration_(),
this.currentTimeline_,
currentTime);
// There is no sync point for this playlist, so switching to it will require a
Copy link
Contributor

Choose a reason for hiding this comment

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

If there is no

}

// set the bandwidth to that of the desired playlist
// (Being sure to scale by BANDWIDTH_VARIANCE and add one so the
Copy link
Contributor

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;
Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

Makes sense.

Copy link
Contributor Author

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

@@ -58,6 +58,98 @@ Object.defineProperty(Hls, 'GOAL_BUFFER_LENGTH', {
}
});

Object.defineProperty(Hls, 'MAX_GOAL_BUFFER_LENGTH', {
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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

Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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() {
Copy link
Contributor

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.

Copy link
Contributor Author

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?

let resolutionBestVariantList = [];

stableSort(sortedPlaylists, comparePlaylistBandwidth);
const simpleSelector = function(master, playerBandwidth, playerWidth, playerHeight) {
Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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?

Copy link
Contributor Author

@mjneil mjneil Jul 3, 2017

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

Copy link
Contributor

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() ||
Copy link
Contributor

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?

return;
}

if (this.abortRequestEarly_(simpleSegment.stats)) {
Copy link
Contributor

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 || [];
Copy link
Contributor

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;

@@ -58,6 +58,98 @@ Object.defineProperty(Hls, 'GOAL_BUFFER_LENGTH', {
}
});

Object.defineProperty(Hls, 'MAX_GOAL_BUFFER_LENGTH', {
Copy link
Contributor

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.

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;
Copy link
Contributor

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.

playbackRate: () => this.playbackRate
}
};
this.goalBufferLength = () => {
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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 ticks that have passed since the request has been made is what is being used for the estimated bandwidth

Copy link
Contributor

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({
Copy link
Contributor

Choose a reason for hiding this comment

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

😎

Copy link
Contributor

Choose a reason for hiding this comment

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

😎


const size = segmentDuration * playlist.attributes.BANDWIDTH;

return (size - (bytesReceived * 8)) / bandwidth;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be bitsReceived

Copy link
Contributor Author

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({
Copy link
Contributor

Choose a reason for hiding this comment

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

😎

@@ -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;
Copy link
Contributor

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.

}
});
if (typeof value !== 'number' || value < 0) {
videojs.log.warn(`value passed to Hls.${prop} must be a positive number or 0`);
Copy link
Contributor

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

@@ -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;
Copy link
Contributor

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

Copy link
Contributor Author

@mjneil mjneil Jul 3, 2017

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

});

const noRebufferingPlaylists = rebufferingEstimates.filter(
(estimate) => estimate.rebufferingImpact < 0);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be <=?

@forbesjo forbesjo self-assigned this Jul 5, 2017
Copy link
Contributor

@gesinger gesinger left a 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 object
  • minRebufferMaxBandwidthSelector
  • 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) {
Copy link
Contributor

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');
Copy link
Contributor

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).

Copy link
Contributor Author

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 ' +
Copy link
Contributor

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');
});
Copy link
Contributor

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants