shaka-player 4.2.2 → 4.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.github/workflows/build-and-test.yaml +11 -6
  2. package/.github/workflows/compute-incremental-coverage.py +14 -3
  3. package/.github/workflows/demo-version-index.yaml +1 -1
  4. package/.github/workflows/nightly-demo.yaml +1 -1
  5. package/.github/workflows/release-please.yaml +5 -5
  6. package/.github/workflows/report-incremental-coverage.yaml +1 -1
  7. package/.github/workflows/selenium-lab-tests.yaml +8 -6
  8. package/AUTHORS +2 -0
  9. package/CHANGELOG.md +32 -0
  10. package/CONTRIBUTORS +2 -0
  11. package/dist/deps.js +5 -5
  12. package/dist/locales.js +33 -33
  13. package/dist/shaka-player.compiled.d.ts +6 -1
  14. package/dist/shaka-player.compiled.debug.d.ts +6 -1
  15. package/dist/shaka-player.compiled.debug.externs.js +3 -1
  16. package/dist/shaka-player.compiled.debug.js +119 -115
  17. package/dist/shaka-player.compiled.debug.map +4 -4
  18. package/dist/shaka-player.compiled.externs.js +3 -1
  19. package/dist/shaka-player.compiled.js +719 -716
  20. package/dist/shaka-player.compiled.map +4 -4
  21. package/dist/shaka-player.ui.d.ts +6 -1
  22. package/dist/shaka-player.ui.debug.d.ts +6 -1
  23. package/dist/shaka-player.ui.debug.externs.js +3 -1
  24. package/dist/shaka-player.ui.debug.js +135 -132
  25. package/dist/shaka-player.ui.debug.map +4 -4
  26. package/dist/shaka-player.ui.externs.js +3 -1
  27. package/dist/shaka-player.ui.js +846 -843
  28. package/dist/shaka-player.ui.map +4 -4
  29. package/docs/tutorials/welcome.md +2 -2
  30. package/externs/mux.js +3 -0
  31. package/lib/ads/client_side_ad_manager.js +5 -2
  32. package/lib/cast/cast_proxy.js +2 -1
  33. package/lib/cast/cast_receiver.js +19 -5
  34. package/lib/cast/cast_sender.js +2 -1
  35. package/lib/cast/cast_utils.js +16 -6
  36. package/lib/hls/hls_parser.js +27 -9
  37. package/lib/media/drm_engine.js +7 -6
  38. package/lib/media/media_source_engine.js +6 -0
  39. package/lib/media/playhead.js +24 -2
  40. package/lib/media/streaming_engine.js +3 -12
  41. package/lib/media/transmuxer.js +6 -0
  42. package/lib/offline/indexeddb/storage_mechanism.js +24 -0
  43. package/lib/player.js +24 -11
  44. package/lib/polyfill/media_capabilities.js +7 -6
  45. package/lib/text/ttml_text_parser.js +69 -93
  46. package/lib/text/vtt_text_parser.js +1 -1
  47. package/lib/util/error.js +7 -0
  48. package/lib/util/manifest_parser_utils.js +4 -0
  49. package/lib/util/periods.js +9 -9
  50. package/lib/util/platform.js +0 -10
  51. package/lib/util/xml_utils.js +45 -6
  52. package/package.json +1 -1
  53. package/roadmap.md +24 -6
  54. package/test/cast/cast_receiver_integration.js +97 -16
  55. package/test/cast/cast_receiver_unit.js +17 -12
  56. package/test/cast/cast_utils_unit.js +1 -0
  57. package/test/media/media_source_engine_integration.js +31 -0
  58. package/test/media/playhead_unit.js +41 -0
  59. package/test/media/streaming_engine_integration.js +13 -0
  60. package/test/media/streaming_engine_unit.js +36 -23
  61. package/test/offline/storage_integration.js +30 -0
  62. package/test/player_unit.js +87 -0
  63. package/test/test/util/simple_fakes.js +3 -0
  64. package/test/text/vtt_text_parser_unit.js +23 -1
  65. package/test/util/periods_unit.js +149 -11
  66. package/test/util/xml_utils_unit.js +64 -0
  67. package/ui/controls.js +4 -1
  68. package/ui/settings_menu.js +11 -2
