-
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
fix: Fix cases where the player reports that it is ready during source selection, but before tech selection. #4665
Changes from all commits
364a2a8
fa66837
9a99f37
3997cec
3fddd8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -24,6 +24,7 @@ import MediaError from './media-error.js'; | |||||||||||||||||||
import safeParseTuple from 'safe-json-parse/tuple'; | ||||||||||||||||||||
import {assign} from './utils/obj'; | ||||||||||||||||||||
import mergeOptions from './utils/merge-options.js'; | ||||||||||||||||||||
import {silencePromise} from './utils/promise'; | ||||||||||||||||||||
import textTrackConverter from './tracks/text-track-list-converter.js'; | ||||||||||||||||||||
import ModalDialog from './modal-dialog'; | ||||||||||||||||||||
import Tech from './tech/tech.js'; | ||||||||||||||||||||
|
@@ -459,8 +460,6 @@ class Player extends Component { | |||||||||||||||||||
|
||||||||||||||||||||
this.on('fullscreenchange', this.handleFullscreenChange_); | ||||||||||||||||||||
this.on('stageclick', this.handleStageClick_); | ||||||||||||||||||||
|
||||||||||||||||||||
this.changingSrc_ = false; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
|
@@ -1547,27 +1546,27 @@ class Player extends Component { | |||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Pass values to the playback tech | ||||||||||||||||||||
* Call a method on the playback tech with an optional argument. | ||||||||||||||||||||
* | ||||||||||||||||||||
* Waits for the tech to be ready before executing the call. | ||||||||||||||||||||
* | ||||||||||||||||||||
* @param {string} [method] | ||||||||||||||||||||
* the method to call | ||||||||||||||||||||
* @param {string} method | ||||||||||||||||||||
* The name of the method to call on the tech. | ||||||||||||||||||||
* | ||||||||||||||||||||
* @param {Object} arg | ||||||||||||||||||||
* the argument to pass | ||||||||||||||||||||
* @param {Object} [arg] | ||||||||||||||||||||
* An argument to pass to the tech. | ||||||||||||||||||||
* | ||||||||||||||||||||
* @private | ||||||||||||||||||||
*/ | ||||||||||||||||||||
techCall_(method, arg) { | ||||||||||||||||||||
// If it's not ready yet, call method when it is | ||||||||||||||||||||
|
||||||||||||||||||||
this.ready(function() { | ||||||||||||||||||||
this.tech_.ready(() => { | ||||||||||||||||||||
if (method in middleware.allowedSetters) { | ||||||||||||||||||||
return middleware.set(this.middleware_, this.tech_, method, arg); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
try { | ||||||||||||||||||||
if (this.tech_) { | ||||||||||||||||||||
this.tech_[method](arg); | ||||||||||||||||||||
return this.tech_[method](arg); | ||||||||||||||||||||
} | ||||||||||||||||||||
} catch (e) { | ||||||||||||||||||||
log(e); | ||||||||||||||||||||
|
@@ -1577,13 +1576,14 @@ class Player extends Component { | |||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* Get calls can't wait for the tech, and sometimes don't need to. | ||||||||||||||||||||
* Get the return value for a method call on the playback tech. | ||||||||||||||||||||
* | ||||||||||||||||||||
* @param {string} method | ||||||||||||||||||||
* Tech method | ||||||||||||||||||||
* @param {string} method | ||||||||||||||||||||
* The name of the method to call on the tech. | ||||||||||||||||||||
* | ||||||||||||||||||||
* @return {Function|undefined} | ||||||||||||||||||||
* the method or undefined | ||||||||||||||||||||
* @return {Mixed|undefined} | ||||||||||||||||||||
* The return value of the method that was called or undefined if | ||||||||||||||||||||
* there is no tech or the tech is not ready. | ||||||||||||||||||||
* | ||||||||||||||||||||
* @private | ||||||||||||||||||||
*/ | ||||||||||||||||||||
|
@@ -1623,35 +1623,38 @@ class Player extends Component { | |||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
/** | ||||||||||||||||||||
* start media playback | ||||||||||||||||||||
* Begin playback. | ||||||||||||||||||||
* | ||||||||||||||||||||
* @return {Promise|undefined} | ||||||||||||||||||||
* Returns a `Promise` if the browser returns one, for most browsers this will | ||||||||||||||||||||
* return undefined. | ||||||||||||||||||||
* Returns a `Promise` only if the browser returns one and the player | ||||||||||||||||||||
* is ready to begin playback. For most browsers and non-ready | ||||||||||||||||||||
* situations, this will return undefined. | ||||||||||||||||||||
*/ | ||||||||||||||||||||
play() { | ||||||||||||||||||||
if (this.changingSrc_) { | ||||||||||||||||||||
this.ready(function() { | ||||||||||||||||||||
const retval = this.techGet_('play'); | ||||||||||||||||||||
|
||||||||||||||||||||
// silence errors (unhandled promise from play) | ||||||||||||||||||||
if (retval !== undefined && typeof retval.then === 'function') { | ||||||||||||||||||||
retval.then(null, (e) => {}); | ||||||||||||||||||||
} | ||||||||||||||||||||
// If either the player or tech are not ready, queue up a call to the | ||||||||||||||||||||
// tech's `play()` method for when it _is_ ready. | ||||||||||||||||||||
if (!this.isReady_) { | ||||||||||||||||||||
this.ready(() => { | ||||||||||||||||||||
silencePromise(this.techGet_('play')); | ||||||||||||||||||||
}); | ||||||||||||||||||||
|
||||||||||||||||||||
} else if (!this.tech_.isReady_) { | ||||||||||||||||||||
this.tech_.ready(() => { | ||||||||||||||||||||
silencePromise(this.techGet_('play')); | ||||||||||||||||||||
}); | ||||||||||||||||||||
|
||||||||||||||||||||
// Only calls the tech's play if we already have a src loaded | ||||||||||||||||||||
} else if (this.isReady_ && (this.src() || this.currentSrc())) { | ||||||||||||||||||||
// If the player/tech are ready and there is a source, call the tech's | ||||||||||||||||||||
// `play()` method. | ||||||||||||||||||||
} else if (this.src() || this.currentSrc()) { | ||||||||||||||||||||
return this.techGet_('play'); | ||||||||||||||||||||
} else { | ||||||||||||||||||||
this.ready(function() { | ||||||||||||||||||||
this.tech_.one('loadstart', function() { | ||||||||||||||||||||
const retval = this.play(); | ||||||||||||||||||||
|
||||||||||||||||||||
// silence errors (unhandled promise from play) | ||||||||||||||||||||
if (retval !== undefined && typeof retval.then === 'function') { | ||||||||||||||||||||
retval.then(null, (e) => {}); | ||||||||||||||||||||
} | ||||||||||||||||||||
// Finally, if the player/tech are ready, but we don't have a source, wait | ||||||||||||||||||||
// for one to be set. | ||||||||||||||||||||
} else { | ||||||||||||||||||||
this.ready(() => { | ||||||||||||||||||||
this.tech_.one('loadstart', () => { | ||||||||||||||||||||
silencePromise(this.techGet_('play')); | ||||||||||||||||||||
}); | ||||||||||||||||||||
}); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
@@ -2284,59 +2287,82 @@ class Player extends Component { | |||||||||||||||||||
* URL. Otherwise, returns nothing/undefined. | ||||||||||||||||||||
*/ | ||||||||||||||||||||
src(source) { | ||||||||||||||||||||
// getter usage | ||||||||||||||||||||
if (typeof source === 'undefined') { | ||||||||||||||||||||
|
||||||||||||||||||||
// Getter usage. | ||||||||||||||||||||
if (source === undefined) { | ||||||||||||||||||||
return this.cache_.src || ''; | ||||||||||||||||||||
} | ||||||||||||||||||||
// filter out invalid sources and turn our source into | ||||||||||||||||||||
// an array of source objects | ||||||||||||||||||||
|
||||||||||||||||||||
// Queues "error 4" to be triggered on the player on the next tick. This | ||||||||||||||||||||
// is done on a delay to give users a chance to listen for "error" events. | ||||||||||||||||||||
const queueError = () => { | ||||||||||||||||||||
this.setTimeout(() => { | ||||||||||||||||||||
this.error({ | ||||||||||||||||||||
code: 4, | ||||||||||||||||||||
message: this.localize(this.options_.notSupportedMessage) | ||||||||||||||||||||
}); | ||||||||||||||||||||
}, 1); | ||||||||||||||||||||
}; | ||||||||||||||||||||
|
||||||||||||||||||||
// Filter out invalid sources and normalize the passed source into an array | ||||||||||||||||||||
// of source objects. | ||||||||||||||||||||
const sources = filterSource(source); | ||||||||||||||||||||
|
||||||||||||||||||||
// if a source was passed in then it is invalid because | ||||||||||||||||||||
// it was filtered to a zero length Array. So we have to | ||||||||||||||||||||
// show an error | ||||||||||||||||||||
// Show an error if there are no sources after filtering. | ||||||||||||||||||||
if (!sources.length) { | ||||||||||||||||||||
this.setTimeout(function() { | ||||||||||||||||||||
this.error({ code: 4, message: this.localize(this.options_.notSupportedMessage) }); | ||||||||||||||||||||
}, 0); | ||||||||||||||||||||
return; | ||||||||||||||||||||
return queueError(); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// intial sources | ||||||||||||||||||||
// Cache initial sources. | ||||||||||||||||||||
this.cache_.sources = sources; | ||||||||||||||||||||
this.changingSrc_ = true; | ||||||||||||||||||||
|
||||||||||||||||||||
// intial source | ||||||||||||||||||||
this.cache_.source = sources[0]; | ||||||||||||||||||||
|
||||||||||||||||||||
// 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; | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||||||||||||
|
||||||||||||||||||||
// This asynchronously resolves any configured middleware. The callback is | ||||||||||||||||||||
// called when the process completes and given the `middlewareSource`, | ||||||||||||||||||||
// which is the final source we'll use to find a compatible tech. | ||||||||||||||||||||
middleware.setSource(this, sources[0], (middlewareSource, mws) => { | ||||||||||||||||||||
|
||||||||||||||||||||
// Cache middleware. | ||||||||||||||||||||
this.middleware_ = mws; | ||||||||||||||||||||
|
||||||||||||||||||||
const err = this.src_(middlewareSource); | ||||||||||||||||||||
// Attempt to set the source on a tech. | ||||||||||||||||||||
const didNotFindTech = this.src_(middlewareSource); | ||||||||||||||||||||
|
||||||||||||||||||||
if (err) { | ||||||||||||||||||||
// We could not find a compatible tech. | ||||||||||||||||||||
if (didNotFindTech) { | ||||||||||||||||||||
|
||||||||||||||||||||
// If no compatible tech was found and there are still sources we have | ||||||||||||||||||||
// not tried, start the process over (including middleware) with the | ||||||||||||||||||||
// next source in the sources array. | ||||||||||||||||||||
if (sources.length > 1) { | ||||||||||||||||||||
return this.src(sources.slice(1)); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// We need to wrap this in a timeout to give folks a chance to add error event handlers | ||||||||||||||||||||
this.setTimeout(function() { | ||||||||||||||||||||
this.error({ code: 4, message: this.localize(this.options_.notSupportedMessage) }); | ||||||||||||||||||||
}, 0); | ||||||||||||||||||||
|
||||||||||||||||||||
// 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 | ||||||||||||||||||||
// If there is no compatible tech, but there are no sources left to | ||||||||||||||||||||
// try, we queue up an error state on the player and trigger "ready" | ||||||||||||||||||||
// to inform any subscribers that the player is ready. | ||||||||||||||||||||
queueError(); | ||||||||||||||||||||
this.triggerReady(); | ||||||||||||||||||||
|
||||||||||||||||||||
return; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
this.changingSrc_ = false; | ||||||||||||||||||||
// video element listed source | ||||||||||||||||||||
this.cache_.src = middlewareSource.src; | ||||||||||||||||||||
|
||||||||||||||||||||
// Notify middlewares of a potentially new tech. | ||||||||||||||||||||
middleware.setTech(mws, this.tech_); | ||||||||||||||||||||
|
||||||||||||||||||||
this.tech_.ready(() => { | ||||||||||||||||||||
|
||||||||||||||||||||
// Cache the source URL. | ||||||||||||||||||||
this.cache_.src = middlewareSource.src; | ||||||||||||||||||||
|
||||||||||||||||||||
// 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(); | ||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, when the player gets the tech's Lines 907 to 908 in c7ad7b3
Lines 1055 to 1061 in c7ad7b3
So, we would end up getting two ready events for the same source unless we change the way that handleTechReady functions.
|
||||||||||||||||||||
}); | ||||||||||||||||||||
}); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
|
@@ -2356,37 +2382,34 @@ class Player extends Component { | |||||||||||||||||||
src_(source) { | ||||||||||||||||||||
const sourceTech = this.selectSource([source]); | ||||||||||||||||||||
|
||||||||||||||||||||
// There is no tech available that can play the chosen source. | ||||||||||||||||||||
if (!sourceTech) { | ||||||||||||||||||||
return true; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// The tech that was found is not the tech that is currently loaded into | ||||||||||||||||||||
// the player; so, we need to unload the old tech, load a new one, and | ||||||||||||||||||||
// set the source on it. | ||||||||||||||||||||
if (!titleCaseEquals(sourceTech.tech, this.techName_)) { | ||||||||||||||||||||
this.changingSrc_ = true; | ||||||||||||||||||||
|
||||||||||||||||||||
// load this technology with the chosen source | ||||||||||||||||||||
this.loadTech_(sourceTech.tech, sourceTech.source); | ||||||||||||||||||||
return false; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// wait until the tech is ready to set the source | ||||||||||||||||||||
this.ready(function() { | ||||||||||||||||||||
|
||||||||||||||||||||
// The setSource tech method was added with source handlers | ||||||||||||||||||||
// so older techs won't support it | ||||||||||||||||||||
// We need to check the direct prototype for the case where subclasses | ||||||||||||||||||||
// of the tech do not support source handlers | ||||||||||||||||||||
if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) { | ||||||||||||||||||||
this.techCall_('setSource', source); | ||||||||||||||||||||
} else { | ||||||||||||||||||||
this.techCall_('src', source.src); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
if (this.options_.preload === 'auto') { | ||||||||||||||||||||
this.load(); | ||||||||||||||||||||
} | ||||||||||||||||||||
// The existing tech will work with this source; so, we can set the | ||||||||||||||||||||
// source on the existing tech. | ||||||||||||||||||||
// | ||||||||||||||||||||
// The `setSource` method was added with source handlers; so, older techs | ||||||||||||||||||||
// won't support it. Also, we need to check the direct prototype for | ||||||||||||||||||||
// the case where subclasses of `Tech` do not support source handlers. | ||||||||||||||||||||
if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) { | ||||||||||||||||||||
this.techCall_('setSource', source); | ||||||||||||||||||||
} else { | ||||||||||||||||||||
this.techCall_('src', source.src); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
// Set the source synchronously if possible (#2326) | ||||||||||||||||||||
}, true); | ||||||||||||||||||||
if (this.options_.preload === 'auto') { | ||||||||||||||||||||
this.load(); | ||||||||||||||||||||
} | ||||||||||||||||||||
|
||||||||||||||||||||
return false; | ||||||||||||||||||||
} | ||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
|
||
/** | ||
* Returns whether an object is `Promise`-like (i.e. has a `then` method). | ||
* | ||
* @param {Object} value | ||
* An object that may or may not be `Promise`-like. | ||
* | ||
* @return {Boolean} | ||
* Whether or not the object is `Promise`-like. | ||
*/ | ||
export function isPromise(value) { | ||
return value !== undefined && typeof value.then === 'function'; | ||
} | ||
|
||
/** | ||
* Silence a Promise-like object. | ||
* | ||
* This is useful for avoiding non-harmful, but potentially confusing "uncaught | ||
* play promise" rejection error messages. | ||
* | ||
* @param {Object} value | ||
* An object that may or may not be `Promise`-like. | ||
*/ | ||
export function silencePromise(value) { | ||
if (isPromise(value)) { | ||
value.then(null, (e) => {}); | ||
} | ||
} |
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.
Having this function sometimes return a promise just sucks, not really something we can/should do about it in this PR though.
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 dunno. I mean, it does model the native
play
method. The alternative would be to include aPromise
polyfill.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 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 :)