shaka-player 3.1.2 → 3.1.6

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 (145) hide show
  1. package/.github/workflows/build_and_test.yaml +73 -0
  2. package/.github/workflows/release.yaml +53 -0
  3. package/AUTHORS +3 -0
  4. package/CHANGELOG.md +170 -1
  5. package/CONTRIBUTORS +3 -0
  6. package/build/checkversion.py +1 -1
  7. package/build/generateExterns.js +1 -1
  8. package/build/misspellings.txt +1 -0
  9. package/build/shakaBuildHelpers.py +6 -5
  10. package/build/subprocessWindowsPatch.py +103 -0
  11. package/build/test.py +6 -0
  12. package/demo/common/assets.js +1 -1
  13. package/dist/controls.css +2 -2
  14. package/dist/controls.css.map +1 -1
  15. package/dist/demo.compiled.debug.js +1 -1
  16. package/dist/demo.compiled.js +1 -1
  17. package/dist/deps.js +7 -7
  18. package/dist/locales.js +35 -35
  19. package/dist/receiver.compiled.debug.js +1 -1
  20. package/dist/receiver.compiled.js +1 -1
  21. package/dist/shaka-player.compiled.d.ts +172 -3
  22. package/dist/shaka-player.compiled.debug.d.ts +172 -3
  23. package/dist/shaka-player.compiled.debug.externs.js +184 -1
  24. package/dist/shaka-player.compiled.debug.js +390 -370
  25. package/dist/shaka-player.compiled.debug.map +3 -3
  26. package/dist/shaka-player.compiled.externs.js +184 -1
  27. package/dist/shaka-player.compiled.js +809 -805
  28. package/dist/shaka-player.compiled.map +3 -3
  29. package/dist/shaka-player.ui.d.ts +174 -14
  30. package/dist/shaka-player.ui.debug.d.ts +174 -14
  31. package/dist/shaka-player.ui.debug.externs.js +200 -26
  32. package/dist/shaka-player.ui.debug.js +431 -412
  33. package/dist/shaka-player.ui.debug.map +4 -4
  34. package/dist/shaka-player.ui.externs.js +200 -26
  35. package/dist/shaka-player.ui.js +934 -929
  36. package/dist/shaka-player.ui.map +4 -4
  37. package/docs/tutorials/fairplay.md +1 -1
  38. package/docs/tutorials/faq.md +14 -0
  39. package/externs/ima.js +4 -0
  40. package/externs/shaka/ads.js +2 -0
  41. package/externs/shaka/text.js +10 -0
  42. package/karma.conf.js +20 -15
  43. package/lib/ads/ad_manager.js +30 -11
  44. package/lib/ads/client_side_ad_manager.js +76 -86
  45. package/lib/ads/server_side_ad_manager.js +26 -11
  46. package/lib/cast/cast_proxy.js +6 -2
  47. package/lib/cast/cast_receiver.js +5 -1
  48. package/lib/cast/cast_sender.js +2 -2
  49. package/lib/cea/sei_processor.js +24 -17
  50. package/lib/dash/dash_parser.js +131 -24
  51. package/lib/dash/segment_list.js +1 -1
  52. package/lib/dash/segment_template.js +1 -1
  53. package/lib/hls/hls_parser.js +44 -17
  54. package/lib/hls/manifest_text_parser.js +1 -0
  55. package/lib/media/closed_caption_parser.js +3 -1
  56. package/lib/media/content_workarounds.js +12 -0
  57. package/lib/media/drm_engine.js +20 -1
  58. package/lib/media/gap_jumping_controller.js +8 -4
  59. package/lib/media/media_source_engine.js +1 -2
  60. package/lib/media/playhead.js +1 -1
  61. package/lib/media/region_observer.js +39 -33
  62. package/lib/media/region_timeline.js +17 -19
  63. package/lib/media/streaming_engine.js +8 -4
  64. package/lib/net/networking_engine.js +6 -1
  65. package/lib/offline/storage.js +10 -11
  66. package/lib/player.js +66 -30
  67. package/lib/polyfill/encryption_scheme.js +2 -0
  68. package/lib/polyfill/fullscreen.js +2 -0
  69. package/lib/polyfill/mathround.js +2 -0
  70. package/lib/polyfill/media_capabilities.js +73 -21
  71. package/lib/polyfill/mediasource.js +8 -0
  72. package/lib/polyfill/orientation.js +3 -1
  73. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  74. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  75. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  76. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  77. package/lib/polyfill/pip_webkit.js +2 -0
  78. package/lib/polyfill/storage_estimate.js +2 -0
  79. package/lib/polyfill/video_play_promise.js +2 -0
  80. package/lib/polyfill/videoplaybackquality.js +2 -0
  81. package/lib/polyfill/vttcue.js +2 -0
  82. package/lib/text/cue.js +6 -0
  83. package/lib/text/mp4_ttml_parser.js +3 -0
  84. package/lib/text/text_engine.js +14 -2
  85. package/lib/text/ttml_text_parser.js +135 -87
  86. package/lib/text/ui_text_displayer.js +213 -80
  87. package/lib/text/vtt_text_parser.js +4 -2
  88. package/lib/util/fake_event.js +53 -7
  89. package/lib/util/fake_event_target.js +21 -1
  90. package/lib/util/mp4_box_parsers.js +1 -1
  91. package/lib/util/periods.js +4 -1
  92. package/lib/util/platform.js +13 -1
  93. package/lib/util/stream_utils.js +77 -10
  94. package/lib/util/xml_utils.js +19 -0
  95. package/package.json +3 -6
  96. package/test/cast/cast_proxy_unit.js +12 -6
  97. package/test/cast/cast_receiver_integration.js +1 -1
  98. package/test/cast/cast_utils_unit.js +1 -1
  99. package/test/cea/mp4_cea_parser_unit.js +26 -0
  100. package/test/dash/dash_parser_live_unit.js +43 -0
  101. package/test/dash/dash_parser_manifest_unit.js +222 -3
  102. package/test/hls/hls_parser_unit.js +79 -1
  103. package/test/media/closed_caption_parser_unit.js +20 -0
  104. package/test/media/drm_engine_unit.js +56 -0
  105. package/test/media/playhead_unit.js +27 -1
  106. package/test/media/region_observer_unit.js +40 -5
  107. package/test/media/region_timeline_unit.js +40 -22
  108. package/test/media/streaming_engine_integration.js +2 -1
  109. package/test/media/streaming_engine_unit.js +145 -0
  110. package/test/player_integration.js +38 -0
  111. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  112. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  113. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  114. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  115. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  116. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  117. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  118. package/test/test/assets/3675/dash_0.mpd +30 -0
  119. package/test/test/assets/3675/dash_1.mpd +30 -0
  120. package/test/test/assets/3675/dash_2.mpd +30 -0
  121. package/test/test/assets/3675/dash_3.mpd +30 -0
  122. package/test/test/assets/3675/dash_4.mpd +30 -0
  123. package/test/test/assets/3675/dash_5.mpd +26 -0
  124. package/test/test/assets/3675/init-stream0.m4s +0 -0
  125. package/test/test/assets/empty_caption_video_init.mp4 +0 -0
  126. package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
  127. package/test/test/util/fake_ad_manager.js +4 -1
  128. package/test/test/util/ttml_utils.js +81 -0
  129. package/test/test/util/util.js +10 -7
  130. package/test/text/mp4_ttml_parser_unit.js +11 -18
  131. package/test/text/ttml_text_parser_unit.js +542 -220
  132. package/test/text/ui_text_displayer_unit.js +93 -20
  133. package/test/text/vtt_text_parser_unit.js +17 -1
  134. package/test/ui/localization_unit.js +15 -14
  135. package/test/ui/text_displayer_layout_unit.js +21 -8
  136. package/test/util/mp4_box_parsers_unit.js +28 -0
  137. package/test/util/stream_utils_unit.js +59 -2
  138. package/ui/cast_button.js +2 -3
  139. package/ui/controls.js +11 -6
  140. package/ui/less/buttons.less +0 -4
  141. package/ui/less/containers.less +6 -7
  142. package/ui/locales/ru.json +6 -6
  143. package/ui/localization.js +40 -87
  144. package/ui/pip_button.js +2 -3
  145. package/ui/text_selection.js +1 -0
