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

Make goalBufferLength and bufferLowWaterLine use time since last seek for calculation #1239

Closed
wants to merge 7 commits into from

Conversation

squarebracket
Copy link
Contributor

@squarebracket squarebracket commented Aug 21, 2017

Description

Attempt to fix the problem where the ABR will select the lowest quality on seek. It should instead pick the last quality (or something similar).

Specific Changes proposed

Instead of using the currentTime to determine the buffer goal details, use the time elapsed since the last seek (or start, if there was no seek).

Requirements Checklist

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

There's still some problems. The quality drops significantly for a couple seconds sometimes before it's kicked back to an appropriate rate. Sometimes it'll start playing and stall for a second split second then resume.

@squarebracket squarebracket force-pushed the seek-fix branch 2 times, most recently from 7f93b58 to 8b891fe Compare August 24, 2017 21:29
@squarebracket
Copy link
Contributor Author

Added cool off for a segment duration post-seek. Let me know if you want me to add any tests.

@mjneil
Copy link
Contributor

mjneil commented Aug 25, 2017

A test for the cooloff behavior and a test for elapsedSinceStart behavior would be great

@squarebracket
Copy link
Contributor Author

I added tests for the dynamic buffer stuff and elapsedSinceStart. It'll probably take me a while to figure out how to write a test for the cool-off behaviour. I saw this test but couldn't really figure out what's going on. I guess I'll see if I can find a test that uses the actual download workflow since I think that's what needed here.

On another note, is there a test for aborting when the buffer is low? i.e. like this but for buffer size? I saw a test for the logic inside minRebuffer... but not for the abort logic.

@mjneil
Copy link
Contributor

mjneil commented Aug 28, 2017

I believe XHR within the tests do not trigger progress events on their own, so you will have to do something similar to the test you linked by manually waiting clock ticks and triggering progress events when you want them. Basically what is happening in that test is mimicking the behavior of playback start, where the request is the first segment, and you basically take the loaded property divided by the number of clock ticks that have passed to get the calculated bandwidth.

So

tick(999)
loaded: 2000 // 2000bytes/999ms
tick(2)
loaded(2001) // 2001bytes/1001ms

At which point the code decides to abort early.

I would say you could use this test as the ground work for the cool-off test. Basically before the progress events for the first test, you'd want to mock seeking to true, check that its not aborted from progress events even though it would've if seeking was false, then youd want to complete the request and make seeking false again so another gets started, then show that it does not abort that one because of the cool-off. Finally I would then complete that request and use enough clock ticks so that you get past the cool-off period and show that it will abort again post cool-off if bandwidth is still low

It looks like there isn't a specific test for when there is forward buffer but it is low. The test you linked doesn't seem to mock anything with the buffer so its basically testing the already buffering case

@squarebracket
Copy link
Contributor Author

Why doesn't it trigger at the first (well, second) progress event? 2B/s is lower than all the playlist bandwidths.

@squarebracket
Copy link
Contributor Author

Oh... wait... it's 2000B/s..... nevermind. I can't math good :)

@mjneil
Copy link
Contributor

mjneil commented Aug 28, 2017

You were right the first time with 2B/s :) The reason it does not trigger right away is that it waits until it has been a full second since the first progress event before considering an abort because we've seen that progress events have an initial ramp-up period where the calculated bandwidth is very innacurate and tend to stabilize after a second

@squarebracket
Copy link
Contributor Author

Oh, my bad, I thought it was "wait until first byte", not "wait until 1 second after first byte".

@squarebracket
Copy link
Contributor Author

@mjneil you meant something like this, right? Note, I had to tick 1000ms first so that I could reach the seeking code path.

@mjneil mjneil self-assigned this Sep 19, 2017
@mjneil
Copy link
Contributor

mjneil commented Sep 19, 2017

nice test, ya thats what I meant

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.

Thanks for the PR!

I didn't review the noAbortsUntil_ component of the PR yet, as I was wondering if it would make sense as a separate PR, or is needed for the elapsed time change.

*
* @return {Number} Elapsed time
*/
elapsedSinceStart() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor, but since this is time elapsed since either start or last seek, maybe we should leave it somewhat generic and call it elapsedTime()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, if you think that's descriptive enough, I'm fine with changing it.

Copy link
Contributor

Choose a reason for hiding this comment

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

It probably is still a bit ambiguous, I may just be hooked onto it reading Time somewhere

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it's hard to think of a good name that's not super long. I'd say maybe like a timeSinceEmptyBuffer but I'm not sure that accurately encapsulates the purpose of the function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed to elapsedTime.

const currentTime = this.tech_.currentTime();
const elapsedTime = currentTime - this.startTime_;

return elapsedTime;
Copy link
Contributor

Choose a reason for hiding this comment

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

Preference, but the function could just be return this.tech_.currentTime() - this.startTime_

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, not sure why I did it like that.

