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

Queue playback events when the playback rate is zero #5024

Conversation

squarebracket
Copy link
Contributor

@squarebracket squarebracket commented Mar 15, 2018

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 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)
  • Reviewed by Two Core Contributors

@asd29696
Copy link

asd29696 commented Mar 15, 2018 via email

Copy link
Member

@gkatsev gkatsev 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 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_);
Copy link
Member

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

Copy link
Contributor Author

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

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]}_`]();
Copy link
Member

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

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.

Copy link
Contributor Author

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.

Copy link
Member

@gkatsev gkatsev Mar 16, 2018

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:

video.js/src/js/tech/html5.js

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',

video.js/src/js/tech/html5.js

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?

video.js/src/js/tech/tech.js

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.

Copy link
Contributor Author

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?

Copy link
Member

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?

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

Copy link
Member

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.

  1. the tech and SH aren't supposed to have a reference to the player, so, they can't trigger an event on it.
  2. 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.

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, I was talking about removing this.trigger to avoid the event loop, but you got the gist of what I was going for.

Copy link
Member

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.

@gkatsev
Copy link
Member

gkatsev commented Mar 19, 2018

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.

@squarebracket
Copy link
Contributor Author

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.

@squarebracket squarebracket changed the title WIP: Queue playback events when the playback rate is zero Queue playback events when the playback rate is zero Mar 26, 2018
@@ -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) {
Copy link
Member

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?

Copy link
Contributor Author

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

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

Copy link
Contributor Author

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.

@gkatsev
Copy link
Member

gkatsev commented Mar 29, 2018

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

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_)?

Copy link
Member

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.

Copy link
Contributor Author

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

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.

Copy link
Member

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.

video.js/src/js/player.js

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;
}

@squarebracket
Copy link
Contributor Author

LMK if there's somewhere in the docs these changes should be reflected.

@gkatsev
Copy link
Member

gkatsev commented Apr 4, 2018

It doesn't seem like we have any docs for source handlers, so, not really any place to put it currently.

Copy link
Member

@gkatsev gkatsev left a comment

Choose a reason for hiding this comment

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

👍

@gkatsev
Copy link
Member

gkatsev commented Apr 4, 2018

@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?

@squarebracket
Copy link
Contributor Author

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 tech.supportsProperSeek or some such thing.

@gkatsev
Copy link
Member

gkatsev commented Apr 4, 2018

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.

@squarebracket
Copy link
Contributor Author

Care to elaborate what you mean by making the choice explicitly?

@gkatsev
Copy link
Member

gkatsev commented Apr 5, 2018

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.

@forbesjo
Copy link
Contributor

forbesjo commented Apr 5, 2018

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.

@gkatsev gkatsev merged this pull request into videojs:master Apr 17, 2018
gkatsev pushed a commit that referenced this pull request Apr 17, 2018
… 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.
gkatsev pushed a commit that referenced this pull request Apr 17, 2018
… 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.
@gkatsev
Copy link
Member

gkatsev commented Apr 17, 2018

Thanks @squarebracket!

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.

6 participants