1
0
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:
Alex Barstow 2021-03-23 17:50:12 -04:00 committed by GitHub
parent 5f59391a74
commit 22e9843942
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 231 additions and 5 deletions

View File

@ -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);
}
/**

View File

@ -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();