Skip to content
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

Remove buffered data on fast quality switches #113

Merged

Conversation

alex-barstow
Copy link
Contributor

@alex-barstow alex-barstow commented Jun 22, 2018

Description

This adds logic to remove all buffered data when a new playlist is enabled via Representation.enabled, such as with manual quality selection. Currently, the rendition switch following the enabling of a new playlist is slow due to the new segments being appended to the end of the buffer. This change comes following discussion with @gesinger, @forbesjo, and @mjneil. There was also some talk about modifying this conditional to check fetchAtBuffer_ to reduce delay before fetching new segments at the playhead. Let me know if you would like me to add that to this PR.

Specific Changes proposed

  • Rename the existing fastQualityChange_ function to smoothQualityChange_
  • Create new function called fastQualityChange_ which calls resetEverything() instead of just resetLoader()
  • Modify existing tests for the old fastQualityChange_ method to reflect the new function name
  • Add new test for the new fastQualityChange_ method

@gkatsev
Copy link
Member

gkatsev commented Jun 22, 2018

This would also fix videojs/video.js#5044, I believe.

@mjneil
Copy link
Contributor

mjneil commented Jun 22, 2018

Did we want to put this behind a feature flag (either enabled or disabled by default) so that users can keep the smooth manual switch if they want?

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

forbesjo commented Jul 5, 2018

In testing this PR it looks like there's a bit of a stutter when manually switching renditions. I'm expecting to see a loading spinner while rebuffering and smooth playback. Perhaps the stuttering happens when near the end of a segment?

Here's a gif of the behavior (BC skin)
fast-switch

@alex-barstow
Copy link
Contributor Author

@forbesjo Hm I'll into that. Interestingly, Firefox handles the switch smoothly

@alex-barstow
Copy link
Contributor Author

alex-barstow commented Jul 18, 2018

Pushed my changes. I believe the solution for the stutter is to seek in place after clearing the buffer. Chrome seems to cache a few frames from the previous rendition, and seeking in place gives the browser the "kick" it needs to clear them.

