Skip to content

Commit bf3e4e8

Browse files
committed
fix: Allow overriding special handling of 404s (#4635)
In general, streaming.failureCallback is meant to give applications control over error handling at the level of streaming. However, there was a special case for HTTP 404s built into StreamingEngine in a way that applications could not override. This was in spite of the fact that the default failureCallback would already check for and retry on the error code BAD_HTTP_STATUS. This removes the special case in StreamingEngine and refactors failureCallback and retryStreaming to preserve the special delay imposed in the old 404 handler. With this, applications can override failureCallback to have complete control over 404 handling. Closes #4548
1 parent 20e2d93 commit bf3e4e8

File tree

5 files changed

+159
-45
lines changed

5 files changed

+159
-45
lines changed

lib/media/streaming_engine.js

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1403,16 +1403,6 @@ shaka.media.StreamingEngine = class {
14031403
this.mediaStates_.delete(ContentType.TEXT);
14041404
} else if (error.code == shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR) {
14051405
this.handleQuotaExceeded_(mediaState, error);
1406-
} else if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS &&
1407-
error.data && error.data[1] == 404) {
1408-
// The segment could not be found, does not exist, or is not available.
1409-
// In any case just try again.
1410-
// The current segment is not available. Schedule another update to
1411-
// fetch the segment again.
1412-
shaka.log.v2(logPrefix, 'segment not available.');
1413-
mediaState.performingUpdate = false;
1414-
mediaState.updateTimer = null;
1415-
this.scheduleUpdate_(mediaState, 1);
14161406
} else {
14171407
shaka.log.error(logPrefix, 'failed fetch and append: code=' +
14181408
error.code);
@@ -1455,9 +1445,10 @@ shaka.media.StreamingEngine = class {
14551445

14561446
/**
14571447
* Clear per-stream error states and retry any failed streams.
1448+
* @param {number} delaySeconds
14581449
* @return {boolean} False if unable to retry.
14591450
*/
1460-
retry() {
1451+
retry(delaySeconds) {
14611452
if (this.destroyer_.destroyed()) {
14621453
shaka.log.error('Unable to retry after StreamingEngine is destroyed!');
14631454
return false;
@@ -1474,7 +1465,7 @@ shaka.media.StreamingEngine = class {
14741465
if (mediaState.hasError) {
14751466
shaka.log.info(logPrefix, 'Retrying after failure...');
14761467
mediaState.hasError = false;
1477-
this.scheduleUpdate_(mediaState, 0.1);
1468+
this.scheduleUpdate_(mediaState, delaySeconds);
14781469
}
14791470
}
14801471

lib/player.js

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4841,12 +4841,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
48414841
* If the player has loaded content, and streaming seen an error, but the
48424842
* could not resume streaming, this will return <code>false</code>.
48434843
*
4844+
* @param {number=} retryDelaySeconds
48444845
* @return {boolean}
48454846
* @export
48464847
*/
4847-
retryStreaming() {
4848+
retryStreaming(retryDelaySeconds = 0.1) {
48484849
return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ?
4849-
this.streamingEngine_.retry() :
4850+
this.streamingEngine_.retry(retryDelaySeconds) :
48504851
false;
48514852
}
48524853

@@ -4942,17 +4943,26 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
49424943
* @private
49434944
*/
49444945
defaultStreamingFailureCallback_(error) {
4945-
const retryErrorCodes = [
4946-
shaka.util.Error.Code.BAD_HTTP_STATUS,
4947-
shaka.util.Error.Code.HTTP_ERROR,
4948-
shaka.util.Error.Code.TIMEOUT,
4949-
];
4946+
// For live streams, we retry streaming automatically for certain errors.
4947+
// For VOD streams, all streaming failures are fatal.
4948+
if (!this.isLive()) {
4949+
return;
4950+
}
49504951

4951-
if (this.isLive() && retryErrorCodes.includes(error.code)) {
4952-
error.severity = shaka.util.Error.Severity.RECOVERABLE;
4952+
let retryDelaySeconds = null;
4953+
if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS ||
4954+
error.code == shaka.util.Error.Code.HTTP_ERROR) {
4955+
// These errors can be near-instant, so delay a bit before retrying.
4956+
retryDelaySeconds = 1;
4957+
} else if (error.code == shaka.util.Error.Code.TIMEOUT) {
4958+
// We already waited for a timeout, so retry quickly.
4959+
retryDelaySeconds = 0.1;
4960+
}
49534961

4962+
if (retryDelaySeconds != null) {
4963+
error.severity = shaka.util.Error.Severity.RECOVERABLE;
49544964
shaka.log.warning('Live streaming error. Retrying automatically...');
4955-
this.retryStreaming();
4965+
this.retryStreaming(retryDelaySeconds);
49564966
}
49574967
}
49584968

test/media/streaming_engine_integration.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,19 @@ describe('StreamingEngine', () => {
154154
/* presentationDuration= */ Infinity);
155155
setupPlayhead();
156156

157+
// Retry on failure for live streams.
158+
config.failureCallback = () => streamingEngine.retry(0.1);
159+
160+
// Ignore 404 errors in live stream tests.
161+
onError.and.callFake((error) => {
162+
if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS &&
163+
error.data[1] == 404) {
164+
// 404 error
165+
} else {
166+
fail(error);
167+
}
168+
});
169+
157170
createStreamingEngine();
158171
}
159172

test/media/streaming_engine_unit.js

Lines changed: 36 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,7 @@ describe('StreamingEngine', () => {
878878

879879
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
880880
config.bufferingGoal = 60;
881-
config.failureCallback = () => streamingEngine.retry();
881+
config.failureCallback = () => streamingEngine.retry(0.1);
882882
createStreamingEngine(config);
883883

884884
// Make requests for different types take different amounts of time.
@@ -1737,8 +1737,23 @@ describe('StreamingEngine', () => {
17371737
beforeEach(() => {
17381738
setupLive();
17391739
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData, 0);
1740-
createStreamingEngine();
1740+
1741+
// Retry on failure for live streams.
1742+
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
1743+
config.failureCallback = () => streamingEngine.retry(0.1);
1744+
1745+
createStreamingEngine(config);
17411746
presentationTimeInSeconds = 100;
1747+
1748+
// Ignore 404 errors in live stream tests.
1749+
onError.and.callFake((error) => {
1750+
if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS &&
1751+
error.data[1] == 404) {
1752+
// 404 error
1753+
} else {
1754+
fail(error);
1755+
}
1756+
});
17421757
});
17431758

17441759
it('outside segment availability window', async () => {
@@ -1768,24 +1783,22 @@ describe('StreamingEngine', () => {
17681783
const originalAppendBuffer =
17691784
// eslint-disable-next-line no-restricted-syntax
17701785
shaka.test.FakeMediaSourceEngine.prototype.appendBufferImpl;
1771-
mediaSourceEngine.appendBuffer.and.callFake(
1772-
(type, data, reference) => {
1773-
expect(presentationTimeInSeconds).toBe(125);
1774-
if (reference && reference.startTime >= 100) {
1775-
// Ignore a possible call for the first Period.
1776-
expect(Util.invokeSpy(timeline.getSegmentAvailabilityStart))
1777-
.toBe(100);
1778-
expect(Util.invokeSpy(timeline.getSegmentAvailabilityEnd))
1779-
.toBe(120);
1780-
playing = true;
1781-
mediaSourceEngine.appendBuffer.and.callFake(
1782-
originalAppendBuffer);
1783-
}
1786+
mediaSourceEngine.appendBuffer.and.callFake((type, data, reference) => {
1787+
expect(presentationTimeInSeconds).toBe(125);
1788+
// Ignore a possible call for the first Period.
1789+
if (reference && reference.startTime >= 100) {
1790+
expect(Util.invokeSpy(timeline.getSegmentAvailabilityStart))
1791+
.not.toBeLessThan(100);
1792+
expect(Util.invokeSpy(timeline.getSegmentAvailabilityEnd))
1793+
.not.toBeLessThan(120);
1794+
playing = true;
1795+
mediaSourceEngine.appendBuffer.and.callFake(originalAppendBuffer);
1796+
}
17841797

1785-
// eslint-disable-next-line no-restricted-syntax
1786-
return originalAppendBuffer.call(
1787-
mediaSourceEngine, type, data, reference);
1788-
});
1798+
// eslint-disable-next-line no-restricted-syntax
1799+
return originalAppendBuffer.call(
1800+
mediaSourceEngine, type, data, reference);
1801+
});
17891802

17901803
await runTest(slideSegmentAvailabilityWindow);
17911804
// Verify buffers.
@@ -2001,7 +2014,7 @@ describe('StreamingEngine', () => {
20012014
mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
20022015

20032016
const config = shaka.util.PlayerConfiguration.createDefault().streaming;
2004-
config.failureCallback = () => streamingEngine.retry();
2017+
config.failureCallback = () => streamingEngine.retry(0.1);
20052018
createStreamingEngine(config);
20062019

20072020
presentationTimeInSeconds = 100;
@@ -2162,7 +2175,7 @@ describe('StreamingEngine', () => {
21622175
netEngine.request.calls.reset();
21632176

21642177
// Retry streaming.
2165-
expect(streamingEngine.retry()).toBe(true);
2178+
expect(streamingEngine.retry(0.1)).toBe(true);
21662179
});
21672180

21682181
// Here we go!
@@ -2192,7 +2205,7 @@ describe('StreamingEngine', () => {
21922205

21932206
// Retry streaming, which should fail and return false.
21942207
netEngine.request.calls.reset();
2195-
expect(streamingEngine.retry()).toBe(false);
2208+
expect(streamingEngine.retry(0.1)).toBe(false);
21962209
});
21972210

21982211
// Here we go!
@@ -2244,7 +2257,7 @@ describe('StreamingEngine', () => {
22442257

22452258
// Retry streaming, which should fail and return false.
22462259
netEngine.request.calls.reset();
2247-
expect(streamingEngine.retry()).toBe(false);
2260+
expect(streamingEngine.retry(0.1)).toBe(false);
22482261
}
22492262
});
22502263

test/player_unit.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3940,6 +3940,93 @@ describe('Player', () => {
39403940
});
39413941
});
39423942

3943+
describe('config streaming.failureCallback default', () => {
3944+
/** @type {jasmine.Spy} */
3945+
let retryStreaming;
3946+
/** @type {jasmine.Spy} */
3947+
let isLive;
3948+
3949+
/**
3950+
* @suppress {accessControls}
3951+
* @param {!shaka.util.Error} error
3952+
*/
3953+
function defaultStreamingFailureCallback(error) {
3954+
player.defaultStreamingFailureCallback_(error);
3955+
}
3956+
3957+
beforeEach(() => {
3958+
retryStreaming = jasmine.createSpy('retryStreaming');
3959+
isLive = jasmine.createSpy('isLive');
3960+
3961+
player.retryStreaming = Util.spyFunc(retryStreaming);
3962+
player.isLive = Util.spyFunc(isLive);
3963+
});
3964+
3965+
it('ignores VOD failures', () => {
3966+
isLive.and.returnValue(false);
3967+
3968+
const error = new shaka.util.Error(
3969+
shaka.util.Error.Severity.CRITICAL,
3970+
shaka.util.Error.Category.NETWORK,
3971+
shaka.util.Error.Code.BAD_HTTP_STATUS,
3972+
/* url= */ '',
3973+
/* http_status= */ 404);
3974+
3975+
defaultStreamingFailureCallback(error);
3976+
expect(retryStreaming).not.toHaveBeenCalled();
3977+
});
3978+
3979+
it('retries live on HTTP 404', () => {
3980+
isLive.and.returnValue(true);
3981+
3982+
const error = new shaka.util.Error(
3983+
shaka.util.Error.Severity.CRITICAL,
3984+
shaka.util.Error.Category.NETWORK,
3985+
shaka.util.Error.Code.BAD_HTTP_STATUS,
3986+
/* url= */ '',
3987+
/* http_status= */ 404);
3988+
3989+
defaultStreamingFailureCallback(error);
3990+
expect(retryStreaming).toHaveBeenCalled();
3991+
});
3992+
3993+
it('retries live on generic HTTP error', () => {
3994+
isLive.and.returnValue(true);
3995+
3996+
const error = new shaka.util.Error(
3997+
shaka.util.Error.Severity.CRITICAL,
3998+
shaka.util.Error.Category.NETWORK,
3999+
shaka.util.Error.Code.HTTP_ERROR);
4000+
4001+
defaultStreamingFailureCallback(error);
4002+
expect(retryStreaming).toHaveBeenCalled();
4003+
});
4004+
4005+
it('retries live on HTTP timeout', () => {
4006+
isLive.and.returnValue(true);
4007+
4008+
const error = new shaka.util.Error(
4009+
shaka.util.Error.Severity.CRITICAL,
4010+
shaka.util.Error.Category.NETWORK,
4011+
shaka.util.Error.Code.TIMEOUT);
4012+
4013+
defaultStreamingFailureCallback(error);
4014+
expect(retryStreaming).toHaveBeenCalled();
4015+
});
4016+
4017+
it('ignores other live failures', () => {
4018+
isLive.and.returnValue(true);
4019+
4020+
const error = new shaka.util.Error(
4021+
shaka.util.Error.Severity.CRITICAL,
4022+
shaka.util.Error.Category.MEDIA,
4023+
shaka.util.Error.Code.VIDEO_ERROR);
4024+
4025+
defaultStreamingFailureCallback(error);
4026+
expect(retryStreaming).not.toHaveBeenCalled();
4027+
});
4028+
});
4029+
39434030
/**
39444031
* Gets the currently active variant track.
39454032
* @return {shaka.extern.Track}

0 commit comments

Comments
 (0)