mirror of
https://github.com/videojs/video.js.git
synced 2025-03-05 15:16:06 +02:00
feat: retry on error (#7038)
Add a `retryOnError` option. When set, during source selection, if a source fails to load, we will retry the next item in the sources list. In the future, we may enable this by default. A source that fails during playback will *not* trigger this behavior. Fixes #1805.
This commit is contained in:
parent
5f59391a74
commit
22e9843942
@ -3308,7 +3308,7 @@ class Player extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the video source.
|
||||
* Executes source setting and getting logic
|
||||
*
|
||||
* @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
|
||||
* A SourceObject, an array of SourceObjects, or a string referencing
|
||||
@ -3317,16 +3317,24 @@ class Player extends Component {
|
||||
* algorithms can take the `type` into account.
|
||||
*
|
||||
* If not provided, this method acts as a getter.
|
||||
* @param {boolean} isRetry
|
||||
* Indicates whether this is being called internally as a result of a retry
|
||||
*
|
||||
* @return {string|undefined}
|
||||
* If the `source` argument is missing, returns the current source
|
||||
* URL. Otherwise, returns nothing/undefined.
|
||||
*/
|
||||
src(source) {
|
||||
handleSrc_(source, isRetry) {
|
||||
// getter usage
|
||||
if (typeof source === 'undefined') {
|
||||
return this.cache_.src || '';
|
||||
}
|
||||
|
||||
// Reset retry behavior for new source
|
||||
if (this.resetRetryOnError_) {
|
||||
this.resetRetryOnError_();
|
||||
}
|
||||
|
||||
// filter out invalid sources and turn our source into
|
||||
// an array of source objects
|
||||
const sources = filterSource(source);
|
||||
@ -3344,7 +3352,12 @@ class Player extends Component {
|
||||
// initial sources
|
||||
this.changingSrc_ = true;
|
||||
|
||||
this.cache_.sources = sources;
|
||||
// Only update the cached source list if we are not retrying a new source after error,
|
||||
// since in that case we want to include the failed source(s) in the cache
|
||||
if (!isRetry) {
|
||||
this.cache_.sources = sources;
|
||||
}
|
||||
|
||||
this.updateSourceCaches_(sources[0]);
|
||||
|
||||
// middlewareSource is the source after it has been changed by middleware
|
||||
@ -3353,14 +3366,17 @@ class Player extends Component {
|
||||
|
||||
// since sourceSet is async we have to update the cache again after we select a source since
|
||||
// the source that is selected could be out of order from the cache update above this callback.
|
||||
this.cache_.sources = sources;
|
||||
if (!isRetry) {
|
||||
this.cache_.sources = sources;
|
||||
}
|
||||
|
||||
this.updateSourceCaches_(middlewareSource);
|
||||
|
||||
const err = this.src_(middlewareSource);
|
||||
|
||||
if (err) {
|
||||
if (sources.length > 1) {
|
||||
return this.src(sources.slice(1));
|
||||
return this.handleSrc_(sources.slice(1));
|
||||
}
|
||||
|
||||
this.changingSrc_ = false;
|
||||
@ -3379,6 +3395,46 @@ class Player extends Component {
|
||||
|
||||
middleware.setTech(mws, this.tech_);
|
||||
});
|
||||
|
||||
// Try another available source if this one fails before playback.
|
||||
if (this.options_.retryOnError && sources.length > 1) {
|
||||
const retry = () => {
|
||||
// Remove the error modal
|
||||
this.error(null);
|
||||
this.handleSrc_(sources.slice(1), true);
|
||||
};
|
||||
|
||||
const stopListeningForErrors = () => {
|
||||
this.off('error', retry);
|
||||
};
|
||||
|
||||
this.one('error', retry);
|
||||
this.one('playing', stopListeningForErrors);
|
||||
|
||||
this.resetRetryOnError_ = () => {
|
||||
this.off('error', retry);
|
||||
this.off('playing', stopListeningForErrors);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or set the video source.
|
||||
*
|
||||
* @param {Tech~SourceObject|Tech~SourceObject[]|string} [source]
|
||||
* A SourceObject, an array of SourceObjects, or a string referencing
|
||||
* a URL to a media source. It is _highly recommended_ that an object
|
||||
* or array of objects is used here, so that source selection
|
||||
* algorithms can take the `type` into account.
|
||||
*
|
||||
* If not provided, this method acts as a getter.
|
||||
*
|
||||
* @return {string|undefined}
|
||||
* If the `source` argument is missing, returns the current source
|
||||
* URL. Otherwise, returns nothing/undefined.
|
||||
*/
|
||||
src(source) {
|
||||
return this.handleSrc_(source, false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -349,6 +349,176 @@ QUnit.test('should asynchronously fire error events during source selection', fu
|
||||
log.error.restore();
|
||||
});
|
||||
|
||||
QUnit.test('should retry setting source if error occurs and retryOnError: true', function(assert) {
|
||||
const player = TestHelpers.makePlayer({
|
||||
techOrder: ['html5'],
|
||||
retryOnError: true,
|
||||
sources: [
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans2.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans3.mp4', type: 'video/mp4' }
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
|
||||
'first source set'
|
||||
);
|
||||
|
||||
player.trigger('error');
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans2.mp4', type: 'video/mp4' },
|
||||
'second source set'
|
||||
);
|
||||
|
||||
player.trigger('error');
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans3.mp4', type: 'video/mp4' },
|
||||
'last source set'
|
||||
);
|
||||
|
||||
// No more sources to try so the previous source should remain
|
||||
player.trigger('error');
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans3.mp4', type: 'video/mp4' },
|
||||
'last source remains'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSources(),
|
||||
[
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans2.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans3.mp4', type: 'video/mp4' }
|
||||
],
|
||||
'currentSources() correctly returns the full source list'
|
||||
);
|
||||
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('should not retry setting source if retryOnError: true and error occurs during playback', function(assert) {
|
||||
const player = TestHelpers.makePlayer({
|
||||
techOrder: ['html5'],
|
||||
retryOnError: true,
|
||||
sources: [
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans2.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans3.mp4', type: 'video/mp4' }
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
|
||||
'first source set'
|
||||
);
|
||||
|
||||
player.trigger('error');
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans2.mp4', type: 'video/mp4' },
|
||||
'second source set'
|
||||
);
|
||||
|
||||
// Playback starts then error occurs
|
||||
player.trigger('playing');
|
||||
player.trigger('error');
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans2.mp4', type: 'video/mp4' },
|
||||
'second source remains'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSources(),
|
||||
[
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans2.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans3.mp4', type: 'video/mp4' }
|
||||
],
|
||||
'currentSources() correctly returns the full source list'
|
||||
);
|
||||
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('aborts and resets retryOnError behavior if new src() call made during a retry', function(assert) {
|
||||
const player = TestHelpers.makePlayer({
|
||||
techOrder: ['html5'],
|
||||
retryOnError: true,
|
||||
sources: [
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans2.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans3.mp4', type: 'video/mp4' }
|
||||
]
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' },
|
||||
'first source set'
|
||||
);
|
||||
|
||||
player.trigger('error');
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/oceans2.mp4', type: 'video/mp4' },
|
||||
'second source set'
|
||||
);
|
||||
|
||||
// Setting a new source list should reset retry behavior and enable it for the new sources
|
||||
player.src([
|
||||
{ src: 'http://vjs.zencdn.net/v/newSource.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/newSource2.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/newSource3.mp4', type: 'video/mp4' }
|
||||
]);
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/newSource.mp4', type: 'video/mp4' },
|
||||
'first new source set'
|
||||
);
|
||||
|
||||
player.trigger('error');
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/newSource2.mp4', type: 'video/mp4' },
|
||||
'second new source set'
|
||||
);
|
||||
|
||||
player.trigger('error');
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSource(),
|
||||
{ src: 'http://vjs.zencdn.net/v/newSource3.mp4', type: 'video/mp4' },
|
||||
'third new source set'
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
player.currentSources(),
|
||||
[
|
||||
{ src: 'http://vjs.zencdn.net/v/newSource.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/newSource2.mp4', type: 'video/mp4' },
|
||||
{ src: 'http://vjs.zencdn.net/v/newSource3.mp4', type: 'video/mp4' }
|
||||
],
|
||||
'currentSources() correctly returns the full new source list'
|
||||
);
|
||||
|
||||
player.dispose();
|
||||
});
|
||||
|
||||
QUnit.test('should suppress source error messages', function(assert) {
|
||||
sinon.stub(log, 'error');
|
||||
const clock = sinon.useFakeTimers();
|
||||
|
Loading…
x
Reference in New Issue
Block a user