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

fix: Fix cases where the player reports that it is ready during source selection, but before tech selection. #4665

Closed
wants to merge 5 commits into from

Conversation

misteroneill
Copy link
Member

@misteroneill misteroneill commented Oct 16, 2017

In working on some issues with asynchronous middleware and playlists, I realized that there is a period of time during the source and tech setting process where a new source and tech have not been selected, but the player still considers itself "ready".

The effect of this is to cause any callbacks to ready() during source selection to be called immediately instead of waiting for the source/tech to be selected:

player.src({src: 'foo.mp4', type: 'video/mp4'});
player.ready(() => {
  player.play();
});

I think most people would expect, in that case, foo.mp4 to begin playback. However, if there is asynchronous middleware happening, what you may get is some playback of the current source and then a source change to foo.mp4 (which will pause the player).

The essence of this change is to separate the player's readiness from the tech's readiness. The tech can be ready when the player is not because the player is still resolving middleware, etc.

This should be backward-compatible and make ready() more reliable and better match user expectations.

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

src/js/player.js Outdated
});
// Given a return value for a tech's `play()` method, silences "unhandled
// promise from play" errors.
const silencePromise = (value) => {
Copy link
Member

Choose a reason for hiding this comment

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

we probably want a promise helper file that contains isPromise and silencePromise, since we silence promises in a few places.

src/js/player.js Outdated
// Finally, if the player is ready, but we don't have a source, wait for
// one to be set.
} else {
this.ready(() => this.tech_.one('loadstart', () => silencePromise(this.play())));
Copy link
Member

Choose a reason for hiding this comment

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

This will fail because the function gets called with the context of the player and not the tech as relied on previously.

Copy link
Member Author

Choose a reason for hiding this comment

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

D'oh.

src/js/player.js Outdated
// we could not find an appropriate tech, but let's still notify the delegate that this is it
// this needs a better comment about why this is needed
// we could not find an appropriate tech, but let's still notify the
// delegate that this is it this needs a better comment about why this
Copy link
Member

Choose a reason for hiding this comment

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

can you revert this change or put a new line in? Otherwise the two statements run in together.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, yes, it is hard to distinguish sentences without capitals and punctuation. ;)

@@ -574,7 +574,7 @@ if (Html5.isSupported()) {
player.addRemoteTextTrack(track, false);
player.src({src: 'example.mp4', type: 'video/mp4'});

this.clock.tick(1);
this.clock.tick(2);
Copy link
Member Author

Choose a reason for hiding this comment

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

Calling src() now results in two async functions being queued up in sequence. First, middleware.setSource runs async. Then, its callback invokes this.triggerReady(), which queues up a flush of the ready queue.

That's why this changed from 1 to 2.

Copy link
Contributor

Choose a reason for hiding this comment

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

Im just wondering if this is going to break plugin tests, I don't really see an issue other that that.

Copy link
Member Author

Choose a reason for hiding this comment

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

It might.

loadTech_() {
loadTechCalled++;
}
};

Player.prototype.src_.call(playerProxy);
Player.prototype.src_.call(playerProxy, {});
Copy link
Member Author

Choose a reason for hiding this comment

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

The changes to this test were necessary because src_() no longer calls ready(). Instead, it immediately loads the source into the tech because the only cases where this is possible is if the tech did not change; so, it should already be ready.

// middlewareSource is the source after it has been changed by middleware
// Set the player to a non-ready state while middleware asynchronously
// determines the final source.
this.isReady_ = false;
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the key fix: the player should not be "ready" while middleware is resolving a source and deciding if it needs to load a new tech or set the resolved source on the existing tech.

