Skip to content

Improved seek behaviour#370

Closed
squarebracket wants to merge 5 commits into
videojs:masterfrom
squarebracket:feature/improved-seek-behaviour
Closed

Improved seek behaviour#370
squarebracket wants to merge 5 commits into
videojs:masterfrom
squarebracket:feature/improved-seek-behaviour

Conversation

@squarebracket

@squarebracket squarebracket commented Jan 11, 2019

Copy link
Copy Markdown
Contributor

Description

This change requires videojs/video.js#5024, which takes care of the UI and API aspects of buffering.

SourceHandlers that use MSE have a problem: if they push a segment into a SourceBuffer and then seek close to the end, playback will stall and/or there will be a massive downswitch in quality. The general approach to fixing this that was discussed on slack was by setting the playback rate of the player to zero, buffering all that was required, and then restoring the previous playback rate. In my implementation, I've done this in the source handler.

Additionally, there is a new seekDeadline option for the source handler which controls how long a seek should take to resume. When seeking, the master playlist controller calculates the maximum bandwidth playlist for all requests needed to complete a full seek. The calculations are done using maxBandwidthForDeadlineSelector, which is essentially just minRebufferMaxBandwidthSelector renamed.

Taken together, this makes the seeking behavior vastly improved:

  • playback is on highest rate
  • seek is requested by client to right before the end of a segment
  • currentTime of video is updated, playback pauses, spinner appears, player.seeking() is now true
  • buffer is sufficiently filled at new time in reasonable timeframe, downswitching if necessary
  • two segments at lower rate are now in buffer
  • playback resumes, spinner disappears, player fires playback events, player.seeking() is now false
  • next segment download is at seeking rate (i.e. the rate calculated when making the seek)
  • pre-seek quality is resumed bufferLowWaterLine seconds after resuming playback

Specific Changes proposed

  • Added seekDeadline option for controlling how long a seek should take to resume
  • seeking() is deferred from the tech to the SourceHandler
  • setCurrentTime stores the current playback rate and sets the tech's playback rate to zero
  • Inside SegmentLoader#handleUpdateEnd_, if we have enough buffer that we can make another request in that time, fire buffered on this
  • The MasterPlaylistController receives the buffered event and restores the previous playback rate
  • minRebufferMaxBandwidthSelector has been renamed to maxBandwidthForDeadlineSelector, and made more generic
    • it now accepts a needsExtraRequest callback which is used to determine whether another request must be made
    • instead of returning rebufferingImpact, it now just returns requestTimeEstimate
  • For the deadline passed to maxBandwidthForDeadlineSelector, abortRequestEarly_ uses either the current calculation (when not seeking) or the time elapsed since seeking started
  • Math inside abortRequestEarly_ is now a bit simpler because it's now using the requestTimeEstimate instead of rebufferingImpact
  • The quality used when seeking is determined by how long it will take to get the number of segments needed to fill the buffer after the seek
  • The MPC's fastQualityChange_ and smoothQualityChange_ now accept a nextPlaylist option which will be used instead of this.selectPlaylist() when given
  • The playback watcher has been modified to not bork when the playback rate is zero and nothing is happening

Requirements Checklist

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

Notes

  • This is implemented assuming that the video segment loader will always finish after the other segment loaders. I also made an implementation that guarantees all segment loaders have enough data, if you want to compare what that would be like. I found this to be much more concise which is why it gets the PR.
  • Unfortunately, as it turns out, Make goalBufferLength and bufferLowWaterLine use time since last seek for calculation videojs-contrib-hls#1239 can't be added to this PR. The goal of that PR was to upswitch immediately post-seek, so that quality would dip only for those segments needed to fill the buffer post-seek. However, since the same segment # can be downloaded twice while upswitching (once at the old rate, once at the new rate), the buffer will sometimes run out post-seek. So, for now, it will take bufferLowWaterLine seconds post-seek before an upswitch is made to the pre-seek quality. Eventually I'd like to fix this, but I think for now it's not super important.
  • Because of the way things are being done with the playbackRate, after the first segment is downloaded, the video frame changes to be the frame at currentTime while the second segment is downloaded. This is kinda weird from a UI point of view, but I think it's acceptable.

@squarebracket

Copy link
Copy Markdown
Contributor Author

I removed the Chrome workaround since it was added to video.js proper in videojs/video.js#5533. I feel that the same should be done for the IE/Edge workaround as well.

@squarebracket squarebracket changed the title port improved seek behaviour Improved seek behaviour Jan 11, 2019
@squarebracket

Copy link
Copy Markdown
Contributor Author

Also, let me know if you think I missed any necessary tests.

@squarebracket squarebracket mentioned this pull request Feb 22, 2019
@stale

stale Bot commented Mar 12, 2019

Copy link
Copy Markdown

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale Bot added the outdated label Mar 12, 2019
@squarebracket

Copy link
Copy Markdown
Contributor Author

ಠ_ಠ

@stale stale Bot removed the outdated label Mar 12, 2019
@gkatsev

gkatsev commented Mar 22, 2019

Copy link
Copy Markdown
Member

This needs a rebase and otherwise is good to go.

@squarebracket squarebracket force-pushed the feature/improved-seek-behaviour branch from 3386f66 to 4b521b7 Compare March 26, 2019 15:30

@squarebracket squarebracket left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rebased to master. I haven't yet tested post-rebase but it's ready for review.

// before we can download the next segment. when we're rebuffering,
// there should be a spinner over the video, but this requires a bit
// of finagling for some browsers.
if (videojs.browser.IS_EDGE || videojs.browser.IE_VERSION) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

IMO this should be taken care of in video.js. Thoughts?

Comment thread src/segment-loader.js
let buffered = this.buffered_();

if (isEndOfStream ||
(buffered.end(0) - this.currentTime_()) >= (this.roundTrip / 1000) + 1) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Do I need to make sure buffered.length > 0 here?

@squarebracket

squarebracket commented Mar 29, 2019

Copy link
Copy Markdown
Contributor Author

I have run into a problem with this patch when running my "load the same live source 100 times" test.

Without this patch, this is what happens when loading a live stream:

  • Select playlist x
  • Seek to end of seekable
  • Load segment n from x
  • Load segment n+1 from x

With this patch, this changes to the following:

  • Select playlist x
  • Seek to end of seekable; downswitches to playlist y to meet seek deadline
  • Load segment n from x because we don't have the new playlist yet
  • New playlist finishes
  • Load segment n+1 from y

This usually works, but sometimes n+1 is actually 2 segments after n, in that there ends up being a gap in the buffer of 1 target duration. When this occurs, playback doesn't start, since we're seeking to the end of a buffer, which is immediately followed by a gap. The way the call flow works during initial start/playback, mediachanging fires before load() called is on the main segment loader, so this logic doesn't protect the segment loader:

this.masterPlaylistLoader_.on('mediachanging', () => {
this.mainSegmentLoader_.abort();
this.mainSegmentLoader_.pause();
});

Instead, segment n finishes after switching to the new playlist and sets the mediaIndex, so the segment loader doesn't realize it has to make a sync request.

I've fixed this by simply gating downswitching during seek to this.hasPlayed_(). This way the initial seek won't change the current playlist. That being said, it made me wonder if there are other conditions where we might hit such a problem.

@squarebracket squarebracket mentioned this pull request May 2, 2019
6 tasks
@stale

stale Bot commented Jul 1, 2019

Copy link
Copy Markdown

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale Bot added the outdated label Jul 1, 2019
@stale stale Bot closed this Jul 8, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants