diff --git a/AUTHORS b/AUTHORS index 480615eeba..8a5702c5dc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,6 +14,7 @@ # Please keep the list sorted. AdsWizz <*@adswizz.com> +Bryan Huh Esteban Dosztal Google Inc. <*@google.com> Edgeware AB <*@edgeware.tv> diff --git a/docs/tutorials/config.md b/docs/tutorials/config.md index 1bf3a93463..eda71e334f 100644 --- a/docs/tutorials/config.md +++ b/docs/tutorials/config.md @@ -46,6 +46,7 @@ player.getConfiguration(); bufferBehind: 30 bufferingGoal: 10 ignoreTextStreamFailures: false + infiniteRetriesForLiveStreams: true rebufferingGoal: 2 retryParameters: Object diff --git a/externs/shaka/player.js b/externs/shaka/player.js index 3173df6eb3..d4c107c39f 100644 --- a/externs/shaka/player.js +++ b/externs/shaka/player.js @@ -489,6 +489,7 @@ shakaExtern.ManifestConfiguration; /** * @typedef {{ * retryParameters: shakaExtern.RetryParameters, + * infiniteRetriesForLiveStreams: boolean, * rebufferingGoal: number, * bufferingGoal: number, * bufferBehind: number, @@ -503,6 +504,9 @@ shakaExtern.ManifestConfiguration; * * @property {shakaExtern.RetryParameters} retryParameters * Retry parameters for segment requests. + * @property {boolean} infiniteRetriesForLiveStreams + * If true, will retry infinitely on network errors, for live streams only. + * Defaults to true. * @property {number} rebufferingGoal * The minimum number of seconds of content that the StreamingEngine must * buffer before it can begin playback or can continue playback after it has diff --git a/lib/media/streaming_engine.js b/lib/media/streaming_engine.js index 69ab7afeef..95d86bb690 100644 --- a/lib/media/streaming_engine.js +++ b/lib/media/streaming_engine.js @@ -1243,9 +1243,11 @@ shaka.media.StreamingEngine.prototype.fetchAndAppend_ = function( mediaState.performingUpdate = false; - if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS || + if (this.manifest_.presentationTimeline.isLive() && + this.config_.infiniteRetriesForLiveStreams && + (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS || error.code == shaka.util.Error.Code.HTTP_ERROR || - error.code == shaka.util.Error.Code.TIMEOUT) { + error.code == shaka.util.Error.Code.TIMEOUT)) { this.handleNetworkError_(mediaState, error); } else if (error.code == shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR) { this.handleQuotaExceeded_(mediaState, error); diff --git a/lib/player.js b/lib/player.js index 3eff141148..356a9343a7 100644 --- a/lib/player.js +++ b/lib/player.js @@ -1752,6 +1752,7 @@ shaka.Player.prototype.defaultConfig_ = function() { }, streaming: { retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: true, rebufferingGoal: 2, bufferingGoal: 10, bufferBehind: 30, diff --git a/test/media/playhead_observer_unit.js b/test/media/playhead_observer_unit.js index 1757fd96da..e3c92f65b7 100644 --- a/test/media/playhead_observer_unit.js +++ b/test/media/playhead_observer_unit.js @@ -55,6 +55,7 @@ describe('PlayheadObserver', function() { rebufferingGoal: 10, bufferingGoal: 5, retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: true, bufferBehind: 15, ignoreTextStreamFailures: false, useRelativeCueTimestamps: false, diff --git a/test/media/playhead_unit.js b/test/media/playhead_unit.js index 7d4eeeec31..be2a2845cb 100644 --- a/test/media/playhead_unit.js +++ b/test/media/playhead_unit.js @@ -138,6 +138,7 @@ describe('Playhead', function() { rebufferingGoal: 10, bufferingGoal: 5, retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: true, bufferBehind: 15, ignoreTextStreamFailures: false, useRelativeCueTimestamps: false, diff --git a/test/media/streaming_engine_integration.js b/test/media/streaming_engine_integration.js index d7cb5ee14e..af317f2a71 100644 --- a/test/media/streaming_engine_integration.js +++ b/test/media/streaming_engine_integration.js @@ -65,6 +65,7 @@ describe('StreamingEngine', function() { rebufferingGoal: 2, bufferingGoal: 5, retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: true, bufferBehind: 15, ignoreTextStreamFailures: false, useRelativeCueTimestamps: false, @@ -128,7 +129,8 @@ describe('StreamingEngine', function() { timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline( 0 /* segmentAvailabilityStart */, 60 /* segmentAvailabilityEnd */, - 60 /* presentationDuration */); + 60 /* presentationDuration */, + false /* isLive */); setupNetworkingEngine( 0 /* firstPeriodStartTime */, @@ -162,7 +164,8 @@ describe('StreamingEngine', function() { timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline( 275 - 10 /* segmentAvailabilityStart */, 295 - 10 /* segmentAvailabilityEnd */, - Infinity /* presentationDuration */); + Infinity /* presentationDuration */, + true /* isLive */); setupNetworkingEngine( 0 /* firstPeriodStartTime */, @@ -669,7 +672,8 @@ describe('StreamingEngine', function() { shaka.test.StreamingEngineUtil.createFakePresentationTimeline( 0 /* segmentAvailabilityStart */, 30 /* segmentAvailabilityEnd */, - 30 /* presentationDuration */); + 30 /* presentationDuration */, + false /* isLive */); setupNetworkingEngine( 0 /* firstPeriodStartTime */, diff --git a/test/media/streaming_engine_unit.js b/test/media/streaming_engine_unit.js index c0e4bdc8e7..a8afa34b18 100644 --- a/test/media/streaming_engine_unit.js +++ b/test/media/streaming_engine_unit.js @@ -161,7 +161,8 @@ describe('StreamingEngine', function() { timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline( 0 /* segmentAvailabilityStart */, 40 /* segmentAvailabilityEnd */, - 40 /* presentationDuration */); + 40 /* presentationDuration */, + false /* isLive */); setupManifest( 0 /* firstPeriodStartTime */, @@ -259,8 +260,9 @@ describe('StreamingEngine', function() { timeline = shaka.test.StreamingEngineUtil.createFakePresentationTimeline( 100 /* segmentAvailabilityStart */, - 120 /* segmentAvailabilityEnd */, - 140 /* presentationDuration */); + 140 /* segmentAvailabilityEnd */, + 140 /* presentationDuration */, + true /* isLive */); setupManifest( 0 /* firstPeriodStartTime */, @@ -385,6 +387,7 @@ describe('StreamingEngine', function() { rebufferingGoal: 2, bufferingGoal: 5, retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: true, bufferBehind: Infinity, ignoreTextStreamFailures: false, startAtSegmentBoundary: false, @@ -1544,7 +1547,7 @@ describe('StreamingEngine', function() { describe('handles network errors', function() { function testRecoverableError(targetUri, code) { - setupVod(); + setupLive(); // Wrap the NetworkingEngine to perform errors. var originalNetEngine = netEngine; @@ -1574,9 +1577,9 @@ describe('StreamingEngine', function() { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine(); - playhead.getTime.and.returnValue(0); + playhead.getTime.and.returnValue(100); onStartupComplete.and.callFake(function() { - setupFakeGetTime(0); + setupFakeGetTime(100); }); onError.and.callFake(function(error) { @@ -1603,7 +1606,7 @@ describe('StreamingEngine', function() { null, '2_video_init', shaka.util.Error.Code.BAD_HTTP_STATUS)); it('from missing media, first Period', testRecoverableError.bind( - null, '1_video_1', shaka.util.Error.Code.BAD_HTTP_STATUS)); + null, '1_video_10', shaka.util.Error.Code.BAD_HTTP_STATUS)); it('from missing media, second Period', testRecoverableError.bind( null, '2_audio_2', shaka.util.Error.Code.BAD_HTTP_STATUS)); @@ -1616,7 +1619,7 @@ describe('StreamingEngine', function() { null, '2_audio_init', shaka.util.Error.Code.HTTP_ERROR)); it('from missing media, first Period', testRecoverableError.bind( - null, '1_audio_1', shaka.util.Error.Code.HTTP_ERROR)); + null, '1_audio_10', shaka.util.Error.Code.HTTP_ERROR)); it('from missing media, second Period', testRecoverableError.bind( null, '2_video_2', shaka.util.Error.Code.HTTP_ERROR)); @@ -1629,7 +1632,7 @@ describe('StreamingEngine', function() { null, '2_video_init', shaka.util.Error.Code.TIMEOUT)); it('from missing media, first Period', testRecoverableError.bind( - null, '1_video_2', shaka.util.Error.Code.TIMEOUT)); + null, '1_video_11', shaka.util.Error.Code.TIMEOUT)); it('from missing media, second Period', testRecoverableError.bind( null, '2_audio_1', shaka.util.Error.Code.TIMEOUT)); @@ -1735,6 +1738,118 @@ describe('StreamingEngine', function() { expect(onError.calls.count()).toBe(0); expect(mediaSourceEngine.endOfStream).toHaveBeenCalled(); }); + + it('Does not retry if configured not to', function() { + setupLive(); + // Wrap the NetworkingEngine to perform errors. + var originalNetEngine = netEngine; + netEngine = { + request: jasmine.createSpy('request') + }; + var attempts = 0; + var targetUri = '1_audio_init'; + netEngine.request.and.callFake(function(requestType, request) { + if (request.uris[0] == targetUri) { + ++attempts; + if (attempts == 1) { + var data = [targetUri]; + data.push(404); + data.push(''); + + return Promise.reject(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.BAD_HTTP_STATUS, data)); + } + } + return originalNetEngine.request(requestType, request); + }); + + mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); + + var config = { + rebufferingGoal: 2, + bufferingGoal: 5, + retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: false, + bufferBehind: Infinity, + ignoreTextStreamFailures: false, + startAtSegmentBoundary: false, + smallGapLimit: 0.5, + jumpLargeGaps: false + }; + createStreamingEngine(config); + + playhead.getTime.and.returnValue(100); + onStartupComplete.and.callFake(function() { + setupFakeGetTime(100); + }); + + onError.and.callFake(function(error) { + expect(error.severity).toBe(shaka.util.Error.Severity.CRITICAL); + expect(error.category).toBe(shaka.util.Error.Category.NETWORK); + expect(error.code).toBe(shaka.util.Error.Code.BAD_HTTP_STATUS); + }); + + // Here we go! + onChooseStreams.and.callFake(defaultOnChooseStreams.bind(null)); + streamingEngine.init(); + + runTest(); + expect(onError.calls.count()).toBe(1); + expect(attempts).toBe(1); + expect(mediaSourceEngine.endOfStream).toHaveBeenCalledTimes(0); + }); + + it('Does not retry for VOD', function() { + setupVod(); + // Wrap the NetworkingEngine to perform errors. + var originalNetEngine = netEngine; + netEngine = { + request: jasmine.createSpy('request') + }; + var attempts = 0; + var targetUri = '1_audio_init'; + netEngine.request.and.callFake(function(requestType, request) { + if (request.uris[0] == targetUri) { + ++attempts; + if (attempts == 1) { + var data = [targetUri]; + data.push(404); + data.push(''); + + return Promise.reject(new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.NETWORK, + shaka.util.Error.Code.BAD_HTTP_STATUS, data)); + } + } + return originalNetEngine.request(requestType, request); + }); + + mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); + createStreamingEngine(); + + playhead.getTime.and.returnValue(0); + onStartupComplete.and.callFake(function() { + setupFakeGetTime(0); + }); + + onError.and.callFake(function(error) { + expect(error.severity).toBe(shaka.util.Error.Severity.CRITICAL); + expect(error.category).toBe(shaka.util.Error.Category.NETWORK); + expect(error.code).toBe(shaka.util.Error.Code.BAD_HTTP_STATUS); + }); + + // Here we go! + onChooseStreams.and.callFake(defaultOnChooseStreams.bind(null)); + streamingEngine.init(); + + runTest(); + expect(onError.calls.count()).toBe(1); + expect(attempts).toBe(1); + expect(mediaSourceEngine.endOfStream).toHaveBeenCalledTimes(0); + }); }); describe('eviction', function() { @@ -1748,6 +1863,7 @@ describe('StreamingEngine', function() { rebufferingGoal: 1, bufferingGoal: 1, retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: true, bufferBehind: 10, ignoreTextStreamFailures: false, startAtSegmentBoundary: false, @@ -1837,6 +1953,7 @@ describe('StreamingEngine', function() { rebufferingGoal: 1, bufferingGoal: 1, retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: true, bufferBehind: 10, ignoreTextStreamFailures: false, startAtSegmentBoundary: false, @@ -1906,6 +2023,7 @@ describe('StreamingEngine', function() { rebufferingGoal: 1, bufferingGoal: 1, retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: true, bufferBehind: 10, ignoreTextStreamFailures: false, startAtSegmentBoundary: false, @@ -2090,6 +2208,7 @@ describe('StreamingEngine', function() { mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData); createStreamingEngine({ retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(), + infiniteRetriesForLiveStreams: true, bufferBehind: Infinity, ignoreTextStreamFailures: false, startAtSegmentBoundary: false, diff --git a/test/test/util/streaming_engine_util.js b/test/test/util/streaming_engine_util.js index 3787d37a9e..aadbe92825 100644 --- a/test/test/util/streaming_engine_util.js +++ b/test/test/util/streaming_engine_util.js @@ -101,11 +101,13 @@ shaka.test.StreamingEngineUtil.createFakeNetworkingEngine = function( * @param {number} segmentAvailabilityEnd The initial value of * |segmentAvailabilityEnd|. * @param {number} presentationDuration + * @param {boolean} isLive * @return {!Object} A PresentationTimeline look-alike. * */ shaka.test.StreamingEngineUtil.createFakePresentationTimeline = function( - segmentAvailabilityStart, segmentAvailabilityEnd, presentationDuration) { + segmentAvailabilityStart, segmentAvailabilityEnd, presentationDuration, + isLive) { var timeline = { getDuration: jasmine.createSpy('getDuration'), setDuration: jasmine.createSpy('setDuration'), @@ -127,7 +129,7 @@ shaka.test.StreamingEngineUtil.createFakePresentationTimeline = function( timeline.getDuration.and.returnValue(presentationDuration); timeline.isLive.and.callFake(function() { - return presentationDuration == Infinity; + return isLive; }); timeline.getEarliestStart.and.callFake(function() {