-
Notifications
You must be signed in to change notification settings - Fork 7.5k
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
Queue playback events when the playback rate is zero #5024
Queue playback events when the playback rate is zero #5024
Conversation
2018-03-16 6:54 GMT+08:00 Chuck Wilson <[email protected]>:
… Description
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.
From the video.js perspective, it should ensure that the UI reflects the
buffering status and that the player API behaves like you'd expect -- that
is to say, that it will fire seeking immediately after a call to
currentTime, and it will fire seeked, canplay, canplaythrough, and playing
when everything is buffered.
Specific Changes proposed
- When the playback rate of the tech is zero, queue up callbacks for
the seeked, canplay, canplaythrough, and playing events
- When the playback rate is changed to something non-zero and its
previous playback rate was zero, fire the queued callbacks
Requirements Checklist
- Feature implemented / Bug fixed
- If necessary, more likely in a feature request than a bug fix
- Change has been verified in an actual browser (Chome, Firefox, IE)
- Unit Tests updated or fixed
- Docs/guides updated
- Example created (starter template on JSBin
<http://jsbin.com/axedog/edit?html,output>)
- Reviewed by Two Core Contributors
Missing
I still need to edit/add tests and docs, but I'd like to get some opinions
on this approach first.
------------------------------
You can view, comment on, or merge this pull request online at:
#5024
Commit Summary
- Queue playback events when the playback rate is zero
File Changes
- *M* src/js/player.js
<https://github.com/videojs/video.js/pull/5024/files#diff-0> (37)
Patch Links:
- https://github.com/videojs/video.js/pull/5024.patch
- https://github.com/videojs/video.js/pull/5024.diff
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#5024>, or mute the thread
<https://github.com/notifications/unsubscribe-auth/AeI8rvH8ryBWZC1274OAo_ExrtfdbsmJks5tevEggaJpZM4StAPQ>
.
|
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.
Thanks for starting this @squarebracket. Made some comments.
@@ -985,6 +1002,7 @@ class Player extends Component { | |||
this.on(this.tech_, 'loadedmetadata', this.updateStyleEl_); | |||
this.on(this.tech_, 'posterchange', this.handleTechPosterChange_); | |||
this.on(this.tech_, 'textdata', this.handleTechTextData_); | |||
this.on(this.tech_, 'ratechange', this.handleTechRateChange_); |
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 we want to run special handling, it needs to be removed from https://github.com/videojs/video.js/blob/master/src/js/player.js#L196 and the documentation needs to be moved to the handleTechRateChange_.
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.
Moved the documentation.
src/js/player.js
Outdated
Object.keys(TECH_EVENTS_QUEUE).forEach((event) => { | ||
this.on(this.tech_, event, () => { | ||
if (this.tech_.playbackRate() === 0) { | ||
this.queuedCallbacks_.push(this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this)); |
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.
the event object should be passed through as well.
src/js/player.js
Outdated
this.queuedCallbacks_.push(this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this)); | ||
return; | ||
} | ||
this[`handleTech${TECH_EVENTS_QUEUE[event]}_`](); |
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.
event object should be passed through as well.
src/js/player.js
Outdated
|
||
Object.keys(TECH_EVENTS_QUEUE).forEach((event) => { | ||
this.on(this.tech_, event, () => { | ||
if (this.tech_.playbackRate() === 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.
I think this would be a breaking change. Potentially, doing it when isSeeking()
is true and playbackRate
is 0
, might be OK.
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 you elaborate what would/could be breaking? A tech or source handler would have to both set the playback rate to zero and rely on these events being dispatched by the player (the tech itself will still dispatch them) right after the seek. Perhaps there's some use case I'm not thinking of.
I don't think that would work. Correct me if I'm wrong but I think seeking()
is only true between the seeking
and seeked
events, and this needs to queue everything after and including the seeked
event.
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.
Because people could still expect these events even when playbackRate
is 0
. Granted, this is veery unlikely but I would still worry about making a change like that. Would be much nicer to section it off only for seeking()
.
Maybe we need to allow techs/source handlers dictate when seeking is happening? Looks like seeking()
method just asks the video element directly:
Lines 1552 to 1564 in d7f45ba
/** | |
* Get the value of `seeking` from the media element. `seeking` indicates whether the | |
* media is currently seeking to a new position or not. | |
* | |
* @method Html5#seeking | |
* @return {boolean} | |
* - The value of `seeking` from the media element. | |
* - True indicates that the media is currently seeking to a new position. | |
* - Flase indicates that the media is not seeking to a new position at this time. | |
* | |
* @see [Spec]{@link https://www.w3.org/TR/html5/embedded-content-0.html#dom-media-seeking} | |
*/ | |
'seeking', |
Lines 1700 to 1704 in d7f45ba
].forEach(function(prop) { | |
Html5.prototype[prop] = function() { | |
return this.el_[prop]; | |
}; | |
}); |
but we could modify it so that sourcehandlers can handle it like duration
and seekable
?
Lines 1166 to 1173 in d7f45ba
/** | |
* When using a source handler, prefer its implementation of | |
* any function normally provided by the tech. | |
*/ | |
const deferrable = [ | |
'seekable', | |
'duration' | |
]; |
that way, we could gate it on player.seekable()
and so any potential breakage would be minimal.
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.
Looks like seeking() method just asks the video element directly
Yeah, that's why I said it wouldn't work. I like the idea of deferring seeking()
to the source handlers. In fact, that should probably be done anyway to make the API correct -- i.e. you'd expect seeking()
to be true up to the point a seeked
is fired but that's not the case with the current code. I'll make that change and gate the event queueing to it.
Any other APIs you can think of that might need to be deferred to spoof things properly?
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.
Not sure, but we've been deferring them as needed, we just haven't really needed any others until now, apparently.
Do you think that deferring to SH and then only queueing the events if seeking()
works?
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.
Yeah, that should work.
If you want to go with override-only behaviour, there's potentially another possibility. We could rejigger player.js so that most (all?) of the event handlers are dispatched on the player event rather than the tech event. And then have something like tech.delegateEvent(event)
. An undelegated event would then just be retriggered on the player, whereas a delegated event would be triggered on a source handler -- and expected to eventually be fired on the player manually be the source handler. I did a quick test and it worked as expected, but you obviously have a better idea about the bigger implications of such a change.
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 that deferring seeking()
makes sense in this case.
I'm not sure I follow this 100%. If I understand correctly, the player would not listen for tech events but rather the tech would trigger the events directly on the player when necessary? If that is the case, I see two issues.
- the tech and SH aren't supposed to have a reference to the player, so, they can't trigger an event on it.
- triggering the event on the player directly would mean that the player doesn't necessarily have a chance to do something related to the event before others would receive the event, plus, if the player would to re-trigger the event, it would end up in an event loop.
This also does lend itself a bit to what middleware strives to be and we've been thinking/wanting to have events be able to be intercepted by mw as well, so, maybe we can do that eventually. I think that deferring seeking()
is the quick solution and a decent solution to this.
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.
Yeah, I was talking about removing this.trigger
to avoid the event loop, but you got the gist of what I was going for.
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.
Cool. Yeah, I think that's a bit more complicated and would require a lot of though to get it right.
Also, this probably needs to be changed to be a PR against 6.x, master will soon be a 7.x-only. Or you can leave it against master and we can backport it later on. |
I'll have to open the PR against any VJS branch that contrib-hls/VHS supports, since the PR there will require the change in video.js. |
test/unit/player.test.js
Outdated
@@ -1364,6 +1364,49 @@ QUnit.test('Remove waiting class on timeupdate after tech waiting', function(ass | |||
player.dispose(); | |||
}); | |||
|
|||
QUnit.test('Queues playing events when playback rate is zero', 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.
should we have a test to verify that events aren't queued if the playbackRate was changed and we're not seeking?
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 as well. Added tests.
src/js/player.js
Outdated
if (this.tech_.playbackRate() === 0 && this.tech_.seeking()) { | ||
this.queuedCallbacks_.push({ | ||
callback: this[`handleTech${TECH_EVENTS_QUEUE[event]}_`].bind(this), | ||
event |
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.
the event that we want to cache is actually event object that's passed to the handler (the argument that would be passed into this arrow function), not the event name
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.
Woops! That one slipped right by me, thanks for catching.
Just tested it, works great. Brings us one step closer to natively seeking via playbackrate 0, too |
src/js/player.js
Outdated
this.queuedCallbacks_.forEach((queued) => queued.callback(queued.event)); | ||
this.queuedCallbacks_ = []; | ||
} | ||
this.previousPlaybackRate = this.tech_.playbackRate(); |
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 pseudo-private (i.e. previousPlaybackRate_
)?
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.
totally missed that, yes, it should. Could even potentially be stored on the cache_ object as well/instead.
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.
Changed to this.cache_.lastPlaybackRate
@@ -389,6 +387,9 @@ class Player extends Component { | |||
|
|||
this.el_ = this.createEl(); | |||
|
|||
// Set default value for lastPlaybackRate | |||
this.cache_.lastPlaybackRate = this.defaultPlaybackRate(); |
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.
Is it appropriate to use defaultPlaybackRate
here? i.e. will everything be set up properly at this point? Since the element exists, I made the assumption that it would.
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.
some techs, namely flash, don't support playbackrate. However, if it isn't supported or the tech isn't ready, it'll just return the default value of 1.0, so, this is probably fine.
Lines 3090 to 3099 in 4e79a04
defaultPlaybackRate(rate) { | |
if (rate !== undefined) { | |
return this.techCall_('setDefaultPlaybackRate', rate); | |
} | |
if (this.tech_ && this.tech_.featuresPlaybackRate) { | |
return this.techGet_('defaultPlaybackRate'); | |
} | |
return 1.0; | |
} |
LMK if there's somewhere in the docs these changes should be reflected. |
It doesn't seem like we have any docs for source handlers, so, not really any place to put it currently. |
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.
👍
@forbesjo brought up that when contrib-hls is updated with this, what happens if a user is using a version of Video.js without this change? Would this make the contrib-hls update a breaking change or can we make the contrib-hls backwards compatible? |
I figured it would require a bump in the dependency version. So we'd wait until this change is merged in and released, and then the changes for contrib-hls would be merged in with a change to its package.json. Is that a problem? I imagine that if you're updating contrib-hls, it's probably not that big of a deal to update video.js as well. Otherwise we'll have to do some feature detection. A |
it means that the minimum supported version of video.js for contrib-hls is increased, which is probably a breaking change. It's probably fine to do, but we'd want to make this choice explicitly. |
Care to elaborate what you mean by making the choice explicitly? |
As in, we should decide what to do in this case, from making a breaking change on contrib-hls, or just upping the minimum requires video.js version in a minor, to making contrib-hls work with or without this change (with degraded behavior without this change). What we shouldn't do is just merge and release and let the cards fall where they may. Though, choosing that could be an option too. |
Upon further discussion we'll make it very clear in the contrib-hls README and package.json which version of video.js this change is compatible with. |
… seeking (#5024) 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 (see: videojs/videojs-contrib-hls#1374). From the video.js perspective, it should ensure that the UI reflects the buffering status and that the player API behaves like you'd expect -- that is to say, that it will fire seeking immediately after a call to currentTime, and it will fire seeked, canplay, canplaythrough, and playing when everything is buffered.
… seeking (#5061) 6.x version of #5024 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 (see: videojs/videojs-contrib-hls#1374). From the video.js perspective, it should ensure that the UI reflects the buffering status and that the player API behaves like you'd expect -- that is to say, that it will fire seeking immediately after a call to currentTime, and it will fire seeked, canplay, canplaythrough, and playing when everything is buffered.
Thanks @squarebracket! |
Description
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 (see: videojs/videojs-contrib-hls#1374).From the video.js perspective, it should ensure that the UI reflects the buffering status and that the player API behaves like you'd expect -- that is to say, that it will fire
seeking
immediately after a call tocurrentTime
, and it will fireseeked
,canplay
,canplaythrough
, andplaying
when everything is buffered.Specific Changes proposed
seeked
,canplay
,canplaythrough
, andplaying
eventsRequirements Checklist