shaka-player 3.2.2 → 3.3.2

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 (188) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +1 -1
  3. package/.github/ISSUE_TEMPLATE/question.md +1 -1
  4. package/.github/workflows/build_and_test.yaml +73 -0
  5. package/.github/workflows/release.yaml +53 -0
  6. package/AUTHORS +6 -5
  7. package/CHANGELOG.md +314 -1
  8. package/CONTRIBUTORS +4 -2
  9. package/build/checkversion.py +1 -1
  10. package/build/conformance.textproto +183 -169
  11. package/build/test.py +6 -0
  12. package/build/types/core +3 -0
  13. package/build/types/offline +1 -0
  14. package/build/types/polyfill +1 -0
  15. package/build/types/ui +2 -0
  16. package/demo/common/assets.js +18 -0
  17. package/demo/common/message_ids.js +13 -1
  18. package/demo/config.js +31 -1
  19. package/demo/index.html +1 -1
  20. package/demo/locales/en.json +12 -0
  21. package/demo/locales/source.json +48 -0
  22. package/dist/controls.css +26 -2
  23. package/dist/controls.css.map +1 -1
  24. package/dist/demo.compiled.debug.js +19 -16
  25. package/dist/demo.compiled.debug.map +3 -3
  26. package/dist/demo.compiled.js +19 -16
  27. package/dist/demo.compiled.map +3 -3
  28. package/dist/deps.js +21 -14
  29. package/dist/locales.js +31 -29
  30. package/dist/receiver.compiled.debug.js +11 -9
  31. package/dist/receiver.compiled.debug.map +3 -3
  32. package/dist/receiver.compiled.js +11 -9
  33. package/dist/receiver.compiled.map +3 -3
  34. package/dist/shaka-player.compiled.d.ts +232 -18
  35. package/dist/shaka-player.compiled.debug.d.ts +232 -18
  36. package/dist/shaka-player.compiled.debug.externs.js +293 -17
  37. package/dist/shaka-player.compiled.debug.js +481 -422
  38. package/dist/shaka-player.compiled.debug.map +4 -4
  39. package/dist/shaka-player.compiled.externs.js +293 -17
  40. package/dist/shaka-player.compiled.js +846 -817
  41. package/dist/shaka-player.compiled.map +4 -4
  42. package/dist/shaka-player.ui.d.ts +250 -30
  43. package/dist/shaka-player.ui.debug.d.ts +250 -30
  44. package/dist/shaka-player.ui.debug.externs.js +322 -28
  45. package/dist/shaka-player.ui.debug.js +630 -553
  46. package/dist/shaka-player.ui.debug.map +4 -4
  47. package/dist/shaka-player.ui.externs.js +322 -28
  48. package/dist/shaka-player.ui.js +1048 -1006
  49. package/dist/shaka-player.ui.map +4 -4
  50. package/docs/design/bg-fetch-after.gv +29 -0
  51. package/docs/design/bg-fetch-after.gv.png +0 -0
  52. package/docs/design/bg-fetch-before.gv +18 -0
  53. package/docs/design/bg-fetch-before.gv.png +0 -0
  54. package/docs/design/bg-fetch.md +134 -0
  55. package/docs/tutorials/blob-url.md +15 -0
  56. package/docs/tutorials/index.json +1 -0
  57. package/docs/tutorials/ui-customization.md +66 -0
  58. package/docs/tutorials/ui.md +5 -5
  59. package/externs/cmcd.js +186 -0
  60. package/externs/ima.js +3 -0
  61. package/externs/shaka/ads.js +5 -0
  62. package/externs/shaka/manifest.js +12 -1
  63. package/externs/shaka/manifest_parser.js +10 -1
  64. package/externs/shaka/net.js +20 -4
  65. package/externs/shaka/offline.js +31 -2
  66. package/externs/shaka/player.js +138 -5
  67. package/externs/shaka/text.js +24 -0
  68. package/externs/webcrypto.js +18 -0
  69. package/karma.conf.js +19 -15
  70. package/lib/abr/ewma.js +14 -0
  71. package/lib/abr/ewma_bandwidth_estimator.js +16 -2
  72. package/lib/abr/simple_abr_manager.js +6 -0
  73. package/lib/ads/ad_manager.js +1 -0
  74. package/lib/ads/client_side_ad.js +8 -0
  75. package/lib/ads/client_side_ad_manager.js +8 -4
  76. package/lib/ads/server_side_ad.js +7 -0
  77. package/lib/cast/cast_proxy.js +3 -0
  78. package/lib/cast/cast_receiver.js +3 -0
  79. package/lib/cast/cast_utils.js +1 -0
  80. package/lib/dash/dash_parser.js +79 -33
  81. package/lib/dash/segment_base.js +25 -1
  82. package/lib/dash/segment_list.js +18 -9
  83. package/lib/dash/segment_template.js +23 -10
  84. package/lib/hls/hls_parser.js +116 -23
  85. package/lib/hls/manifest_text_parser.js +1 -0
  86. package/lib/media/quality_observer.js +274 -0
  87. package/lib/media/region_observer.js +39 -33
  88. package/lib/media/region_timeline.js +17 -19
  89. package/lib/media/segment_index.js +8 -1
  90. package/lib/media/segment_reference.js +37 -4
  91. package/lib/media/streaming_engine.js +47 -6
  92. package/lib/net/http_fetch_plugin.js +31 -11
  93. package/lib/net/http_plugin_utils.js +1 -0
  94. package/lib/net/http_xhr_plugin.js +40 -14
  95. package/lib/net/networking_engine.js +85 -18
  96. package/lib/offline/download_info.js +68 -0
  97. package/lib/offline/download_manager.js +13 -5
  98. package/lib/offline/indexeddb/base_storage_cell.js +35 -0
  99. package/lib/offline/indexeddb/storage_mechanism.js +1 -1
  100. package/lib/offline/indexeddb/v5_storage_cell.js +5 -0
  101. package/lib/offline/storage.js +401 -293
  102. package/lib/offline/stored_content_utils.js +2 -0
  103. package/lib/player.js +254 -27
  104. package/lib/polyfill/media_capabilities.js +30 -15
  105. package/lib/polyfill/random_uuid.js +52 -0
  106. package/lib/text/cue.js +18 -0
  107. package/lib/text/ttml_text_parser.js +109 -60
  108. package/lib/text/ui_text_displayer.js +19 -13
  109. package/lib/util/cmcd_manager.js +672 -0
  110. package/lib/util/error.js +6 -0
  111. package/lib/util/fake_event_target.js +21 -1
  112. package/lib/util/language_utils.js +12 -0
  113. package/lib/util/mutex.js +52 -0
  114. package/lib/util/periods.js +98 -64
  115. package/lib/util/player_configuration.js +18 -0
  116. package/lib/util/stream_utils.js +86 -10
  117. package/lib/util/xml_utils.js +19 -0
  118. package/package.json +3 -6
  119. package/shaka-player.uncompiled.js +3 -0
  120. package/test/dash/dash_parser_content_protection_unit.js +2 -0
  121. package/test/dash/dash_parser_live_unit.js +144 -1
  122. package/test/dash/dash_parser_manifest_unit.js +37 -2
  123. package/test/dash/dash_parser_segment_base_unit.js +2 -0
  124. package/test/dash/dash_parser_segment_list_unit.js +2 -1
  125. package/test/dash/dash_parser_segment_template_unit.js +2 -1
  126. package/test/hls/hls_live_unit.js +2 -0
  127. package/test/hls/hls_parser_unit.js +76 -0
  128. package/test/media/drm_engine_unit.js +3 -3
  129. package/test/media/quality_observer_unit.js +142 -0
  130. package/test/media/region_observer_unit.js +40 -5
  131. package/test/media/region_timeline_unit.js +40 -22
  132. package/test/media/segment_reference_unit.js +16 -1
  133. package/test/media/streaming_engine_integration.js +4 -2
  134. package/test/media/streaming_engine_unit.js +147 -0
  135. package/test/net/http_plugin_unit.js +20 -15
  136. package/test/offline/storage_integration.js +108 -4
  137. package/test/player_integration.js +140 -8
  138. package/test/player_src_equals_integration.js +41 -8
  139. package/test/player_unit.js +19 -9
  140. package/test/test/assets/chapters.vtt +2 -2
  141. package/test/test/assets/chapters2.vtt +10 -0
  142. package/test/test/util/dash_parser_util.js +4 -0
  143. package/test/test/util/fake_ad.js +7 -0
  144. package/test/test/util/fake_drm_engine.js +9 -2
  145. package/test/test/util/manifest_generator.js +3 -2
  146. package/test/test/util/test_scheme.js +14 -1
  147. package/test/test/util/ttml_utils.js +81 -0
  148. package/test/test/util/util.js +10 -7
  149. package/test/text/mp4_ttml_parser_unit.js +11 -18
  150. package/test/text/ttml_text_parser_unit.js +544 -228
  151. package/test/ui/text_displayer_layout_unit.js +8 -7
  152. package/test/ui/ui_unit.js +250 -0
  153. package/test/util/cmcd_manager_unit.js +291 -0
  154. package/test/util/stream_utils_unit.js +36 -30
  155. package/third_party/closure-uri/uri.js +21 -0
  156. package/ui/airplay_button.js +1 -0
  157. package/ui/audio_language_selection.js +7 -0
  158. package/ui/cast_button.js +1 -0
  159. package/ui/context_menu.js +109 -0
  160. package/ui/controls.js +44 -7
  161. package/ui/controls.less +1 -0
  162. package/ui/enums.js +4 -0
  163. package/ui/externs/ui.js +28 -1
  164. package/ui/fast_forward_button.js +14 -5
  165. package/ui/fullscreen_button.js +1 -0
  166. package/ui/less/buttons.less +0 -4
  167. package/ui/less/containers.less +74 -0
  168. package/ui/less/overflow_menu.less +0 -10
  169. package/ui/less/tooltip.less +97 -0
  170. package/ui/locales/en.json +1 -0
  171. package/ui/locales/source.json +4 -0
  172. package/ui/localization.js +12 -36
  173. package/ui/loop_button.js +5 -0
  174. package/ui/mute_button.js +2 -1
  175. package/ui/overflow_menu.js +5 -25
  176. package/ui/pip_button.js +5 -0
  177. package/ui/play_button.js +2 -2
  178. package/ui/playback_rate_selection.js +8 -9
  179. package/ui/resolution_selection.js +7 -0
  180. package/ui/rewind_button.js +15 -6
  181. package/ui/seek_bar.js +4 -2
  182. package/ui/settings_menu.js +36 -4
  183. package/ui/small_play_button.js +1 -0
  184. package/ui/statistics_button.js +253 -0
  185. package/ui/text_selection.js +12 -4
  186. package/ui/ui.js +30 -0
  187. package/ui/volume_bar.js +1 -1
  188. package/externs/media_capabilities.js +0 -16