@@ -574,7 +574,7 @@ if (Html5.isSupported()) {
player.addRemoteTextTrack(track, false);
player.src({src: 'example.mp4', type: 'video/mp4'});

this.clock.tick(1);
this.clock.tick(2);
Copy link
Contributor

Choose a reason for hiding this comment

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

Im just wondering if this is going to break plugin tests, I don't really see an issue other that that.

*/
play() {
if (this.changingSrc_) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Having this function sometimes return a promise just sucks, not really something we can/should do about it in this PR though.

Copy link
Member Author

Choose a reason for hiding this comment

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

I dunno. I mean, it does model the native play method. The alternative would be to include a Promise polyfill.

Copy link
Member

Choose a reason for hiding this comment

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

I do think making a Promise is what we'd want to do eventually. Though, we'd probably ask users to provide a polyfill rather than include it in video.js. Then if Promises exists or was passed in we'll make one.
Either way, outside the scope of this PR :)

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.

Would be great to make a test for the originally breaking condition.

@gkatsev
Copy link
Member

gkatsev commented Oct 18, 2017

@videojs/core-committers any thoughts or comments regarding this change?

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.

Changes seem good to go. Should be tested first, though.

@gkatsev
Copy link
Member

gkatsev commented Oct 25, 2017

removing confirmed until this is tested so we don't merge it in early.

@gkatsev
Copy link
Member

gkatsev commented Oct 31, 2017

I just tested this with videojs-playlist and autoadvance is broken with this but works with the released version 6.3.3. We might want to re think about this taking the sourceset PR into account.

@alex-barstow
Copy link
Contributor

@gkatsev I saw the same playlist behavior when re-testing to see if #4639 is still necessary. Autoadvance didnt work unless I wrapped silencePromise(this.techGet_('play')) in a timeout of some arbitrary amount (can't recall exact wait time), so perhaps the player is still reporting it is ready too early.

@misteroneill
Copy link
Member Author

@alex-barstow is correct. What's happening is that we're calling triggerReady when Video.js has updated its source, but the tech/video element still has the old source.

@misteroneill
Copy link
Member Author

I'm not sure this is going to work, but I'm going to gather some thoughts here, partly to get my head on straight.

The current and previous assumption within Video.js has been that the player's ready state is identical to the tech's ready state.

This PR aims to adjust that assumption such that the tech could be ready, but the player may still be resolving a source asynchronously via middleware; so, the player should not be ready.

Why? Because, I think it's fair to assume as a user of Video.js that if I queue a ready callback on a player, I should be able to interact with it once that callback fires (e.g., calling play()). This is not currently the case.

However, the src_ function uses techCall_ to set the source, which relies on the tech/player being in a ready state. This leads to a chicken or egg situation that's not easy to solve. Changing techCall_ to use tech_.ready instead causes a raft of weird side effects, such as... video not playing. 😆

I'm going to spend some time today tinkering with this, but it may not be possible to work around without breaking something.

@misteroneill
Copy link
Member Author

Alright, turns out most of the issues with changing techCall_ to wait for the tech, specifically, to be ready, were very minor.

// At this point, we've found a compatible tech and source and set the
// source both on the tech and in the internal cache. It's safe to tell
// any subscribers that the player is ready again.
this.triggerReady();
Copy link
Member

Choose a reason for hiding this comment

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

do we even need to do this? Should we be getting ready from the tech for the source change in the first place?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, the whole idea here is that tech ready != player ready - this is the latter. The tech is ready before the source change is complete. Maybe I'm not following the line of questions.

Copy link
Member

@gkatsev gkatsev Nov 6, 2017

Choose a reason for hiding this comment

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

Well, when the player gets the tech's ready event, it already triggers ready itself:

video.js/src/js/player.js

Lines 907 to 908 in c7ad7b3

// player.triggerReady is always async, so don't need this to be async
this.tech_.ready(Fn.bind(this, this.handleTechReady_), true);

video.js/src/js/player.js

Lines 1055 to 1061 in c7ad7b3

/**
* Player waits for the tech to be ready
*
* @private
*/
handleTechReady_() {
this.triggerReady();

So, we would end up getting two ready events for the same source unless we change the way that handleTechReady functions.

src/js/player.js Outdated
@@ -2324,10 +2330,10 @@ class Player extends Component {
this.middleware_ = mws;

// Attempt to set the source on a tech.
const foundTech = this.src_(middlewareSource);
const didNotFindTech = this.src_(middlewareSource);
Copy link
Member

Choose a reason for hiding this comment

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

that is a better name

@misteroneill
Copy link
Member Author

@dmlap brought up an interesting question related to this. If you are using on('ready') will you get more ready events than you expect?

I wondered more about this possibly being a subtle breaking change and whether the alternative approach should be to suggest waiting for canplay before calling play(), leaving the ready as a tech crutch...

player.src({src: 'foo.mp4', type: 'video/mp4'});
player.ready(() => {
  player.one('canplay', () => player.play());
});

@gkatsev
Copy link
Member

gkatsev commented Nov 7, 2017

@misteroneill sounds like @dmlap and I had similar questions though worded differently.

@misteroneill
Copy link
Member Author

I wonder if a better fix is just to have play() wait for loadstart if it's changing sources?

@misteroneill
Copy link
Member Author

I don't think this is the right path forward. I'm going back to the drawing board.

@misteroneill misteroneill deleted the r-u-ready branch November 13, 2017 17:15
gkatsev pushed a commit that referenced this pull request Nov 16, 2017
…instead of just ready. (#4743)

The core goal here is to make sure the following works in light of some middleware process that makes setting the source more async than next tick:

```js
player.src('...');
player.ready(() => player.play());
```

In fact, given this change, we should even be able to do:

```js
player.src('...');
player.play();
```

Unlike #4665, which would have clarified/changed the meaning of "ready", it remains a reflection of the tech's state and we make better use of the ability to queue things on that state and on the middleware `setSource` process.
misteroneill added a commit to videojs/generator-videojs-plugin that referenced this pull request Nov 27, 2017
misteroneill added a commit to videojs/generator-videojs-plugin that referenced this pull request Nov 27, 2017
brandonocasey added a commit to videojs/tooling that referenced this pull request Jun 3, 2019
brandonocasey pushed a commit to videojs/tooling that referenced this pull request Jun 3, 2019
…#4665 (#136) (#141)

This reverts commit 87f0d08119a825098e944e37a20d95596306eae2.
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.

4 participants