@@ -209,6 +209,80 @@ describe('HlsParser', () => {
209
209
  expect(actual).toEqual(manifest);
210
210
  });
211
211
 
212
+ it('parses manifest attributes with space', async () => {
213
+ const master = [
214
+ '#EXTM3U\n',
215
+ '#EXT-X-MEDIA:TYPE=AUDIO, GROUP-ID="aud1", LANGUAGE="eng", ',
216
+ 'CHANNELS="16/JOC", URI="audio"\n',
217
+ '#EXT-X-MEDIA:TYPE=SUBTITLES, GROUP-ID="sub1", LANGUAGE="eng", ',
218
+ 'URI="text"\n',
219
+ '#EXT-X-MEDIA:TYPE=SUBTITLES, GROUP-ID="sub2", LANGUAGE="es", ',
220
+ 'URI="text2"\n',
221
+ '#EXT-X-STREAM-INF:BANDWIDTH=200, CODECS="avc1,mp4a", ',
222
+ 'RESOLUTION=960x540, FRAME-RATE=60, AUDIO="aud1"\n',
223
+ 'video\n',
224
+ ].join('');
225
+
226
+ const media = [
227
+ '#EXTM3U\n',
228
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
229
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
230
+ '#EXTINF:5,\n',
231
+ '#EXT-X-BYTERANGE:121090@616\n',
232
+ 'main.mp4',
233
+ ].join('');
234
+
235
+ const textMedia = [
236
+ '#EXTM3U\n',
237
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
238
+ '#EXTINF:5,\n',
239
+ '#EXT-X-BYTERANGE:121090@616\n',
240
+ 'main.vtt',
241
+ ].join('');
242
+
243
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
244
+ manifest.anyTimeline();
245
+ manifest.addPartialVariant((variant) => {
246
+ variant.language = 'en';
247
+ variant.bandwidth = 200;
248
+ variant.addPartialStream(ContentType.VIDEO, (stream) => {
249
+ stream.frameRate = 60;
250
+ stream.mime('video/mp4', 'avc1');
251
+ stream.size(960, 540);
252
+ });
253
+ variant.addPartialStream(ContentType.AUDIO, (stream) => {
254
+ stream.language = 'en';
255
+ stream.channelsCount = 16;
256
+ stream.spatialAudio = true;
257
+ stream.mime('audio/mp4', 'mp4a');
258
+ });
259
+ });
260
+ manifest.addPartialTextStream((stream) => {
261
+ stream.language = 'en';
262
+ stream.kind = TextStreamKind.SUBTITLE;
263
+ stream.mime('text/vtt', '');
264
+ });
265
+ manifest.addPartialTextStream((stream) => {
266
+ stream.language = 'es';
267
+ stream.kind = TextStreamKind.SUBTITLE;
268
+ stream.mime('text/vtt', '');
269
+ });
270
+ });
271
+
272
+ fakeNetEngine
273
+ .setResponseText('test:/master', master)
274
+ .setResponseText('test:/audio', media)
275
+ .setResponseText('test:/video', media)
276
+ .setResponseText('test:/text', textMedia)
277
+ .setResponseText('test:/text2', textMedia)
278
+ .setResponseText('test:/main.vtt', vttText)
279
+ .setResponseValue('test:/init.mp4', initSegmentData)
280
+ .setResponseValue('test:/main.mp4', segmentData);
281
+
282
+ const actual = await parser.start('test:/master', playerInterface);
283
+ expect(actual).toEqual(manifest);
284
+ });
285
+
212
286
  it('prioritize AVERAGE-BANDWIDTH to BANDWIDTH', async () => {
213
287
  const master = [
214
288
  '#EXTM3U\n',
@@ -783,7 +857,7 @@ describe('HlsParser', () => {
783
857
  'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud2"\n',
784
858
  'video2\n',
785
859
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
786
- 'URI="audio"\n',
860
+ 'DEFAULT=YES,URI="audio"\n',
787
861
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",LANGUAGE="fr",',
788
862
  'URI="audio2"\n',
789
863
  ].join('');
@@ -801,6 +875,7 @@ describe('HlsParser', () => {
801
875
  manifest.anyTimeline();
802
876
  manifest.addPartialVariant((variant) => {
803
877
  variant.bandwidth = 200;
878
+ variant.primary = true;
804
879
  variant.addPartialStream(ContentType.VIDEO, (stream) => {
805
880
  stream.size(960, 540);
806
881
  });
@@ -810,6 +885,7 @@ describe('HlsParser', () => {
810
885
  });
811
886
  manifest.addPartialVariant((variant) => {
812
887
  variant.bandwidth = 300;
888
+ variant.primary = false;
813
889
  variant.addPartialStream(ContentType.VIDEO, (stream) => {
814
890
  stream.size(960, 540);
815
891
  });
@@ -1307,6 +1383,8 @@ describe('HlsParser', () => {
1307
1383
  // stream.
1308
1384
  const timeline = actual.presentationTimeline;
1309
1385
  expect(timeline.getDuration()).toBe(10);
1386
+ expect(timeline.getSeekRangeStart()).toBe(0);
1387
+ expect(timeline.getSeekRangeEnd()).toBe(10);
1310
1388
 
1311
1389
  expect(actual.textStreams.length).toBe(1);
1312
1390
  expect(actual.variants.length).toBe(1);
@@ -0,0 +1,20 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ goog.require('shaka.media.ClosedCaptionParser');
8
+ goog.require('shaka.test.Util');
9
+
10
+ describe('ClosedCaptionParser', () => {
11
+ it('can handle empty caption packets', async () => {
12
+ const initSegment = await shaka.test.Util.fetch(
13
+ 'base/test/test/assets/empty_caption_video_init.mp4');
14
+ const videoSegment = await shaka.test.Util.fetch(
15
+ 'base/test/test/assets/empty_caption_video_segment.mp4');
16
+ const parser = new shaka.media.ClosedCaptionParser();
17
+ parser.init(initSegment);
18
+ parser.parseFrom(videoSegment);
19
+ });
20
+ });
@@ -2278,6 +2278,49 @@ function testDrmEngine(useMediaCapabilities) {
2278
2278
  [drmInfoAudio]);
2279
2279
  expect(returned).toEqual([drmInfoDesired]);
2280
2280
  });
2281
+
2282
+ it('dedupes the merged init data based on keyId matching', () => {
2283
+ const serverCert = new Uint8Array(0);
2284
+ const drmInfoVideo = {
2285
+ keySystem: 'drm.abc',
2286
+ licenseServerUri: 'http://abc.drm/license',
2287
+ distinctiveIdentifierRequired: false,
2288
+ persistentStateRequired: true,
2289
+ videoRobustness: 'really_really_ridiculously_good',
2290
+ serverCertificate: serverCert,
2291
+ serverCertificateUri: '',
2292
+ initData: [{keyId: 'v-init'}],
2293
+ keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']),
2294
+ };
2295
+ const drmInfoAudio = {
2296
+ keySystem: 'drm.abc',
2297
+ licenseServerUri: undefined,
2298
+ distinctiveIdentifierRequired: true,
2299
+ persistentStateRequired: false,
2300
+ audioRobustness: 'good',
2301
+ serverCertificate: undefined,
2302
+ serverCertificateUri: '',
2303
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
2304
+ keyIds: new Set(['eadbeefdeadbeefdeadbeefdeadbeefd']),
2305
+ };
2306
+ const drmInfoDesired = {
2307
+ keySystem: 'drm.abc',
2308
+ licenseServerUri: 'http://abc.drm/license',
2309
+ distinctiveIdentifierRequired: true,
2310
+ persistentStateRequired: true,
2311
+ audioRobustness: 'good',
2312
+ videoRobustness: 'really_really_ridiculously_good',
2313
+ serverCertificate: serverCert,
2314
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
2315
+ keyIds: new Set([
2316
+ 'deadbeefdeadbeefdeadbeefdeadbeef',
2317
+ 'eadbeefdeadbeefdeadbeefdeadbeefd',
2318
+ ]),
2319
+ };
2320
+ const returned = shaka.media.DrmEngine.getCommonDrmInfos([drmInfoVideo],
2321
+ [drmInfoAudio]);
2322
+ expect(returned).toEqual([drmInfoDesired]);
2323
+ });
2281
2324
  }); // describe('getCommonDrmInfos')
2282
2325
 
2283
2326
  describe('configure', () => {
@@ -2412,6 +2455,19 @@ function testDrmEngine(useMediaCapabilities) {
2412
2455
  await expectAsync(drmEngine.removeSession('abc'))
2413
2456
  .toBeRejectedWith(expected);
2414
2457
  });
2458
+
2459
+ // Regression test for #3534
2460
+ it('does not remove the same session again on destroy', async () => {
2461
+ updatePromise.resolve();
2462
+ expect(session1.remove).not.toHaveBeenCalled();
2463
+ await drmEngine.removeSession('abc');
2464
+ expect(session1.remove).toHaveBeenCalled();
2465
+ session1.remove.calls.reset();
2466
+ await drmEngine.destroy();
2467
+ // The session should only be removed ONCE. If it's double-removed, it
2468
+ // will make a (non-fatal) DOMException.
2469
+ expect(session1.remove).not.toHaveBeenCalled();
2470
+ });
2415
2471
  });
2416
2472
 
2417
2473
  describe('expiration', () => {
@@ -1296,12 +1296,13 @@ describe('Playhead', () => {
1296
1296
  });
1297
1297
 
1298
1298
  // Regression test for https://github.com/google/shaka-player/issues/2987
1299
- it('does gap jump if paused at 0', () => {
1299
+ it('does gap jump if paused at 0 and has autoplay', () => {
1300
1300
  const buffered = [{start: 10, end: 20}];
1301
1301
  video.buffered = createFakeBuffered(buffered);
1302
1302
  video.currentTime = 0;
1303
1303
  video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA;
1304
1304
  video.paused = true;
1305
+ video.autoplay = true;
1305
1306
 
1306
1307
  config.jumpLargeGaps = true;
1307
1308
  playhead = new shaka.media.MediaSourcePlayhead(
@@ -1319,6 +1320,31 @@ describe('Playhead', () => {
1319
1320
  expect(video.currentTime).toBe(10);
1320
1321
  });
1321
1322
 
1323
+ // Regression test for https://github.com/google/shaka-player/issues/3451
1324
+ it('doesn\'t gap jump if paused at 0 and hasn\'t autoplay', () => {
1325
+ const buffered = [{start: 10, end: 20}];
1326
+ video.buffered = createFakeBuffered(buffered);
1327
+ video.currentTime = 0;
1328
+ video.readyState = HTMLMediaElement.HAVE_ENOUGH_DATA;
1329
+ video.paused = true;
1330
+ video.autoplay = false;
1331
+
1332
+ config.jumpLargeGaps = true;
1333
+ playhead = new shaka.media.MediaSourcePlayhead(
1334
+ video,
1335
+ manifest,
1336
+ config,
1337
+ /* startTime= */ 0,
1338
+ Util.spyFunc(onSeek),
1339
+ Util.spyFunc(onEvent));
1340
+
1341
+ playhead.notifyOfBufferingChange();
1342
+ jasmine.clock().tick(500);
1343
+
1344
+ // There should NOT have been a gap jump.
1345
+ expect(video.currentTime).toBe(0);
1346
+ });
1347
+
1322
1348
  /**
1323
1349
  * @param {string} name
1324
1350
  * @param {SeekTestInfo} data
@@ -15,6 +15,9 @@ describe('RegionObserver', () => {
15
15
  /** @type {!shaka.media.RegionObserver} */
16
16
  let observer;
17
17
 
18
+ /** @type {!jasmine.Spy} */
19
+ let onSeekRange;
20
+
18
21
  /** @type {!jasmine.Spy} */
19
22
  let onEnterRegion;
20
23
  /** @type {!jasmine.Spy} */
@@ -23,18 +26,26 @@ describe('RegionObserver', () => {
23
26
  let onSkipRegion;
24
27
 
25
28
  beforeEach(() => {
29
+ onSeekRange = jasmine.createSpy('onSeekRange');
30
+ onSeekRange.and.returnValue({start: 0, end: 100});
31
+
26
32
  onEnterRegion = jasmine.createSpy('onEnterRegion');
27
33
  onExitRegion = jasmine.createSpy('onExitRegion');
28
34
  onSkipRegion = jasmine.createSpy('onSkipRegion');
29
35
 
30
36
  timeline = new shaka.media.RegionTimeline(
31
- () => { return {start: 0, end: 100}; });
37
+ shaka.test.Util.spyFunc(onSeekRange));
32
38
 
33
39
  observer = new shaka.media.RegionObserver(timeline);
34
- observer.setListeners(
35
- /* onEnter= */ shaka.test.Util.spyFunc(onEnterRegion),
36
- /* onExit= */ shaka.test.Util.spyFunc(onExitRegion),
37
- /* onSkip= */ shaka.test.Util.spyFunc(onSkipRegion));
40
+ observer.addEventListener('enter', (event) => {
41
+ shaka.test.Util.spyFunc(onEnterRegion)(event['region'], event['seeking']);
42
+ });
43
+ observer.addEventListener('exit', (event) => {
44
+ shaka.test.Util.spyFunc(onExitRegion)(event['region'], event['seeking']);
45
+ });
46
+ observer.addEventListener('skip', (event) => {
47
+ shaka.test.Util.spyFunc(onSkipRegion)(event['region'], event['seeking']);
48
+ });
38
49
  });
39
50
 
40
51
  it('fires enter event when adding a region the playhead is in', () => {
@@ -313,6 +324,30 @@ describe('RegionObserver', () => {
313
324
  expect(onSkipRegion).not.toHaveBeenCalled();
314
325
  });
315
326
 
327
+ // See https://github.com/google/shaka-player/issues/3949
328
+ it('cleans up references to regions', async () => {
329
+ const region = createRegion('my-region', 0, 10);
330
+ timeline.addRegion(region);
331
+
332
+ // Poll so that RegionObserver will store regions.
333
+ poll(observer,
334
+ /* timeInSeconds= */ 5,
335
+ /* seeking= */ false);
336
+
337
+ // Hack to get a private member without the compiler complaining:
338
+ /** @type {Map} */
339
+ const regionMap = (/** @type {?} */(observer))['oldPosition_'];
340
+ expect(regionMap.size).not.toBe(0);
341
+
342
+ // Move the seek range so that the region will be removed.
343
+ onSeekRange.and.returnValue({start: 20, end: 100});
344
+ // Give the timeline time to filter regions
345
+ await shaka.test.Util.delay(
346
+ shaka.media.RegionTimeline.REGION_FILTER_INTERVAL * 2);
347
+
348
+ expect(regionMap.size).toBe(0);
349
+ });
350
+
316
351
  /**
317
352
  * @param {string} id
318
353
  * @param {number} startTimeSeconds
@@ -14,18 +14,27 @@ describe('RegionTimeline', () => {
14
14
  /** @type {!jasmine.Spy} */
15
15
  let onNewRegion;
16
16
 
17
+ /** @type {!jasmine.Spy} */
18
+ let onRemoveRegion;
19
+
17
20
  /** @type {!jasmine.Spy} */
18
21
  let onSeekRange;
19
22
 
20
23
  beforeEach(() => {
21
- onNewRegion = jasmine.createSpy('onNewRegion');
22
-
23
24
  onSeekRange = jasmine.createSpy('onSeekRange');
24
25
  onSeekRange.and.returnValue({start: 0, end: 100});
25
-
26
26
  timeline = new shaka.media.RegionTimeline(
27
27
  shaka.test.Util.spyFunc(onSeekRange));
28
- timeline.setListeners(shaka.test.Util.spyFunc(onNewRegion));
28
+
29
+ onNewRegion = jasmine.createSpy('onNewRegion');
30
+ timeline.addEventListener('regionadd', (event) => {
31
+ shaka.test.Util.spyFunc(onNewRegion)(event['region']);
32
+ });
33
+
34
+ onRemoveRegion = jasmine.createSpy('onRemoveRegion');
35
+ timeline.addEventListener('regionremove', (event) => {
36
+ shaka.test.Util.spyFunc(onRemoveRegion)(event['region']);
37
+ });
29
38
  });
30
39
 
31
40
  afterEach(() => {
@@ -85,24 +94,33 @@ describe('RegionTimeline', () => {
85
94
  ]);
86
95
  });
87
96
 
88
- it('filters out regions that end before the start of the seek range',
89
- async () => {
90
- onSeekRange.and.returnValue({start: 5, end: 100});
91
- const region1 = createRegion('urn:foo', 'my-region', 0, 3);
92
- const region2 = createRegion('urn:foo', 'my-region', 3, 10);
93
- const region3 = createRegion('urn:foo', 'my-region', 5, 10);
94
- timeline.addRegion(region1);
95
- timeline.addRegion(region2);
96
- timeline.addRegion(region3);
97
- expect(onNewRegion).toHaveBeenCalledTimes(3);
98
- let regions = Array.from(timeline.regions());
99
- expect(regions.length).toBe(3);
100
- // Give the timeline time to filter regions
101
- await shaka.test.Util.delay(
102
- shaka.media.RegionTimeline.REGION_FILTER_INTERVAL * 2);
103
- regions = Array.from(timeline.regions());
104
- expect(regions).toEqual([region2, region3]);
105
- });
97
+ it('filters regions that end before the seek range', async () => {
98
+ onSeekRange.and.returnValue({start: 5, end: 100});
99
+
100
+ const region1 = createRegion('urn:foo', 'my-region', 0, 3);
101
+ const region2 = createRegion('urn:foo', 'my-region', 3, 10);
102
+ const region3 = createRegion('urn:foo', 'my-region', 5, 10);
103
+
104
+ timeline.addRegion(region1);
105
+ timeline.addRegion(region2);
106
+ timeline.addRegion(region3);
107
+ expect(onNewRegion).toHaveBeenCalledTimes(3);
108
+ expect(onNewRegion).toHaveBeenCalledWith(region1);
109
+ expect(onNewRegion).toHaveBeenCalledWith(region2);
110
+ expect(onNewRegion).toHaveBeenCalledWith(region3);
111
+
112
+ let regions = Array.from(timeline.regions());
113
+ expect(regions.length).toBe(3);
114
+
115
+ // Give the timeline time to filter regions
116
+ await shaka.test.Util.delay(
117
+ shaka.media.RegionTimeline.REGION_FILTER_INTERVAL * 2);
118
+
119
+ regions = Array.from(timeline.regions());
120
+ expect(regions).toEqual([region2, region3]);
121
+
122
+ expect(onRemoveRegion).toHaveBeenCalledWith(region1);
123
+ });
106
124
 
107
125
  /**
108
126
  * @param {string} schemeIdUri
@@ -419,8 +419,9 @@ describe('StreamingEngine', () => {
419
419
  // 3. Playhead seeks to force us back inside the window
420
420
  // 4. (maybe) seek if there is a gap at the period boundary
421
421
  // 5. (maybe) seek to flush a pipeline stall
422
+ // 6. (maybe) on slower platforms (e.g. GitHub actions)
422
423
  expect(seekCount).toBeGreaterThan(2);
423
- expect(seekCount).toBeLessThan(6);
424
+ expect(seekCount).toBeLessThan(7);
424
425
  });
425
426
  });
426
427
 
@@ -97,6 +97,9 @@ describe('StreamingEngine', () => {
97
97
  /** @type {!shaka.media.StreamingEngine} */
98
98
  let streamingEngine;
99
99
 
100
+ /** @type {function(function(), number)} */
101
+ let realSetTimeout;
102
+
100
103
  /**
101
104
  * Runs the fake event loop.
102
105
  * @param {function()=} callback An optional callback that is executed
@@ -116,6 +119,7 @@ describe('StreamingEngine', () => {
116
119
  }
117
120
 
118
121
  beforeAll(() => {
122
+ realSetTimeout = window.setTimeout;
119
123
  jasmine.clock().install();
120
124
  jasmine.clock().mockDate();
121
125
  });
@@ -822,6 +826,53 @@ describe('StreamingEngine', () => {
822
826
  .toHaveBeenCalledWith('video', 0, lt20, gt40);
823
827
  });
824
828
 
829
+ // Regression test for https://github.com/google/shaka-player/issues/3717
830
+ it('applies fudge factors for the duration', async () => {
831
+ setupVod();
832
+
833
+ // In #3717, the duration was just barely large enough to encompass an
834
+ // additional segment, but that segment didn't exist, so playback never
835
+ // completed. Here, we set the duration to just beyond the 3rd segment, and
836
+ // we make the 4th segment fail when requested.
837
+ const duration = 30.000000005;
838
+ timeline.getDuration.and.returnValue(duration);
839
+
840
+ const targetUri = '1_video_3'; // The URI of the 4th video segment.
841
+ failRequestsForTarget(netEngine, targetUri);
842
+
843
+ mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
844
+ createStreamingEngine();
845
+
846
+ // Here we go!
847
+ streamingEngine.switchVariant(variant);
848
+ streamingEngine.switchTextStream(textStream);
849
+ await streamingEngine.start();
850
+ playing = true;
851
+ await runTest();
852
+
853
+ // The end of the stream should have been reached, and the 4th segment from
854
+ // each type should never have been requested.
855
+ expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
856
+
857
+ const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
858
+
859
+ netEngine.expectRequest('0_audio_0', segmentType);
860
+ netEngine.expectRequest('0_video_0', segmentType);
861
+ netEngine.expectRequest('0_text_0', segmentType);
862
+
863
+ netEngine.expectRequest('0_audio_1', segmentType);
864
+ netEngine.expectRequest('0_video_1', segmentType);
865
+ netEngine.expectRequest('0_text_1', segmentType);
866
+
867
+ netEngine.expectRequest('1_audio_2', segmentType);
868
+ netEngine.expectRequest('1_video_2', segmentType);
869
+ netEngine.expectRequest('1_text_2', segmentType);
870
+
871
+ netEngine.expectNoRequest('1_audio_3', segmentType);
872
+ netEngine.expectNoRequest('1_video_3', segmentType);
873
+ netEngine.expectNoRequest('1_text_3', segmentType);
874
+ });
875
+
825
876
  it('does not buffer one media type ahead of another', async () => {
826
877
  setupVod();
827
878
  mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
@@ -1389,6 +1440,66 @@ describe('StreamingEngine', () => {
1389
1440
  });
1390
1441
  });
1391
1442
 
1443
+ it('into unbuffered regions when nothing is buffered ' +
1444
+ 'and mediaState is performing an update', async () => {
1445
+ // Here we go!
1446
+ streamingEngine.switchVariant(variant);
1447
+ streamingEngine.switchTextStream(textStream);
1448
+ // ensure init source buffer promise does not resolve before seeked()
1449
+ // so mediaState remains in "performingUpdate" state
1450
+ const initSourceBufferPromise = new shaka.util.PublicPromise();
1451
+ mediaSourceEngine.setStreamProperties.and
1452
+ .returnValue(initSourceBufferPromise);
1453
+ await streamingEngine.start();
1454
+ playing = true;
1455
+
1456
+ // tick to trigger mediaState updates
1457
+ jasmine.clock().tick(1);
1458
+ // give a chance for fetchAndAppend_ to be invoked
1459
+ // by async onUpdate_ callback
1460
+ await Util.shortDelay(realSetTimeout);
1461
+
1462
+ // Nothing is buffered yet.
1463
+ expect(mediaSourceEngine.segments).toEqual({
1464
+ audio: [false, false, false, false],
1465
+ video: [false, false, false, false],
1466
+ text: [false, false, false, false],
1467
+ });
1468
+
1469
+ // Seek forward to an unbuffered region in the first Period.
1470
+ presentationTimeInSeconds = 15;
1471
+ streamingEngine.seeked();
1472
+
1473
+ // resolve initSourceBufferPromise after seeked(), waitingToClearBuffer
1474
+ // should have been set, so this will now trigger the actual flush of
1475
+ // the buffer
1476
+ initSourceBufferPromise.resolve();
1477
+ mediaSourceEngine.setStreamProperties.and
1478
+ .returnValue(Promise.resolve());
1479
+
1480
+ // allow mediaState update to resolve
1481
+ await Util.shortDelay(realSetTimeout);
1482
+
1483
+ onTick.and.callFake(() => {
1484
+ expect(mediaSourceEngine.clear).toHaveBeenCalled();
1485
+ onTick.and.stub();
1486
+ });
1487
+
1488
+ await runTest(Util.spyFunc(onTick));
1489
+
1490
+ // Verify buffers.
1491
+ expect(mediaSourceEngine.initSegments).toEqual({
1492
+ audio: [false, true],
1493
+ video: [false, true],
1494
+ text: [],
1495
+ });
1496
+ expect(mediaSourceEngine.segments).toEqual({
1497
+ audio: [false, true, true, true],
1498
+ video: [false, true, true, true],
1499
+ text: [false, true, true, true],
1500
+ });
1501
+ });
1502
+
1392
1503
  it('into unbuffered regions near segment start', async () => {
1393
1504
  // Here we go!
1394
1505
  streamingEngine.switchVariant(variant);
@@ -3388,6 +3499,40 @@ describe('StreamingEngine', () => {
3388
3499
  segmentAvailability.end++;
3389
3500
  }
3390
3501
 
3502
+ /**
3503
+ * @param {!shaka.test.FakeNetworkingEngine} netEngine A NetworkingEngine
3504
+ * look-alike.
3505
+ * @param {string} targetUri
3506
+ * @param {shaka.util.Error.Code=} errorCode
3507
+ */
3508
+ function failRequestsForTarget(
3509
+ netEngine, targetUri, errorCode=shaka.util.Error.Code.BAD_HTTP_STATUS) {
3510
+ // eslint-disable-next-line no-restricted-syntax
3511
+ const originalNetEngineRequest = netEngine.request.bind(netEngine);
3512
+
3513
+ netEngine.request = jasmine.createSpy('request').and.callFake(
3514
+ (requestType, request) => {
3515
+ if (request.uris[0] == targetUri) {
3516
+ const data = [targetUri];
3517
+
3518
+ if (errorCode == shaka.util.Error.Code.BAD_HTTP_STATUS) {
3519
+ data.push(404);
3520
+ data.push('');
3521
+ }
3522
+
3523
+ // The compiler still sees the error code parameter as potentially
3524
+ // undefined, even though we gave it a default value.
3525
+ goog.asserts.assert(errorCode != undefined, 'Undefined error code');
3526
+
3527
+ return shaka.util.AbortableOperation.failed(new shaka.util.Error(
3528
+ shaka.util.Error.Severity.CRITICAL,
3529
+ shaka.util.Error.Category.NETWORK,
3530
+ errorCode, data));
3531
+ }
3532
+ return originalNetEngineRequest(requestType, request);
3533
+ });
3534
+ }
3535
+
3391
3536
  /**
3392
3537
  * @param {!shaka.test.FakeNetworkingEngine} netEngine A NetworkingEngine
3393
3538
  * look-alike.
@@ -652,6 +652,44 @@ describe('Player', () => {
652
652
  });
653
653
  });
654
654
 
655
+ describe('seek range', () => {
656
+ // Regression test for Issue #3675.
657
+ it('has a reasonable seek range after going from live to VOD', async () => {
658
+ const netEngine = player.getNetworkingEngine();
659
+ const startTime = Date.now();
660
+ netEngine.registerRequestFilter((type, request) => {
661
+ if (type != shaka.net.NetworkingEngine.RequestType.MANIFEST) {
662
+ return;
663
+ }
664
+ // Simulate a live stream by providing different manifests over time.
665
+ const time = (Date.now() - startTime) / 1000;
666
+ const manifestNumber = Math.min(5, Math.floor(0.5 + time / 2));
667
+ request.uris = [
668
+ '/base/test/test/assets/3675/dash_' + manifestNumber + '.mpd',
669
+ ];
670
+ console.log('getting manifest', request.uris);
671
+ });
672
+
673
+ // Play the stream.
674
+ await player.load('/base/test/test/assets/3675/dash_0.mpd');
675
+ video.play();
676
+
677
+ // Wait for the stream to be over.
678
+ eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
679
+ /** @type {shaka.test.Waiter} */
680
+ const waiter = new shaka.test.Waiter(eventManager)
681
+ .timeoutAfter(20).failOnTimeout(true);
682
+ await waiter.waitForEnd(video);
683
+
684
+ // The stream should have transitioned to VOD by now.
685
+ expect(player.isLive()).toBe(false);
686
+
687
+ // Check that the final seek range is as expected.
688
+ const seekRange = player.seekRange();
689
+ expect(seekRange.end).toBe(14);
690
+ });
691
+ });
692
+
655
693
  describe('buffering', () => {
656
694
  const startBuffering = jasmine.objectContaining({buffering: true});
657
695
  const endBuffering = jasmine.objectContaining({buffering: false});