@@ -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
@@ -38,14 +38,29 @@ describe('SegmentReference', () => {
38
38
  });
39
39
 
40
40
  describe('InitSegmentReference', () => {
41
+ const mediaQuality = {
42
+ bandwidth: 1,
43
+ audioSamplingRate: 444000,
44
+ codecs: 'my codec',
45
+ contentType: 'video',
46
+ frameRate: 30,
47
+ height: 720,
48
+ mimeType: 'mime type',
49
+ channelsCount: 2,
50
+ pixelAspectRatio: '1:1',
51
+ width: 1280,
52
+ };
53
+
41
54
  it('returns in getters values from constructor parameters', () => {
42
55
  const reference = new shaka.media.InitSegmentReference(
43
56
  /* getUris= */ () => ['x', 'y'],
44
57
  /* startByte= */ 4,
45
- /* endByte= */ 5);
58
+ /* endByte= */ 5,
59
+ mediaQuality);
46
60
 
47
61
  expect(reference.getUris()).toEqual(['x', 'y']);
48
62
  expect(reference.getStartByte()).toBe(4);
49
63
  expect(reference.getEndByte()).toBe(5);
64
+ expect(reference.getMediaQuality()).toBe(mediaQuality);
50
65
  });
51
66
  });
@@ -60,7 +60,6 @@ describe('StreamingEngine', () => {
60
60
  /** @type {!shaka.media.StreamingEngine} */
61
61
  let streamingEngine;
62
62
 
63
-
64
63
  /** @type {shaka.extern.Variant} */
65
64
  let variant;
66
65
 
@@ -262,6 +261,7 @@ describe('StreamingEngine', () => {
262
261
 
263
262
  function createStreamingEngine() {
264
263
  const playerInterface = {
264
+ modifySegmentRequest: (request, segmentInfo) => {},
265
265
  getPresentationTime: () => playhead.getTime(),
266
266
  getBandwidthEstimate: () => 1e6,
267
267
  mediaSourceEngine: mediaSourceEngine,
@@ -270,6 +270,7 @@ describe('StreamingEngine', () => {
270
270
  onEvent: Util.spyFunc(onEvent),
271
271
  onManifestUpdate: () => {},
272
272
  onSegmentAppended: () => playhead.notifyOfBufferingChange(),
273
+ onInitSegmentAppended: () => {},
273
274
  };
274
275
  streamingEngine = new shaka.media.StreamingEngine(
275
276
  /** @type {shaka.extern.Manifest} */(manifest), playerInterface);
@@ -419,8 +420,9 @@ describe('StreamingEngine', () => {
419
420
  // 3. Playhead seeks to force us back inside the window
420
421
  // 4. (maybe) seek if there is a gap at the period boundary
421
422
  // 5. (maybe) seek to flush a pipeline stall
423
+ // 6. (maybe) on slower platforms (e.g. GitHub actions)
422
424
  expect(seekCount).toBeGreaterThan(2);
423
- expect(seekCount).toBeLessThan(6);
425
+ expect(seekCount).toBeLessThan(7);
424
426
  });
425
427
  });
426
428
 
@@ -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
  });
@@ -451,6 +455,7 @@ describe('StreamingEngine', () => {
451
455
  presentationTimeInSeconds != undefined,
452
456
  'All tests should have defined an initial presentation time by now!');
453
457
  const playerInterface = {
458
+ modifySegmentRequest: (request, segmentInfo) => {},
454
459
  getPresentationTime: () => presentationTimeInSeconds,
455
460
  getBandwidthEstimate: Util.spyFunc(getBandwidthEstimate),
456
461
  mediaSourceEngine: mediaSourceEngine,
@@ -459,6 +464,7 @@ describe('StreamingEngine', () => {
459
464
  onEvent: Util.spyFunc(onEvent),
460
465
  onManifestUpdate: Util.spyFunc(onManifestUpdate),
461
466
  onSegmentAppended: Util.spyFunc(onSegmentAppended),
467
+ onInitSegmentAppended: () => {},
462
468
  };
463
469
  streamingEngine = new shaka.media.StreamingEngine(
464
470
  /** @type {shaka.extern.Manifest} */(manifest), playerInterface);
@@ -822,6 +828,53 @@ describe('StreamingEngine', () => {
822
828
  .toHaveBeenCalledWith('video', 0, lt20, gt40);
823
829
  });
824
830
 
831
+ // Regression test for https://github.com/google/shaka-player/issues/3717
832
+ it('applies fudge factors for the duration', async () => {
833
+ setupVod();
834
+
835
+ // In #3717, the duration was just barely large enough to encompass an
836
+ // additional segment, but that segment didn't exist, so playback never
837
+ // completed. Here, we set the duration to just beyond the 3rd segment, and
838
+ // we make the 4th segment fail when requested.
839
+ const duration = 30.000000005;
840
+ timeline.getDuration.and.returnValue(duration);
841
+
842
+ const targetUri = '1_video_3'; // The URI of the 4th video segment.
843
+ failRequestsForTarget(netEngine, targetUri);
844
+
845
+ mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
846
+ createStreamingEngine();
847
+
848
+ // Here we go!
849
+ streamingEngine.switchVariant(variant);
850
+ streamingEngine.switchTextStream(textStream);
851
+ await streamingEngine.start();
852
+ playing = true;
853
+ await runTest();
854
+
855
+ // The end of the stream should have been reached, and the 4th segment from
856
+ // each type should never have been requested.
857
+ expect(mediaSourceEngine.endOfStream).toHaveBeenCalled();
858
+
859
+ const segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
860
+
861
+ netEngine.expectRequest('0_audio_0', segmentType);
862
+ netEngine.expectRequest('0_video_0', segmentType);
863
+ netEngine.expectRequest('0_text_0', segmentType);
864
+
865
+ netEngine.expectRequest('0_audio_1', segmentType);
866
+ netEngine.expectRequest('0_video_1', segmentType);
867
+ netEngine.expectRequest('0_text_1', segmentType);
868
+
869
+ netEngine.expectRequest('1_audio_2', segmentType);
870
+ netEngine.expectRequest('1_video_2', segmentType);
871
+ netEngine.expectRequest('1_text_2', segmentType);
872
+
873
+ netEngine.expectNoRequest('1_audio_3', segmentType);
874
+ netEngine.expectNoRequest('1_video_3', segmentType);
875
+ netEngine.expectNoRequest('1_text_3', segmentType);
876
+ });
877
+
825
878
  it('does not buffer one media type ahead of another', async () => {
826
879
  setupVod();
827
880
  mediaSourceEngine = new shaka.test.FakeMediaSourceEngine(segmentData);
@@ -1389,6 +1442,66 @@ describe('StreamingEngine', () => {
1389
1442
  });
1390
1443
  });
1391
1444
 
1445
+ it('into unbuffered regions when nothing is buffered ' +
1446
+ 'and mediaState is performing an update', async () => {
1447
+ // Here we go!
1448
+ streamingEngine.switchVariant(variant);
1449
+ streamingEngine.switchTextStream(textStream);
1450
+ // ensure init source buffer promise does not resolve before seeked()
1451
+ // so mediaState remains in "performingUpdate" state
1452
+ const initSourceBufferPromise = new shaka.util.PublicPromise();
1453
+ mediaSourceEngine.setStreamProperties.and
1454
+ .returnValue(initSourceBufferPromise);
1455
+ await streamingEngine.start();
1456
+ playing = true;
1457
+
1458
+ // tick to trigger mediaState updates
1459
+ jasmine.clock().tick(1);
1460
+ // give a chance for fetchAndAppend_ to be invoked
1461
+ // by async onUpdate_ callback
1462
+ await Util.shortDelay(realSetTimeout);
1463
+
1464
+ // Nothing is buffered yet.
1465
+ expect(mediaSourceEngine.segments).toEqual({
1466
+ audio: [false, false, false, false],
1467
+ video: [false, false, false, false],
1468
+ text: [false, false, false, false],
1469
+ });
1470
+
1471
+ // Seek forward to an unbuffered region in the first Period.
1472
+ presentationTimeInSeconds = 15;
1473
+ streamingEngine.seeked();
1474
+
1475
+ // resolve initSourceBufferPromise after seeked(), waitingToClearBuffer
1476
+ // should have been set, so this will now trigger the actual flush of
1477
+ // the buffer
1478
+ initSourceBufferPromise.resolve();
1479
+ mediaSourceEngine.setStreamProperties.and
1480
+ .returnValue(Promise.resolve());
1481
+
1482
+ // allow mediaState update to resolve
1483
+ await Util.shortDelay(realSetTimeout);
1484
+
1485
+ onTick.and.callFake(() => {
1486
+ expect(mediaSourceEngine.clear).toHaveBeenCalled();
1487
+ onTick.and.stub();
1488
+ });
1489
+
1490
+ await runTest(Util.spyFunc(onTick));
1491
+
1492
+ // Verify buffers.
1493
+ expect(mediaSourceEngine.initSegments).toEqual({
1494
+ audio: [false, true],
1495
+ video: [false, true],
1496
+ text: [],
1497
+ });
1498
+ expect(mediaSourceEngine.segments).toEqual({
1499
+ audio: [false, true, true, true],
1500
+ video: [false, true, true, true],
1501
+ text: [false, true, true, true],
1502
+ });
1503
+ });
1504
+
1392
1505
  it('into unbuffered regions near segment start', async () => {
1393
1506
  // Here we go!
1394
1507
  streamingEngine.switchVariant(variant);
@@ -3388,6 +3501,40 @@ describe('StreamingEngine', () => {
3388
3501
  segmentAvailability.end++;
3389
3502
  }
3390
3503
 
3504
+ /**
3505
+ * @param {!shaka.test.FakeNetworkingEngine} netEngine A NetworkingEngine
3506
+ * look-alike.
3507
+ * @param {string} targetUri
3508
+ * @param {shaka.util.Error.Code=} errorCode
3509
+ */
3510
+ function failRequestsForTarget(
3511
+ netEngine, targetUri, errorCode=shaka.util.Error.Code.BAD_HTTP_STATUS) {
3512
+ // eslint-disable-next-line no-restricted-syntax
3513
+ const originalNetEngineRequest = netEngine.request.bind(netEngine);
3514
+
3515
+ netEngine.request = jasmine.createSpy('request').and.callFake(
3516
+ (requestType, request) => {
3517
+ if (request.uris[0] == targetUri) {
3518
+ const data = [targetUri];
3519
+
3520
+ if (errorCode == shaka.util.Error.Code.BAD_HTTP_STATUS) {
3521
+ data.push(404);
3522
+ data.push('');
3523
+ }
3524
+
3525
+ // The compiler still sees the error code parameter as potentially
3526
+ // undefined, even though we gave it a default value.
3527
+ goog.asserts.assert(errorCode != undefined, 'Undefined error code');
3528
+
3529
+ return shaka.util.AbortableOperation.failed(new shaka.util.Error(
3530
+ shaka.util.Error.Severity.CRITICAL,
3531
+ shaka.util.Error.Category.NETWORK,
3532
+ errorCode, data));
3533
+ }
3534
+ return originalNetEngineRequest(requestType, request);
3535
+ });
3536
+ }
3537
+
3391
3538
  /**
3392
3539
  * @param {!shaka.test.FakeNetworkingEngine} netEngine A NetworkingEngine
3393
3540
  * look-alike.
@@ -26,6 +26,9 @@ function httpPluginTests(usingFetch) {
26
26
  // A dummy progress callback.
27
27
  const progressUpdated = (elapsedMs, bytes, bytesRemaining) => {};
28
28
 
29
+ // A dummy headers callback.
30
+ const headersReceived = (headers) => {};
31
+
29
32
  /** @type {shaka.extern.RetryParameters} */
30
33
  let retryParameters;
31
34
 
@@ -168,7 +171,8 @@ function httpPluginTests(usingFetch) {
168
171
  request.method = 'POST';
169
172
  request.headers['BAZ'] = '123';
170
173
 
171
- await plugin(request.uris[0], request, requestType, progressUpdated)
174
+ await plugin(
175
+ request.uris[0], request, requestType, progressUpdated, headersReceived)
172
176
  .promise;
173
177
 
174
178
  const actual = mostRecentRequest();
@@ -189,8 +193,8 @@ function httpPluginTests(usingFetch) {
189
193
  request.body = null;
190
194
  request.method = 'GET';
191
195
 
192
- await plugin(request.uris[0], request, requestType, progressUpdated)
193
- .promise;
196
+ await plugin(request.uris[0], request, requestType, progressUpdated,
197
+ headersReceived).promise;
194
198
 
195
199
  const actual = jasmine.Fetch.requests.mostRecent();
196
200
  expect(actual).toBeTruthy();
@@ -205,8 +209,8 @@ function httpPluginTests(usingFetch) {
205
209
 
206
210
  const request = shaka.net.NetworkingEngine.makeRequest(
207
211
  [uri], retryParameters, Util.spyFunc(streamDataCallback));
208
- const response =
209
- await plugin(uri, request, requestType, progressUpdated).promise;
212
+ const response = await plugin(
213
+ uri, request, requestType, progressUpdated, headersReceived).promise;
210
214
 
211
215
  expect(mostRecentRequest().url).toBe(uri);
212
216
  expect(response).toBeTruthy();
@@ -297,9 +301,9 @@ function httpPluginTests(usingFetch) {
297
301
  const request = shaka.net.NetworkingEngine.makeRequest(
298
302
  ['https://foo.bar/cache'], retryParameters);
299
303
 
300
- const response =
301
- await plugin(request.uris[0], request, requestType, progressUpdated)
302
- .promise;
304
+ const response = await plugin(
305
+ request.uris[0], request, requestType, progressUpdated, headersReceived)
306
+ .promise;
303
307
  expect(response).toBeTruthy();
304
308
  expect(response.fromCache).toBe(true);
305
309
  });
@@ -312,8 +316,8 @@ function httpPluginTests(usingFetch) {
312
316
  uri = 'https://foo.bar/timeout';
313
317
  const request = shaka.net.NetworkingEngine.makeRequest(
314
318
  [uri], retryParameters);
315
- const operation = plugin(
316
- request.uris[0], request, requestType, progressUpdated);
319
+ const operation = plugin(request.uris[0], request, requestType,
320
+ progressUpdated, headersReceived);
317
321
 
318
322
  /** @type {jasmine.Fetch.RequestStub} */
319
323
  const actual = jasmine.Fetch.requests.mostRecent();
@@ -354,8 +358,8 @@ function httpPluginTests(usingFetch) {
354
358
  uri = 'https://foo.bar/';
355
359
  const request = shaka.net.NetworkingEngine.makeRequest(
356
360
  [uri], retryParameters);
357
- operation = plugin(
358
- request.uris[0], request, requestType, progressUpdated);
361
+ operation = plugin(request.uris[0], request, requestType, progressUpdated,
362
+ headersReceived);
359
363
  requestPromise = operation.promise;
360
364
  }
361
365
 
@@ -388,8 +392,8 @@ function httpPluginTests(usingFetch) {
388
392
  async function testSucceeds(uri, overrideUri) {
389
393
  const request = shaka.net.NetworkingEngine.makeRequest(
390
394
  [uri], retryParameters);
391
- const response =
392
- await plugin(uri, request, requestType, progressUpdated).promise;
395
+ const response = await plugin(
396
+ uri, request, requestType, progressUpdated, headersReceived).promise;
393
397
 
394
398
  expect(mostRecentRequest().url).toBe(uri);
395
399
  expect(response).toBeTruthy();
@@ -410,7 +414,8 @@ function httpPluginTests(usingFetch) {
410
414
  const request = shaka.net.NetworkingEngine.makeRequest(
411
415
  [uri], retryParameters);
412
416
 
413
- const p = plugin(uri, request, requestType, progressUpdated).promise;
417
+ const p = plugin(
418
+ uri, request, requestType, progressUpdated, headersReceived).promise;
414
419
  if (expected.code == shaka.util.Error.Code.TIMEOUT) {
415
420
  jasmine.clock().tick(5000);
416
421
  }
@@ -22,10 +22,12 @@ goog.require('shaka.test.ManifestGenerator');
22
22
  goog.require('shaka.test.TestScheme');
23
23
  goog.require('shaka.test.Util');
24
24
  goog.require('shaka.util.AbortableOperation');
25
+ goog.require('shaka.util.BufferUtils');
25
26
  goog.require('shaka.util.Error');
26
27
  goog.require('shaka.util.EventManager');
27
28
  goog.require('shaka.util.PlayerConfiguration');
28
29
  goog.require('shaka.util.PublicPromise');
30
+ goog.require('shaka.util.Uint8ArrayUtils');
29
31
  goog.requireType('shaka.media.SegmentReference');
30
32
 
31
33
  /** @return {boolean} */
@@ -58,12 +60,16 @@ filterDescribe('Storage', storageSupport, () => {
58
60
  const manifestWithNonZeroStartUri = 'fake:manifest-with-non-zero-start';
59
61
  const manifestWithLiveTimelineUri = 'fake:manifest-with-live-timeline';
60
62
  const manifestWithAlternateSegmentsUri = 'fake:manifest-with-alt-segments';
63
+ const manifestWithVideoInitSegmentsUri =
64
+ 'fake:manifest-with-video-init-segments';
61
65
 
66
+ const initSegmentUri = 'fake:init-segment';
62
67
  const segment1Uri = 'fake:segment-1';
63
68
  const segment2Uri = 'fake:segment-2';
64
69
  const segment3Uri = 'fake:segment-3';
65
70
  const segment4Uri = 'fake:segment-4';
66
71
 
72
+ const alternateInitSegmentUri = 'fake:alt-init-segment';
67
73
  const alternateSegment1Uri = 'fake:alt-segment-1';
68
74
  const alternateSegment2Uri = 'fake:alt-segment-2';
69
75
  const alternateSegment3Uri = 'fake:alt-segment-3';
@@ -945,6 +951,46 @@ filterDescribe('Storage', storageSupport, () => {
945
951
  }
946
952
  });
947
953
 
954
+ it('can extract DRM info from segments', async () => {
955
+ const pssh1 =
956
+ '00000028' + // atom size
957
+ '70737368' + // atom type='pssh'
958
+ '00000000' + // v0, flags=0
959
+ 'edef8ba979d64acea3c827dcd51d21ed' + // system id (Widevine)
960
+ '00000008' + // data size
961
+ '0102030405060708'; // data
962
+ const psshData1 = shaka.util.Uint8ArrayUtils.fromHex(pssh1);
963
+ const pssh2 =
964
+ '00000028' + // atom size
965
+ '70737368' + // atom type='pssh'
966
+ '00000000' + // v0, flags=0
967
+ 'edef8ba979d64acea3c827dcd51d21ed' + // system id (Widevine)
968
+ '00000008' + // data size
969
+ '1337420123456789'; // data
970
+ const psshData2 = shaka.util.Uint8ArrayUtils.fromHex(pssh2);
971
+ netEngine.setResponseValue(initSegmentUri,
972
+ shaka.util.BufferUtils.toArrayBuffer(psshData1));
973
+ netEngine.setResponseValue(alternateInitSegmentUri,
974
+ shaka.util.BufferUtils.toArrayBuffer(psshData2));
975
+
976
+ const drm = new shaka.test.FakeDrmEngine();
977
+ const drmInfo = makeDrmInfo();
978
+ drmInfo.keySystem = 'com.widevine.alpha';
979
+ drm.setDrmInfo(drmInfo);
980
+ overrideDrmAndManifest(
981
+ storage,
982
+ drm,
983
+ makeManifestWithVideoInitSegments());
984
+
985
+ const stored = await storage.store(
986
+ manifestWithVideoInitSegmentsUri, noMetadata, fakeMimeType).promise;
987
+ goog.asserts.assert(stored.offlineUri != null, 'URI should not be null!');
988
+
989
+ // The manifest chooses the alternate stream, so expect only the alt init
990
+ // segment.
991
+ expect(drm.newInitData).toHaveBeenCalledWith('cenc', psshData2);
992
+ });
993
+
948
994
  it('can store multiple assets at once', async () => {
949
995
  // Block the network so that we won't finish the first store command.
950
996
  /** @type {!shaka.util.PublicPromise} */
@@ -1234,11 +1280,11 @@ filterDescribe('Storage', storageSupport, () => {
1234
1280
  goog.asserts.assert(
1235
1281
  content.offlineUri != null, 'URI should not be null!');
1236
1282
 
1237
- /**
1238
- * @type {!Array.<number>}
1239
- */
1283
+ // We expect 5 progress events because there are 4 unique segments, plus
1284
+ // the manifest.
1285
+ /** @type {!Array.<number>}*/
1240
1286
  const progressSteps = [
1241
- 0.111, 0.222, 0.333, 0.444, 0.555, 0.666, 0.777, 0.888, 1.0,
1287
+ 0.2, 0.4, 0.6, 0.8, 1,
1242
1288
  ];
1243
1289
 
1244
1290
  const progressCallback = (content, progress) => {
@@ -1399,6 +1445,62 @@ filterDescribe('Storage', storageSupport, () => {
1399
1445
  };
1400
1446
  }
1401
1447
 
1448
+ /** @return {shaka.extern.Manifest} */
1449
+ function makeManifestWithVideoInitSegments() {
1450
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
1451
+ manifest.presentationTimeline.setDuration(20);
1452
+
1453
+ manifest.addVariant(0, (variant) => {
1454
+ variant.bandwidth = kbps(13);
1455
+ variant.addVideo(1, (stream) => {
1456
+ stream.bandwidth = kbps(13);
1457
+ stream.size(100, 200);
1458
+ });
1459
+ });
1460
+ manifest.addVariant(2, (variant) => {
1461
+ variant.bandwidth = kbps(20);
1462
+ variant.addVideo(3, (stream) => {
1463
+ stream.bandwidth = kbps(20);
1464
+ stream.size(200, 400);
1465
+ });
1466
+ });
1467
+ });
1468
+
1469
+ const stream = manifest.variants[0].video;
1470
+ goog.asserts.assert(stream, 'The first stream should exist');
1471
+ stream.encrypted = true;
1472
+ const init = new shaka.media.InitSegmentReference(
1473
+ () => [initSegmentUri], 0, null);
1474
+ const refs = [
1475
+ makeReference(segment1Uri, 0, 1),
1476
+ makeReference(segment2Uri, 1, 2),
1477
+ makeReference(segment3Uri, 2, 3),
1478
+ makeReference(segment4Uri, 3, 4),
1479
+ ];
1480
+ for (const ref of refs) {
1481
+ ref.initSegmentReference = init;
1482
+ }
1483
+ overrideSegmentIndex(stream, refs);
1484
+
1485
+ const streamAlt = manifest.variants[1].video;
1486
+ goog.asserts.assert(streamAlt, 'The second stream should exist');
1487
+ streamAlt.encrypted = true;
1488
+ const initAlt = new shaka.media.InitSegmentReference(
1489
+ () => [alternateInitSegmentUri], 0, null);
1490
+ const refsAlt = [
1491
+ makeReference(alternateSegment1Uri, 0, 1),
1492
+ makeReference(alternateSegment2Uri, 1, 2),
1493
+ makeReference(alternateSegment3Uri, 2, 3),
1494
+ makeReference(alternateSegment4Uri, 3, 4),
1495
+ ];
1496
+ for (const ref of refsAlt) {
1497
+ ref.initSegmentReference = initAlt;
1498
+ }
1499
+ overrideSegmentIndex(streamAlt, refsAlt);
1500
+
1501
+ return manifest;
1502
+ }
1503
+
1402
1504
  /** @return {shaka.extern.Manifest} */
1403
1505
  function makeManifestWithPerStreamBandwidth() {
1404
1506
  const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
@@ -1612,6 +1714,8 @@ filterDescribe('Storage', storageSupport, () => {
1612
1714
  makeManifestWithLiveTimeline();
1613
1715
  this.map_[manifestWithAlternateSegmentsUri] =
1614
1716
  makeManifestWithAlternateSegments();
1717
+ this.map_[manifestWithVideoInitSegmentsUri] =
1718
+ makeManifestWithVideoInitSegments();
1615
1719
  }
1616
1720
 
1617
1721
  /** @override */