@@ -245,18 +245,20 @@ filterDescribe('CastReceiver', castReceiverSupport, () => {
245
245
  const fakeEvent = {type: 'timeupdate'};
246
246
  mockVideo.on['timeupdate'](fakeEvent);
247
247
 
248
- // There are now "update" and "event" messages, in that order.
249
- expect(mockShakaMessageBus.messages).toEqual([
250
- {
251
- type: 'update',
252
- update: jasmine.any(Object),
253
- },
254
- {
255
- type: 'event',
256
- targetName: 'video',
257
- event: jasmine.objectContaining(fakeEvent),
258
- },
259
- ]);
248
+ // There are now some number of "update" and "event" messages, in that
249
+ // order.
250
+ expect(mockShakaMessageBus.messages).toContain({
251
+ type: 'update',
252
+ update: jasmine.any(Object),
253
+ });
254
+ expect(mockShakaMessageBus.messages).toContain({
255
+ type: 'event',
256
+ targetName: 'video',
257
+ event: jasmine.objectContaining(fakeEvent),
258
+ });
259
+ const eventIndex = mockShakaMessageBus.messages.findIndex(
260
+ (message) => message.type == 'event');
261
+ expect(eventIndex).toBe(mockShakaMessageBus.messages.length - 1);
260
262
  });
261
263
  });
262
264
 