* @private
*/
fastQualityChange_() {
let media = this.selectPlaylist();
Copy link
Contributor

Choose a reason for hiding this comment

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

minor: this can be const

fastQualityChange_() {
let media = this.selectPlaylist();

if (media !== this.masterPlaylistLoader_.media()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

To save some indentation, we can change the condition to just return if (media === this.masterPlaylistLoader_.media())


// Wait for the buffer to finish clearing, then seek in place to give the
// browser a kick to remove any cached frames from the previous rendition
sourceBuffer.one('updateend', function() {
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 better to use an arrow function here (can also remove the one time hls const above and can just use this.hls_.ignoreNextSeekingEvent_ = true below, and not have to bind setCurrentTime)

Copy link
Contributor

Choose a reason for hiding this comment

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

Depending on line length, also may not need to create the const for sourceBuffer

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 goes for the currentTime-- I think it can just be this.tech_.setCurrentTime(this.tech_.currentTime());


// Wait for the buffer to finish clearing, then seek in place to give the
// browser a kick to remove any cached frames from the previous rendition
sourceBuffer.one('updateend', function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible that this updateend will occur for a different action (e.g., an append)? Is it OK for it to run then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gesinger I suppose if we are in the middle of an append operation when the quality switch is initiated it might be possible? When we get the updateend we could check the segment loader state to make sure it isn't 'APPENDING'. Thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

We might run into a timing issue if we are listening on the updateend but segment loader is also listening on updateend, as segment-loader can transition to another state before this function runs (it may not be the case right now, but to determine that MPC would need to have internal logic of the segment-loader state).

Optimally, we could avoid using the wrapped source buffer directly. It might make sense for segment-loader's resetEverything to take a callback for when it's finished. Then resetEverything can pass the callback along to source updater's remove function, and the remove function can execute it on updateend instead of a noop (as an optional parameter).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gesinger Makes sense. Went ahead and made those changes so let me know how they look.


let segmentLoader = this.masterPlaylistController.mainSegmentLoader_;

segmentLoader.resyncLoader = function() {
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 for this (and below) we can save some lines using arrows, e.g., () => resyncs++


assert.deepEqual(removeFuncArgs, {start: 0, end: 60}, 'remove() called with correct arguments');

// verify stats
Copy link
Contributor

Choose a reason for hiding this comment

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

This kinda hung on in too many tests, but I think it's OK to remove here.

});

this.masterPlaylistController.selectPlaylist = () => {
return this.masterPlaylistController.master().playlists[0];
Copy link
Contributor

Choose a reason for hiding this comment

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

We may also want a test for not resetting if the media remains the same.

this.mainSegmentLoader_.resetEverything(() => {
// we want SegmentLoader.load() to be called only once the new playlist
// has finished loading. This prevents it from being called too soon.
this.hls_.ignoreNextSeekingEvent_ = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

With the merge of #161 ignoreNextSeekingEvent_ was removed, and a new method of seeking was introduced. It's possible seekTo may be used directly and ignoreNextSeekingEvent_ removed.

Copy link
Contributor Author

@alex-barstow alex-barstow Aug 7, 2018

Choose a reason for hiding this comment

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

@gesinger Hm just calling seekTo with the flag removed is problematic since the logic in MasterPlaylistController.setCurrentTime() results in SegmentLoader.load() getting called before the new playlist has finished loading, so we append another segment from the previous rendition before appending the new quality segments. Perhaps adding an another return in MPC.setCurrentTime() if the PlaylistLoader state is 'SWITCHING_MEDIA' would be a possible solution to this? Off the top of my head I'm not sure what other consequences that could have though.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we can avoid looking at the PlaylistLoader state, that would be best. One thing that could be done here is use the normal this.tech_.setCurrentTime call directly, if we want to avoid the MasterPlaylistController.setCurrentTime() call, since seekTo is meant to add exactly that to the setCurrentTime calls. If we do use this.tech_.setCurrentTime directly though, it would be good to add a comment about why we can't go through the normal seekTo method (in this case to avoid resetting the loader, since it's a seek meant to kick things instead of actually performing a seek with buffer clearing).

@@ -582,10 +584,12 @@ export default class SegmentLoader extends videojs.EventTarget {
* Remove any data in the source buffer between start and end times
* @param {Number} start - the start time of the region to remove from the buffer
* @param {Number} end - the end time of the region to remove from the buffer
* @param {Function} done - an optional callback to be executed when the remove
Copy link
Contributor

Choose a reason for hiding this comment

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

For JSDoc optionals we can use [done]

@@ -131,14 +131,16 @@ export default class SourceUpdater {
*
* @param {Number} start where to start the removal
* @param {Number} end where to end the removal
* @param {Function} done optional callback to be executed when the remove
Copy link
Contributor

Choose a reason for hiding this comment

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

For JSDoc optionals we can use [done], or even [done=noop] here


this.masterPlaylistController.fastQualityChange_();

assert.equal(segmentLoader.ended_, false, 'segment loader ended property is false');
Copy link
Contributor

Choose a reason for hiding this comment

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

Was there a specific reason for the ended check?

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 included it since ended_ gets set to false as a side effect of calling resetEverything(). If you think it's superfluous I can remove it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Probably don't need it here. While it's a decent test, since these tests are meant for MasterPlaylistController we can probably leave checking that state change within the SegmentLoader tests.

@alex-barstow alex-barstow force-pushed the fast-quality-switch-nuclear-option branch from a23cf57 to 59c7c3c Compare August 9, 2018 17:38
@alex-barstow
Copy link
Contributor Author

Had to rebase to get the netlify deploy to succeed. @gesinger Ready for another look.

@forbesjo forbesjo merged commit bc94fbb into videojs:master Aug 13, 2018
@welcome
Copy link

welcome bot commented Aug 13, 2018

Congrats on merging your first pull request! 🎉🎉🎉

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

Successfully merging this pull request may close these issues.

5 participants