@@ -118,6 +118,9 @@ export default class SegmentLoader extends videojs.EventTarget {
// ...for determining the fetch location
this.fetchAtBuffer_ = false;

// Temporary hack to not abort/downswitch post-seek
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this change make sense as part of a separate PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Consider seeking to a time which is right before a segment ends. The segment will download and be appended to the buffer, and you'll seek within that buffer to right before its end. Without this, I imagine the rate selection logic will abort the download for the higher rate since the buffer is so low.

Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't that an issue that's present without the other change?

Copy link
Contributor Author

@squarebracket squarebracket Jan 31, 2018

Choose a reason for hiding this comment

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

Ok, I went back and verified the behaviour to make sure I clearly remember what each change does. In short, the GBL change makes the time spent at a lower rate smaller, and the noAbortsUntil removes it completely. So, even though they're disjoint, without the noAbortsUntil addition, there's still a drop to the lowest quality when seeking.

I'm gonna use the example above again but go a bit more into detail about what's actually happening so you can understand what's actually changing.

Current behaviour
(playing at high rate)
-> seek to right before segment ends
-> ABR selects the lowest rate because buffer is so low
-> player buffers a few segments (6 in my case) at lowest rate
(playback resumes after 1st segment complete)
-> player starts buffering higher rate segments
(resume normal behaviour)

With GBL change
(playing at high rate)
-> seek to right before segment ends
-> ABR selects the lowest rate because buffer is so low
-> player buffers 1 segment at lowest rate
(playback resumes)
-> player starts buffering higher rate segments
(resume normal behaviour)

With noAbortsUntil
(playing at high rate)
-> seek to right before segment ends
-> ABR selects current rate because of noAbortsUntil
-> player buffers 1 segment at current rate
(playback resumes)
-> player starts buffering higher rate segments
(player stalls because it can't download the next segment fast enough)
(playback resumes when the next segment finishes downloading)
(resume normal behaviour)

Notice the tradeoff with the noAbortsUntil -- that the player stalls.

I considered the changes "omnibus" because in my mind I was trying to fix the downswitch in quality when seeking, which required the noAbortsUntil. But I have no problem separating them -- I can see why the noAbortsUntil change would be contentious, and the behaviour is still improved with just the GBL changes.

LMK what you think is best.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for all the examples. I see what you mean. It's an interesting question, and we may want more input on it. One thing we may also want to consider is we do have the minimumTimeSaving variable which is adjusted in the case where we're already rebuffering, which should count for the case of seeking.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So... is that a "yes, split them up"?

Setting minimumTimeSaving to be a segmentDuration for 2 segments after a seek would be a clever way of keeping the same rate but still aborting on too-long downloads. Of course, if you did get an abort, you'd still be kicked down to the lowest rate; the playlist selection logic will always choose the lowest rate since it'll have the least rebufferingImpact.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think the stalling issue after 1 second of playback is what we should avoid.

In my mind, the perfect case would be: you seek, we load the same quality. If it's going to take a while we can still wait, up to a point (though that "point" is not well defined).

I think the "best" answer would be to change our seeking behavior such that we only resume playback after we have enough segment data to continue playback without a stall for our current throughput and rendition. If that takes (or will take) in excess of X time, then we consider downswitching.

However, that case may be tricky to implement. In the meantime, I don't think the abort early with downswitching is a bad situation, so long as we avoid playback stalls.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the noAbortsUntil_ stuff


currentTime = 180;

assert.equal(mpc.goalBufferLength(), 60, 'dynamic GBL uses max value at 60s after seek');
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 technically not after a seek, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Care to elaborate? This test exists to make sure the dynamic GBL works as expected after a seek. I'm making the assumption in this PR that a seek will clear the buffer, so we should treat that as if we've started fresh. So here I'm seeking via setCurrentTime and testing to see that GBL responds in the same way as it does when starting from a fresh playback.

Copy link
Contributor

Choose a reason for hiding this comment

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

My mistake. I saw the seek a couple tests higher and assumed the test after had to do with the seek, but then following tests had to do with just normal playback from that point (since only currentTime changed, but not via setCurrentTime again), but you're right, it's still after a seek.


currentTime = 180;

assert.equal(mpc.bufferLowWaterLine(), 30, 'dynamic BLWL uses max value at 60s after seek');
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 technically not after a seek, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same comment as above.

@squarebracket squarebracket changed the title WIP: Fix broken quality on seek Make goalBufferLength and bufferLowWaterLine use time since last seek for calculation Feb 28, 2018
@squarebracket
Copy link
Contributor Author

Made the changes and rebased. Changes since the last review are in commit 4509642 and forwards.

@forbesjo forbesjo assigned gesinger and unassigned gesinger Aug 6, 2018
@forbesjo
Copy link
Contributor

Thank you for your PR but we will not be accepting new changes for this repo and will be archived very soon. I would advise you to open your PR against the next iteration of this project at https://github.com/videojs/http-streaming.

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

Successfully merging this pull request may close these issues.

None yet

4 participants