@@ -1105,6 +1107,9 @@ filterDescribe('CastReceiver', castReceiverSupport, () => {
1105
1107
  for (const name in CastUtils.PlayerGetterMethods) {
1106
1108
  player[name] = jasmine.createSpy(name);
1107
1109
  }
1110
+ for (const name in CastUtils.LargePlayerGetterMethods) {
1111
+ player[name] = jasmine.createSpy(name);
1112
+ }
1108
1113
  for (const name in CastUtils.PlayerGetterMethodsThatRequireLive) {
1109
1114
  player[name] = jasmine.createSpy(name);
1110
1115
  }
@@ -34,6 +34,7 @@ describe('CastUtils', () => {
34
34
  const castMembers = CastUtils.PlayerVoidMethods
35
35
  .concat(CastUtils.PlayerPromiseMethods)
36
36
  .concat(Object.keys(CastUtils.PlayerGetterMethods))
37
+ .concat(Object.keys(CastUtils.LargePlayerGetterMethods))
37
38
  .concat(Object.keys(CastUtils.PlayerGetterMethodsThatRequireLive));
38
39
  // eslint-disable-next-line no-restricted-syntax
39
40
  const allPlayerMembers = Object.getOwnPropertyNames(shaka.Player.prototype);
@@ -70,6 +70,18 @@ describe('MediaSourceEngine', () => {
70
70
  type, segment, reference, /* hasClosedCaptions= */ false);
71
71
  }
72
72
 
73
+ function appendWithSeek(type, segmentNumber) {
74
+ const segment = generators[type]
75
+ .getSegment(segmentNumber, Date.now() / 1000);
76
+ const reference = dummyReference(type, segmentNumber);
77
+ return mediaSourceEngine.appendBuffer(
78
+ type,
79
+ segment,
80
+ reference,
81
+ /* hasClosedCaptions= */ false,
82
+ /* seeked= */ true);
83
+ }
84
+
73
85
  function appendInitWithClosedCaptions(type) {
74
86
  const segment = generators[type].getInitSegment(Date.now() / 1000);
75
87
  const reference = null;
@@ -380,6 +392,25 @@ describe('MediaSourceEngine', () => {
380
392
  expect(textDisplayer.appendSpy).toHaveBeenCalledTimes(3);
381
393
  });
382
394
 
395
+ it('extracts CEA-708 captions from previous segment from hls', async () => {
396
+ // Load TS file with CEA-708 captions.
397
+ metadata = shaka.test.TestScheme.DATA['cea-708_ts'];
398
+ generators = shaka.test.TestScheme.GENERATORS['cea-708_ts'];
399
+
400
+ const initObject = new Map();
401
+ initObject.set(ContentType.VIDEO, getFakeStream(metadata.video));
402
+ initObject.set(ContentType.TEXT, getFakeStream(metadata.text));
403
+ // Call with forceTransmux = true, so that it will transmux even on
404
+ // platforms with native TS support.
405
+ await mediaSourceEngine.init(initObject, /* forceTransmux= */ true);
406
+ mediaSourceEngine.setSelectedClosedCaptionId('CC1');
407
+
408
+ await append(ContentType.VIDEO, 2);
409
+ await appendWithSeek(ContentType.VIDEO, 0);
410
+
411
+ expect(textDisplayer.appendSpy).toHaveBeenCalledTimes(6);
412
+ });
413
+
383
414
  it('buffers partial TS video segments in sequence mode', async () => {
384
415
  metadata = shaka.test.TestScheme.DATA['cea-708_ts'];
385
416
  generators = shaka.test.TestScheme.GENERATORS['cea-708_ts'];
@@ -161,6 +161,8 @@ describe('Playhead', () => {
161
161
  Util.spyFunc(onSeek),
162
162
  Util.spyFunc(onEvent));
163
163
 
164
+ playhead.ready();
165
+
164
166
  expect(video.currentTime).toBe(5);
165
167
  expect(playhead.getTime()).toBe(5);
166
168
 
@@ -182,6 +184,8 @@ describe('Playhead', () => {
182
184
  Util.spyFunc(onSeek),
183
185
  Util.spyFunc(onEvent));
184
186
 
187
+ playhead.ready();
188
+
185
189
  expect(video.addEventListener).toHaveBeenCalledWith(
186
190
  'loadedmetadata', jasmine.any(Function), jasmine.anything());
187
191
  expect(video.addEventListener).not.toHaveBeenCalledWith(
@@ -223,6 +227,8 @@ describe('Playhead', () => {
223
227
  Util.spyFunc(onSeek),
224
228
  Util.spyFunc(onEvent));
225
229
 
230
+ playhead.ready();
231
+
226
232
  video.on['seeking']();
227
233
  expect(playhead.getTime()).toBe(5);
228
234
  expect(video.currentTime).toBe(5);
@@ -264,6 +270,8 @@ describe('Playhead', () => {
264
270
  Util.spyFunc(onSeek),
265
271
  Util.spyFunc(onEvent));
266
272
 
273
+ playhead.ready();
274
+
267
275
  expect(playhead.getTime()).toBe(59); // duration - durationBackoff
268
276
  expect(video.currentTime).toBe(59); // duration - durationBackoff
269
277
  });
@@ -302,6 +310,8 @@ describe('Playhead', () => {
302
310
  Util.spyFunc(onSeek),
303
311
  Util.spyFunc(onEvent));
304
312
 
313
+ playhead.ready();
314
+
305
315
  expect(playhead.getTime()).toBe(30);
306
316
  });
307
317
 
@@ -314,6 +324,8 @@ describe('Playhead', () => {
314
324
  Util.spyFunc(onSeek),
315
325
  Util.spyFunc(onEvent));
316
326
 
327
+ playhead.ready();
328
+
317
329
  expect(video.addEventListener).toHaveBeenCalledWith(
318
330
  'loadedmetadata', jasmine.any(Function), jasmine.anything());
319
331
 
@@ -347,6 +359,8 @@ describe('Playhead', () => {
347
359
  Util.spyFunc(onSeek),
348
360
  Util.spyFunc(onEvent));
349
361
 
362
+ playhead.ready();
363
+
350
364
  expect(video.addEventListener).toHaveBeenCalledWith(
351
365
  'loadedmetadata', jasmine.any(Function), jasmine.anything());
352
366
 
@@ -380,6 +394,8 @@ describe('Playhead', () => {
380
394
  Util.spyFunc(onSeek),
381
395
  Util.spyFunc(onEvent));
382
396
 
397
+ playhead.ready();
398
+
383
399
  // This has to periodically increment the mock date to allow the onSeeking_
384
400
  // handler to seek, if appropriate.
385
401
 
@@ -652,6 +668,8 @@ describe('Playhead', () => {
652
668
  expect(currentTime).toBe(1000);
653
669
  seekCount = 0;
654
670
 
671
+ playhead.ready();
672
+
655
673
  // The availability window slips ahead.
656
674
  timeline.getSeekRangeStart.and.returnValue(1030);
657
675
  timeline.getSeekRangeEnd.and.returnValue(1030);
@@ -696,6 +714,8 @@ describe('Playhead', () => {
696
714
  Util.spyFunc(onSeek),
697
715
  Util.spyFunc(onEvent));
698
716
 
717
+ playhead.ready();
718
+
699
719
  video.on['seeking']();
700
720
  expect(video.currentTime).toBe(5);
701
721
  expect(playhead.getTime()).toBe(5);
@@ -728,6 +748,8 @@ describe('Playhead', () => {
728
748
  Util.spyFunc(onSeek),
729
749
  Util.spyFunc(onEvent));
730
750
 
751
+ playhead.ready();
752
+
731
753
  video.on['seeking']();
732
754
  expect(video.currentTime).toBe(5);
733
755
  expect(playhead.getTime()).toBe(5);
@@ -765,6 +787,9 @@ describe('Playhead', () => {
765
787
 
766
788
  video.currentTime = 0;
767
789
  video.seeking = true;
790
+
791
+ playhead.ready();
792
+
768
793
  // "video.seeking" stays true until the buffered range intersects with
769
794
  // "video.currentTime". Playhead should correct anyway.
770
795
  video.on['seeking']();
@@ -809,6 +834,8 @@ describe('Playhead', () => {
809
834
  Util.spyFunc(onSeek),
810
835
  Util.spyFunc(onEvent));
811
836
 
837
+ playhead.ready();
838
+
812
839
  /**
813
840
  * Prevent retries on the initial start time seek. This will ensure that
814
841
  * only one call is made for the initial seek, and that any additional calls
@@ -933,6 +960,8 @@ describe('Playhead', () => {
933
960
  Util.spyFunc(onSeek),
934
961
  Util.spyFunc(onEvent));
935
962
 
963
+ playhead.ready();
964
+
936
965
  jasmine.clock().tick(500);
937
966
  for (let time = data.start; time < data.waitingAt; time++) {
938
967
  // We don't want to run tick() for 1 second because it will trigger
@@ -1142,6 +1171,8 @@ describe('Playhead', () => {
1142
1171
  Util.spyFunc(onSeek),
1143
1172
  Util.spyFunc(onEvent));
1144
1173
 
1174
+ playhead.ready();
1175
+
1145
1176
  jasmine.clock().tick(500);
1146
1177
  expect(onEvent).not.toHaveBeenCalled();
1147
1178
 
@@ -1190,6 +1221,8 @@ describe('Playhead', () => {
1190
1221
  Util.spyFunc(onSeek),
1191
1222
  Util.spyFunc(onEvent));
1192
1223
 
1224
+ playhead.ready();
1225
+
1193
1226
  playhead.notifyOfBufferingChange();
1194
1227
  jasmine.clock().tick(500);
1195
1228
 
@@ -1212,6 +1245,8 @@ describe('Playhead', () => {
1212
1245
  Util.spyFunc(onSeek),
1213
1246
  Util.spyFunc(onEvent));
1214
1247
 
1248
+ playhead.ready();
1249
+
1215
1250
  playhead.notifyOfBufferingChange();
1216
1251
  jasmine.clock().tick(500);
1217
1252
 
@@ -1236,6 +1271,8 @@ describe('Playhead', () => {
1236
1271
  Util.spyFunc(onSeek),
1237
1272
  Util.spyFunc(onEvent));
1238
1273
 
1274
+ playhead.ready();
1275
+
1239
1276
  playhead.notifyOfBufferingChange();
1240
1277
  jasmine.clock().tick(500);
1241
1278
 
@@ -1260,6 +1297,8 @@ describe('Playhead', () => {
1260
1297
  Util.spyFunc(onSeek),
1261
1298
  Util.spyFunc(onEvent));
1262
1299
 
1300
+ playhead.ready();
1301
+
1263
1302
  playhead.notifyOfBufferingChange();
1264
1303
  jasmine.clock().tick(500);
1265
1304
 
@@ -1287,6 +1326,8 @@ describe('Playhead', () => {
1287
1326
  Util.spyFunc(onSeek),
1288
1327
  Util.spyFunc(onEvent));
1289
1328
 
1329
+ playhead.ready();
1330
+
1290
1331
  jasmine.clock().tick(500);
1291
1332
  expect(onEvent).not.toHaveBeenCalled();
1292
1333
 
@@ -154,6 +154,19 @@ describe('StreamingEngine', () => {
154
154
  /* presentationDuration= */ Infinity);
155
155
  setupPlayhead();
156
156
 
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
+
157
170
  createStreamingEngine();
158
171
  }
159
172
 
@@ -878,7 +878,7 @@ describe('StreamingEngine', () => {
878
878
 
879
879
  const config = shaka.util.PlayerConfiguration.createDefault().streaming;
880
880
  config.bufferingGoal = 60;
881
- config.failureCallback = () => streamingEngine.retry();
881
+ config.failureCallback = () => streamingEngine.retry(0.1);
882
882
  createStreamingEngine(config);
883
883
 
884
884
  // Make requests for different types take different amounts of time.
@@ -1737,8 +1737,23 @@ describe('StreamingEngine', () => {
1737
1737
  beforeEach(() => {
1738
1738
  setupLive();
1739
1739
  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);
1741
1746
  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
+ });
1742
1757
  });
1743
1758
 
1744
1759
  it('outside segment availability window', async () => {
@@ -1768,24 +1783,22 @@ describe('StreamingEngine', () => {
1768
1783
  const originalAppendBuffer =
1769
1784
  // eslint-disable-next-line no-restricted-syntax
1770
1785
  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
+ }
1784
1797
 
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
+ });
1789
1802
 
1790
1803
  await runTest(slideSegmentAvailabilityWindow);
1791
1804
  // Verify buffers.
@@ -2001,7 +2014,7 @@ describe('StreamingEngine', () => {
2001
2014
  mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
2002
2015
 
2003
2016
  const config = shaka.util.PlayerConfiguration.createDefault().streaming;
2004
- config.failureCallback = () => streamingEngine.retry();
2017
+ config.failureCallback = () => streamingEngine.retry(0.1);
2005
2018
  createStreamingEngine(config);
2006
2019
 
2007
2020
  presentationTimeInSeconds = 100;
@@ -2162,7 +2175,7 @@ describe('StreamingEngine', () => {
2162
2175
  netEngine.request.calls.reset();
2163
2176
 
2164
2177
  // Retry streaming.
2165
- expect(streamingEngine.retry()).toBe(true);
2178
+ expect(streamingEngine.retry(0.1)).toBe(true);
2166
2179
  });
2167
2180
 
2168
2181
  // Here we go!
@@ -2192,7 +2205,7 @@ describe('StreamingEngine', () => {
2192
2205
 
2193
2206
  // Retry streaming, which should fail and return false.
2194
2207
  netEngine.request.calls.reset();
2195
- expect(streamingEngine.retry()).toBe(false);
2208
+ expect(streamingEngine.retry(0.1)).toBe(false);
2196
2209
  });
2197
2210
 
2198
2211
  // Here we go!
@@ -2244,7 +2257,7 @@ describe('StreamingEngine', () => {
2244
2257
 
2245
2258
  // Retry streaming, which should fail and return false.
2246
2259
  netEngine.request.calls.reset();
2247
- expect(streamingEngine.retry()).toBe(false);
2260
+ expect(streamingEngine.retry(0.1)).toBe(false);
2248
2261
  }
2249
2262
  });
2250
2263
 
@@ -1034,6 +1034,36 @@ filterDescribe('Storage', storageSupport, () => {
1034
1034
  }
1035
1035
  });
1036
1036
 
1037
+ /**
1038
+ * In some situations, indexedDB.open() can just hang, and call neither the
1039
+ * 'success' nor the 'error' callbacks.
1040
+ * I'm not sure what causes it, but it seems to happen consistently between
1041
+ * reloads when it does so it might be a browser-based issue.
1042
+ * In that case, we should time out with an error, instead of also hanging.
1043
+ */
1044
+ it('throws an error if indexedDB open times out', async () => {
1045
+ const oldOpen = window.indexedDB.open;
1046
+ window.indexedDB.open = () => {
1047
+ // Just return a dummy object.
1048
+ return /** @type {!IDBOpenDBRequest} */ ({
1049
+ onsuccess: (event) => {},
1050
+ onerror: (error) => {},
1051
+ });
1052
+ };
1053
+
1054
+ /** @type {!shaka.offline.StorageMuxer} */
1055
+ const muxer = new shaka.offline.StorageMuxer();
1056
+ const expectedError = shaka.test.Util.jasmineError(new shaka.util.Error(
1057
+ shaka.util.Error.Severity.CRITICAL,
1058
+ shaka.util.Error.Category.STORAGE,
1059
+ shaka.util.Error.Code.INDEXED_DB_INIT_TIMED_OUT));
1060
+
1061
+ await expectAsync(muxer.init())
1062
+ .toBeRejectedWith(expectedError);
1063
+
1064
+ window.indexedDB.open = oldOpen;
1065
+ });
1066
+
1037
1067
  it('throws an error if the content is a live stream', async () => {
1038
1068
  const expected = Util.jasmineError(new shaka.util.Error(
1039
1069
  shaka.util.Error.Severity.CRITICAL,
@@ -3940,6 +3940,93 @@ describe('Player', () => {
3940
3940
  });
3941
3941
  });
3942
3942
 
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
+
3943
4030
  /**
3944
4031
  * Gets the currently active variant track.
3945
4032
  * @return {shaka.extern.Track}
@@ -339,6 +339,9 @@ shaka.test.FakePlayhead = class {
339
339
  /** @private {number} */
340
340
  this.stallsDetected_ = 0;
341
341
 
342
+ /** @type {!jasmine.Spy} */
343
+ this.ready = jasmine.createSpy('ready');
344
+
342
345
  /** @type {!jasmine.Spy} */
343
346
  this.setStartTime = jasmine.createSpy('setStartTime')
344
347
  .and.callFake((value) => {
@@ -597,7 +597,7 @@ describe('VttTextParser', () => {
597
597
  [
598
598
  {startTime: 95443, endTime: 95445, payload: 'Test'},
599
599
  ],
600
- // 8589870000/900000 = 95443 sec, so expect every timestamp to be 95443
600
+ // 8589870000/90000 = 95443 sec, so expect every timestamp to be 95443
601
601
  // seconds ahead of what is specified.
602
602
  'WEBVTT\n' +
603
603
  'X-TIMESTAMP-MAP=MPEGTS:8589870000,LOCAL:00:00:00.000\n\n' +
@@ -623,6 +623,28 @@ describe('VttTextParser', () => {
623
623
  /* sequenceMode= */ true);
624
624
  });
625
625
 
626
+ // A mock-up of HLS live subs as seen in b/253104251.
627
+ it('handles timestamp rollover and negative offset in HLS live', () => {
628
+ // Similar to values seen in b/253104251, for a realistic regression test.
629
+ // When using sequence mode on live HLS, we get negative offsets that
630
+ // represent the timestamp of our first append in sequence mode.
631
+ verifyHelper(
632
+ [
633
+ {startTime: 3600, endTime: 3602, payload: 'Test'},
634
+ ],
635
+ 'WEBVTT\n' +
636
+ 'X-TIMESTAMP-MAP=MPEGTS:8355814896,LOCAL:00:00:00.000\n\n' +
637
+ '00:00:00.000 --> 00:00:02.000 line:0\n' +
638
+ 'Test',
639
+ {
640
+ periodStart: -1234567,
641
+ segmentStart: 3600,
642
+ segmentEnd: 3610,
643
+ vttOffset: -1234567,
644
+ },
645
+ /* sequenceMode= */ true);
646
+ });
647
+
626
648
  it('supports global style blocks', () => {
627
649
  const textShadow = '-1px 0 black, 0 1px black, 1px 0 black, 0 -1px black';
628
650
  verifyHelper(