shaka-player 3.2.2 → 3.3.0

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 (156) 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/AUTHORS +6 -5
  5. package/CHANGELOG.md +247 -0
  6. package/CONTRIBUTORS +4 -2
  7. package/build/types/core +3 -0
  8. package/build/types/offline +1 -0
  9. package/build/types/polyfill +1 -0
  10. package/build/types/ui +2 -0
  11. package/demo/common/assets.js +18 -0
  12. package/demo/common/message_ids.js +13 -1
  13. package/demo/config.js +31 -1
  14. package/demo/index.html +1 -1
  15. package/demo/locales/en.json +12 -0
  16. package/demo/locales/source.json +48 -0
  17. package/dist/controls.css +26 -2
  18. package/dist/controls.css.map +1 -1
  19. package/dist/demo.compiled.debug.js +19 -16
  20. package/dist/demo.compiled.debug.map +3 -3
  21. package/dist/demo.compiled.js +19 -16
  22. package/dist/demo.compiled.map +3 -3
  23. package/dist/deps.js +18 -11
  24. package/dist/locales.js +28 -26
  25. package/dist/receiver.compiled.debug.js +11 -9
  26. package/dist/receiver.compiled.debug.map +3 -3
  27. package/dist/receiver.compiled.js +11 -9
  28. package/dist/receiver.compiled.map +3 -3
  29. package/dist/shaka-player.compiled.d.ts +205 -15
  30. package/dist/shaka-player.compiled.debug.d.ts +205 -15
  31. package/dist/shaka-player.compiled.debug.externs.js +256 -6
  32. package/dist/shaka-player.compiled.debug.js +406 -354
  33. package/dist/shaka-player.compiled.debug.map +4 -4
  34. package/dist/shaka-player.compiled.externs.js +256 -6
  35. package/dist/shaka-player.compiled.js +826 -800
  36. package/dist/shaka-player.compiled.map +4 -4
  37. package/dist/shaka-player.ui.d.ts +221 -16
  38. package/dist/shaka-player.ui.debug.d.ts +221 -16
  39. package/dist/shaka-player.ui.debug.externs.js +283 -6
  40. package/dist/shaka-player.ui.debug.js +547 -476
  41. package/dist/shaka-player.ui.debug.map +4 -4
  42. package/dist/shaka-player.ui.externs.js +283 -6
  43. package/dist/shaka-player.ui.js +1019 -980
  44. package/dist/shaka-player.ui.map +4 -4
  45. package/docs/design/bg-fetch-after.gv +29 -0
  46. package/docs/design/bg-fetch-after.gv.png +0 -0
  47. package/docs/design/bg-fetch-before.gv +18 -0
  48. package/docs/design/bg-fetch-before.gv.png +0 -0
  49. package/docs/design/bg-fetch.md +134 -0
  50. package/docs/tutorials/blob-url.md +15 -0
  51. package/docs/tutorials/index.json +1 -0
  52. package/docs/tutorials/ui-customization.md +66 -0
  53. package/docs/tutorials/ui.md +5 -5
  54. package/externs/ima.js +3 -0
  55. package/externs/shaka/ads.js +5 -0
  56. package/externs/shaka/manifest.js +12 -1
  57. package/externs/shaka/manifest_parser.js +10 -1
  58. package/externs/shaka/net.js +20 -4
  59. package/externs/shaka/offline.js +31 -2
  60. package/externs/shaka/player.js +130 -5
  61. package/externs/shaka/text.js +14 -0
  62. package/externs/webcrypto.js +18 -0
  63. package/lib/abr/ewma.js +14 -0
  64. package/lib/abr/ewma_bandwidth_estimator.js +16 -2
  65. package/lib/abr/simple_abr_manager.js +6 -0
  66. package/lib/ads/client_side_ad.js +8 -0
  67. package/lib/ads/client_side_ad_manager.js +8 -4
  68. package/lib/ads/server_side_ad.js +7 -0
  69. package/lib/cast/cast_utils.js +1 -0
  70. package/lib/dash/dash_parser.js +48 -14
  71. package/lib/dash/segment_base.js +25 -1
  72. package/lib/dash/segment_list.js +18 -9
  73. package/lib/dash/segment_template.js +23 -10
  74. package/lib/hls/hls_parser.js +86 -23
  75. package/lib/media/quality_observer.js +291 -0
  76. package/lib/media/segment_index.js +6 -0
  77. package/lib/media/segment_reference.js +18 -3
  78. package/lib/media/streaming_engine.js +42 -4
  79. package/lib/net/http_fetch_plugin.js +31 -11
  80. package/lib/net/http_plugin_utils.js +1 -0
  81. package/lib/net/http_xhr_plugin.js +40 -14
  82. package/lib/net/networking_engine.js +81 -18
  83. package/lib/offline/download_info.js +68 -0
  84. package/lib/offline/download_manager.js +13 -5
  85. package/lib/offline/indexeddb/base_storage_cell.js +35 -0
  86. package/lib/offline/indexeddb/storage_mechanism.js +1 -1
  87. package/lib/offline/indexeddb/v5_storage_cell.js +5 -0
  88. package/lib/offline/storage.js +393 -284
  89. package/lib/offline/stored_content_utils.js +2 -0
  90. package/lib/player.js +206 -7
  91. package/lib/polyfill/random_uuid.js +52 -0
  92. package/lib/text/cue.js +12 -0
  93. package/lib/text/ttml_text_parser.js +25 -2
  94. package/lib/text/ui_text_displayer.js +2 -2
  95. package/lib/util/cmcd_manager.js +826 -0
  96. package/lib/util/error.js +6 -0
  97. package/lib/util/language_utils.js +12 -0
  98. package/lib/util/mutex.js +52 -0
  99. package/lib/util/periods.js +98 -64
  100. package/lib/util/player_configuration.js +18 -0
  101. package/package.json +1 -1
  102. package/shaka-player.uncompiled.js +3 -0
  103. package/test/dash/dash_parser_content_protection_unit.js +2 -0
  104. package/test/dash/dash_parser_live_unit.js +101 -1
  105. package/test/dash/dash_parser_manifest_unit.js +35 -1
  106. package/test/dash/dash_parser_segment_base_unit.js +2 -0
  107. package/test/dash/dash_parser_segment_list_unit.js +2 -1
  108. package/test/dash/dash_parser_segment_template_unit.js +2 -1
  109. package/test/hls/hls_live_unit.js +2 -0
  110. package/test/hls/hls_parser_unit.js +2 -0
  111. package/test/media/quality_observer_unit.js +139 -0
  112. package/test/media/segment_reference_unit.js +16 -1
  113. package/test/media/streaming_engine_integration.js +2 -1
  114. package/test/media/streaming_engine_unit.js +2 -0
  115. package/test/net/http_plugin_unit.js +20 -15
  116. package/test/offline/storage_integration.js +108 -4
  117. package/test/player_integration.js +99 -0
  118. package/test/test/util/dash_parser_util.js +4 -0
  119. package/test/test/util/fake_ad.js +7 -0
  120. package/test/test/util/fake_drm_engine.js +9 -2
  121. package/test/test/util/manifest_generator.js +3 -2
  122. package/test/test/util/test_scheme.js +14 -1
  123. package/test/text/ttml_text_parser_unit.js +47 -0
  124. package/test/ui/ui_unit.js +250 -0
  125. package/test/util/cmcd_manager_unit.js +277 -0
  126. package/ui/airplay_button.js +1 -0
  127. package/ui/audio_language_selection.js +7 -0
  128. package/ui/cast_button.js +1 -0
  129. package/ui/context_menu.js +109 -0
  130. package/ui/controls.js +41 -7
  131. package/ui/controls.less +1 -0
  132. package/ui/enums.js +4 -0
  133. package/ui/externs/ui.js +28 -1
  134. package/ui/fast_forward_button.js +14 -5
  135. package/ui/fullscreen_button.js +1 -0
  136. package/ui/less/buttons.less +0 -4
  137. package/ui/less/containers.less +74 -0
  138. package/ui/less/overflow_menu.less +0 -10
  139. package/ui/less/tooltip.less +97 -0
  140. package/ui/locales/en.json +1 -0
  141. package/ui/locales/source.json +4 -0
  142. package/ui/loop_button.js +5 -0
  143. package/ui/mute_button.js +2 -1
  144. package/ui/overflow_menu.js +5 -25
  145. package/ui/pip_button.js +5 -0
  146. package/ui/play_button.js +2 -2
  147. package/ui/playback_rate_selection.js +8 -9
  148. package/ui/resolution_selection.js +7 -0
  149. package/ui/rewind_button.js +15 -6
  150. package/ui/seek_bar.js +4 -2
  151. package/ui/settings_menu.js +36 -4
  152. package/ui/small_play_button.js +1 -0
  153. package/ui/statistics_button.js +253 -0
  154. package/ui/text_selection.js +11 -4
  155. package/ui/ui.js +30 -0
  156. package/ui/volume_bar.js +1 -1
@@ -87,6 +87,41 @@ describe('Player', () => {
87
87
  });
88
88
  }); // describe('attach')
89
89
 
90
+ describe('updateStartTime() in manifestparsed event handler', () => {
91
+ it('does not get segments prior to startTime', async () => {
92
+ player.addEventListener('manifestparsed', () => {
93
+ player.updateStartTime(24);
94
+ });
95
+ const results = {
96
+ requestedVideoSegment0: false,
97
+ requestedVideoSegment1: false,
98
+ requestedVideoSegment2: false,
99
+ };
100
+ const expected = {
101
+ requestedVideoSegment0: false,
102
+ requestedVideoSegment1: false,
103
+ requestedVideoSegment2: true,
104
+ };
105
+ player.getNetworkingEngine().registerRequestFilter(
106
+ (type, request) => {
107
+ if (request.uris[0] == 'test:sintel/video/0') {
108
+ results.requestedVideoSegment0 = true;
109
+ }
110
+ if (request.uris[0] == 'test:sintel/video/1') {
111
+ results.requestedVideoSegment1 = true;
112
+ }
113
+ if (request.uris[0] == 'test:sintel/video/2') {
114
+ results.requestedVideoSegment2 = true;
115
+ }
116
+ });
117
+ await player.load('test:sintel_compiled');
118
+ video.play();
119
+ await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 25, 30);
120
+ expect(results).toEqual(expected);
121
+ });
122
+ });
123
+
124
+
90
125
  describe('getStats', () => {
91
126
  it('gives stats about current stream', async () => {
92
127
  // This is tested more in player_unit.js. This is here to test the public
@@ -652,6 +687,70 @@ describe('Player', () => {
652
687
  });
653
688
  });
654
689
 
690
+ describe('mediaQualityChanges', () => {
691
+ /** @type {!jasmine.Spy} */
692
+ let onQualityChange;
693
+ /** @type {!shaka.test.Waiter} */
694
+ let waiter;
695
+
696
+ const qualityChange1 = jasmine.objectContaining({
697
+ mediaQuality: jasmine.objectContaining({
698
+ bandwidth: 1,
699
+ codecs: 'mp4a.40.2',
700
+ contentType: 'audio',
701
+ mimeType: 'audio/mp4',
702
+ }),
703
+ type: 'mediaqualitychanged',
704
+ position: jasmine.any(Number),
705
+ });
706
+ const qualityChange2 = jasmine.objectContaining({
707
+ mediaQuality: jasmine.objectContaining({
708
+ bandwidth: 1,
709
+ codecs: 'avc1.42c01e',
710
+ contentType: 'video',
711
+ mimeType: 'video/mp4',
712
+ }),
713
+ type: 'mediaqualitychanged',
714
+ position: jasmine.any(Number),
715
+ });
716
+
717
+ beforeEach(() => {
718
+ onQualityChange = jasmine.createSpy('onQualityChange');
719
+ // const spyFunc = Util.spyFunc(onQualityChange);
720
+ player.addEventListener('mediaqualitychanged',
721
+ Util.spyFunc(onQualityChange));
722
+ waiter = new shaka.test.Waiter(eventManager)
723
+ .timeoutAfter(10)
724
+ .failOnTimeout(true);
725
+ });
726
+
727
+ it('emits audio/video quality changes at start when enabled', async () => {
728
+ player.configure('streaming.observeQualityChanges', true);
729
+
730
+ await player.load('test:sintel_compiled');
731
+ video.play();
732
+
733
+ // Wait for the video to start playback. If it takes longer than 10
734
+ // seconds, fail the test.
735
+ await waiter.waitForMovementOrFailOnTimeout(video, 10);
736
+ expect(onQualityChange).toHaveBeenCalledTimes(2);
737
+ expect(onQualityChange).toHaveBeenCalledWith(qualityChange1);
738
+ expect(onQualityChange).toHaveBeenCalledWith(qualityChange2);
739
+ });
740
+
741
+ it('does not emit quality changes at start when disabled', async () => {
742
+ player.configure('streaming.observeQualityChanges', false);
743
+
744
+ await player.load('test:sintel_compiled');
745
+ video.play();
746
+
747
+ // Wait for the video to start playback. If it takes longer than 10
748
+ // seconds, fail the test.
749
+ await waiter.waitForMovementOrFailOnTimeout(video, 10);
750
+ expect(onQualityChange).not.toHaveBeenCalled();
751
+ });
752
+ });
753
+
655
754
  describe('seek range', () => {
656
755
  // Regression test for Issue #3675.
657
756
  it('has a reasonable seek range after going from live to VOD', async () => {
@@ -46,6 +46,8 @@ shaka.test.Dash = class {
46
46
 
47
47
  const playerInterface = {
48
48
  networkingEngine: networkingEngine,
49
+ modifyManifestRequest: (request, manifestInfo) => {},
50
+ modifySegmentRequest: (request, segmentInfo) => {},
49
51
  filter: () => {},
50
52
  makeTextStreamsForClosedCaptions: (manifest) => {},
51
53
  onTimelineRegionAdded: fail, // Should not have any EventStream elements.
@@ -78,6 +80,8 @@ shaka.test.Dash = class {
78
80
 
79
81
  const playerInterface = {
80
82
  networkingEngine: networkingEngine,
83
+ modifyManifestRequest: (request, manifestInfo) => {},
84
+ modifySegmentRequest: (request, segmentInfo) => {},
81
85
  filter: () => {},
82
86
  makeTextStreamsForClosedCaptions: (manifest) => {},
83
87
  onTimelineRegionAdded: fail, // Should not have any EventStream elements.
@@ -154,6 +154,13 @@ shaka.test.FakeAd = class {
154
154
  }
155
155
 
156
156
 
157
+ /**
158
+ * @override
159
+ */
160
+ isLinear() {
161
+ return true;
162
+ }
163
+
157
164
  /**
158
165
  * @override
159
166
  */
@@ -17,7 +17,7 @@ goog.require('shaka.test.Util');
17
17
  */
18
18
  shaka.test.FakeDrmEngine = class {
19
19
  constructor() {
20
- /** @private {!Array.<number>} */
20
+ /** @private {!Array.<string>} */
21
21
  this.offlineSessions_ = [];
22
22
  /** @private {?shaka.extern.DrmInfo} */
23
23
  this.drmInfo_ = null;
@@ -45,6 +45,13 @@ shaka.test.FakeDrmEngine = class {
45
45
  // be returned.
46
46
  this.getDrmInfo.and.callFake(() => this.drmInfo_);
47
47
 
48
+ /** @type {!jasmine.Spy} */
49
+ this.newInitData = jasmine.createSpy('newInitData');
50
+ this.newInitData.and.callFake((initDataType, initData) => {
51
+ const num = 1 + this.offlineSessions_.length;
52
+ this.offlineSessions_.push('session-' + num);
53
+ });
54
+
48
55
  /** @type {!jasmine.Spy} */
49
56
  this.getExpiration = jasmine.createSpy('getExpiration');
50
57
  this.getExpiration.and.returnValue(Infinity);
@@ -90,7 +97,7 @@ shaka.test.FakeDrmEngine = class {
90
97
  }
91
98
 
92
99
  /**
93
- * @param {!Array.<number>} sessions
100
+ * @param {!Array.<string>} sessions
94
101
  */
95
102
  setSessionIds(sessions) {
96
103
  // Copy the values to break the reference to the input value.
@@ -653,15 +653,16 @@ shaka.test.ManifestGenerator.Stream = class {
653
653
  * @param {!Array.<string>} uris
654
654
  * @param {number} startByte
655
655
  * @param {?number} endByte
656
+ * @param {null|shaka.extern.MediaQualityInfo=} mediaQuality
656
657
  */
657
- setInitSegmentReference(uris, startByte, endByte) {
658
+ setInitSegmentReference(uris, startByte, endByte, mediaQuality) {
658
659
  goog.asserts.assert(this.manifest_,
659
660
  'A top-level generated Manifest is required to use this method!');
660
661
 
661
662
  const getUris = () => uris;
662
663
  this.initSegmentReference_ =
663
664
  new this.manifest_.shaka_.media.InitSegmentReference(
664
- getUris, startByte, endByte);
665
+ getUris, startByte, endByte, mediaQuality);
665
666
  }
666
667
 
667
668
  /**
@@ -206,10 +206,23 @@ shaka.test.TestScheme = class {
206
206
  * @param {string} name
207
207
  */
208
208
  function addStreamInfo(stream, variant, data, contentType, name) {
209
+ const mediaQualityInfo = {
210
+ bandwidth: 1,
211
+ codecs: data[contentType].codecs || 'unknown',
212
+ contentType: contentType,
213
+ mimeType: data[contentType].mimeType,
214
+ audioSamplingRate: null,
215
+ frameRate: null,
216
+ height: null,
217
+ channelsCount: null,
218
+ pixelAspectRatio: null,
219
+ width: null,
220
+ };
209
221
  stream.mimeType = data[contentType].mimeType;
210
222
  stream.codecs = data[contentType].codecs;
211
223
  stream.setInitSegmentReference(
212
- ['test:' + name + '/' + contentType + '/init'], 0, null);
224
+ ['test:' + name + '/' + contentType + '/init'], 0, null,
225
+ mediaQualityInfo);
213
226
  stream.useSegmentTemplate(
214
227
  'test:' + name + '/' + contentType + '/%d',
215
228
  data[contentType].segmentDuration);
@@ -1266,6 +1266,8 @@ describe('TtmlTextParser', () => {
1266
1266
  fontStyle: Cue.fontStyle.ITALIC,
1267
1267
  lineHeight: '20px',
1268
1268
  fontSize: '10em',
1269
+ textStrokeColor: 'blue',
1270
+ textStrokeWidth: '3px',
1269
1271
  },
1270
1272
  {
1271
1273
  startTime: 2,
@@ -1277,6 +1279,7 @@ describe('TtmlTextParser', () => {
1277
1279
  '<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
1278
1280
  '<styling>' +
1279
1281
  '<style xml:id="s1" tts:color="red" ' +
1282
+ 'tts:textOutline="blue 3px" ' +
1280
1283
  'tts:backgroundColor="blue" ' +
1281
1284
  'tts:fontWeight="bold" ' +
1282
1285
  'tts:fontFamily="Times New Roman" ' +
@@ -1295,6 +1298,50 @@ describe('TtmlTextParser', () => {
1295
1298
  {periodStart: 0, segmentStart: 0, segmentEnd: 0});
1296
1299
  });
1297
1300
 
1301
+ it('uses text color if tts:textOutline does not specify color', () => {
1302
+ verifyHelper(
1303
+ [
1304
+ {
1305
+ startTime: 1,
1306
+ endTime: 2,
1307
+ payload: 'Test',
1308
+ color: 'red',
1309
+ textStrokeColor: 'red',
1310
+ textStrokeWidth: '3px',
1311
+ },
1312
+ ],
1313
+ '<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
1314
+ '<styling>' +
1315
+ '<style xml:id="s1" tts:color="red" ' +
1316
+ 'tts:textOutline="3px" />' +
1317
+ '</styling>' +
1318
+ '<body region="subtitleArea"><div>' +
1319
+ '<p begin="00:01.00" end="00:02.00" style="s1">Test</p>' +
1320
+ '</div></body></tt>',
1321
+ {periodStart: 0, segmentStart: 0, segmentEnd: 0});
1322
+ });
1323
+
1324
+ it('does not add an outline if tts:textOutline only contains color', () => {
1325
+ verifyHelper(
1326
+ [
1327
+ {
1328
+ startTime: 1,
1329
+ endTime: 2,
1330
+ payload: 'Test',
1331
+ color: 'red',
1332
+ },
1333
+ ],
1334
+ '<tt xmlns:tts="http://www.w3.org/ns/ttml#styling">' +
1335
+ '<styling>' +
1336
+ '<style xml:id="s1" tts:color="red" ' +
1337
+ 'tts:textOutline="blue" />' +
1338
+ '</styling>' +
1339
+ '<body region="subtitleArea"><div>' +
1340
+ '<p begin="00:01.00" end="00:02.00" style="s1">Test</p>' +
1341
+ '</div></body></tt>',
1342
+ {periodStart: 0, segmentStart: 0, segmentEnd: 0});
1343
+ });
1344
+
1298
1345
  // Regression test for #2623
1299
1346
  it('does not apply background colors to containers', () => {
1300
1347
  verifyHelper(
@@ -409,6 +409,80 @@ describe('UI', () => {
409
409
  });
410
410
  });
411
411
 
412
+ describe('control panel buttons with submenus', () => {
413
+ /** @type {!HTMLElement} */
414
+ let resolutionMenu;
415
+ /** @type {!Element} */
416
+ let resolutionMenuButton;
417
+ /** @type {!HTMLElement} */
418
+ let languageMenu;
419
+ /** @type {!Element} */
420
+ let languageMenuButton;
421
+
422
+ beforeEach(() => {
423
+ const config = {
424
+ controlPanelElements: [
425
+ 'quality',
426
+ 'language',
427
+ ],
428
+ };
429
+ const ui = UiUtils.createUIThroughAPI(videoContainer, video, config);
430
+ player = ui.getControls().getLocalPlayer();
431
+
432
+ const resolutionsMenus =
433
+ videoContainer.getElementsByClassName('shaka-resolutions');
434
+ expect(resolutionsMenus.length).toBe(1);
435
+ resolutionMenu = /** @type {!HTMLElement} */ (resolutionsMenus[0]);
436
+
437
+ const resolutionMenuButtons =
438
+ videoContainer.getElementsByClassName('shaka-resolution-button');
439
+ expect(resolutionMenuButtons.length).toBe(1);
440
+ resolutionMenuButton = resolutionMenuButtons[0];
441
+
442
+ const languageMenus =
443
+ videoContainer.getElementsByClassName('shaka-audio-languages');
444
+ expect(languageMenus.length).toBe(1);
445
+ languageMenu = /** @type {!HTMLElement} */ (languageMenus[0]);
446
+
447
+ const languageMenuButtons =
448
+ videoContainer.getElementsByClassName('shaka-language-button');
449
+ expect(languageMenuButtons.length).toBe(1);
450
+ languageMenuButton = languageMenuButtons[0];
451
+ });
452
+
453
+ it('menus are initially hidden', () => {
454
+ expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(true);
455
+ expect(languageMenu.classList.contains('shaka-hidden')).toBe(true);
456
+ });
457
+
458
+ it('a menu becomes visible if the button is clicked', () => {
459
+ resolutionMenuButton.click();
460
+
461
+ expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(false);
462
+ });
463
+
464
+ it('a menu becomes hidden if the "close" button is clicked', () => {
465
+ resolutionMenuButton.click();
466
+
467
+ const backToOverflowButtons =
468
+ videoContainer.getElementsByClassName('shaka-back-to-overflow-button');
469
+ expect(backToOverflowButtons.length).toBe(2);
470
+ const backToOverflowButton =
471
+ /** @type {!HTMLElement} */ (backToOverflowButtons[0]);
472
+ backToOverflowButton.click();
473
+
474
+ expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(true);
475
+ });
476
+
477
+ it('a menu becomes hidden if another one is opened', () => {
478
+ resolutionMenuButton.click();
479
+ languageMenuButton.click();
480
+
481
+ expect(resolutionMenu.classList.contains('shaka-hidden')).toBe(true);
482
+ expect(languageMenu.classList.contains('shaka-hidden')).toBe(false);
483
+ });
484
+ });
485
+
412
486
  describe('resolutions menu', () => {
413
487
  /** @type {!HTMLElement} */
414
488
  let resolutionsMenu;
@@ -611,6 +685,182 @@ describe('UI', () => {
611
685
  goog.asserts.assert(found, 'Unable to find resolution menu');
612
686
  }
613
687
  });
688
+
689
+ describe('custom context menu', () => {
690
+ /** @type {!HTMLElement} */
691
+ let controlsContainer;
692
+ /** @type {!HTMLElement} */
693
+ let contextMenu;
694
+
695
+ beforeEach(() => {
696
+ const config = {
697
+ customContextMenu: true,
698
+ contextMenuElements: [
699
+ 'fakeElement',
700
+ 'statistics',
701
+ 'fakeElement',
702
+ ],
703
+ };
704
+ const ui = UiUtils.createUIThroughAPI(videoContainer, video, config);
705
+
706
+ controlsContainer = ui.getControls().getControlsContainer();
707
+
708
+ const contextMenus =
709
+ videoContainer.getElementsByClassName('shaka-context-menu');
710
+ expect(contextMenus.length).toBe(1);
711
+ contextMenu = /** @type {!HTMLElement} */
712
+ (contextMenus[0]);
713
+ });
714
+
715
+ it('responds to contextmenu event', () => {
716
+ expect(contextMenu.classList.contains('shaka-hidden')).toBe(true);
717
+ UiUtils.simulateEvent(controlsContainer, 'contextmenu');
718
+ expect(contextMenu.classList.contains('shaka-hidden')).toBe(false);
719
+ UiUtils.simulateEvent(controlsContainer, 'contextmenu');
720
+ expect(contextMenu.classList.contains('shaka-hidden')).toBe(true);
721
+ });
722
+ it('hides on click event', () => {
723
+ UiUtils.simulateEvent(controlsContainer, 'contextmenu');
724
+ UiUtils.simulateEvent(controlsContainer, 'click');
725
+ expect(contextMenu.classList.contains('shaka-hidden')).toBe(true);
726
+ UiUtils.simulateEvent(controlsContainer, 'contextmenu');
727
+ UiUtils.simulateEvent(window, 'click');
728
+ expect(contextMenu.classList.contains('shaka-hidden')).toBe(true);
729
+ });
730
+ it('builds internal elements', () => {
731
+ expect(contextMenu.childNodes.length).toBe(1);
732
+
733
+ expect(contextMenu.childNodes[0]['className'])
734
+ .toBe('shaka-statistics-button');
735
+ });
736
+ });
737
+
738
+ describe('statistics context menu', () => {
739
+ /** @type {!HTMLElement} */
740
+ let statisticsButton;
741
+ /** @type {!HTMLElement} */
742
+ let statisticsContainer;
743
+
744
+ beforeEach(() => {
745
+ const config = {
746
+ customContextMenu: true,
747
+ contextMenuElements: [
748
+ 'statistics',
749
+ ],
750
+ statisticsList: Object.keys(new shaka.util.Stats().getBlob()),
751
+ };
752
+ const ui = UiUtils.createUIThroughAPI(videoContainer, video, config);
753
+ player = ui.getControls().getLocalPlayer();
754
+
755
+ const statisticsButtons =
756
+ videoContainer.getElementsByClassName('shaka-statistics-button');
757
+ expect(statisticsButtons.length).toBe(1);
758
+ statisticsButton = /** @type {!HTMLElement} */
759
+ (statisticsButtons[0]);
760
+
761
+ const statisticsContainers =
762
+ videoContainer.getElementsByClassName('shaka-statistics-container');
763
+ expect(statisticsContainers.length).toBe(1);
764
+ statisticsContainer = /** @type {!HTMLElement} */
765
+ (statisticsContainers[0]);
766
+ });
767
+
768
+ it('appears and disappears on toggle', () => {
769
+ expect(statisticsContainer.classList.contains('shaka-hidden'))
770
+ .toBe(true);
771
+
772
+ statisticsButton.click();
773
+ expect(statisticsContainer.classList.contains('shaka-hidden'))
774
+ .toBe(false);
775
+
776
+ statisticsButton.click();
777
+ expect(statisticsContainer.classList.contains('shaka-hidden'))
778
+ .toBe(true);
779
+ });
780
+
781
+ it('displays all the available statistics', () => {
782
+ const skippedStats = ['stateHistory', 'switchHistory'];
783
+ const nodes = statisticsContainer.childNodes;
784
+ let nodeIndex = 0;
785
+
786
+ for (const statistic in new shaka.util.Stats().getBlob()) {
787
+ if (!skippedStats.includes(statistic)) {
788
+ // Text content of label (without ':') is a valid statistic
789
+ const label = nodes[nodeIndex].childNodes[0].textContent;
790
+ expect(label.replace(':', '')).toBe(statistic);
791
+
792
+ // Value has been parsed and it is not the default 'NaN'
793
+ const value = nodes[nodeIndex].childNodes[1].textContent;
794
+ expect(value).not.toBe('NaN');
795
+
796
+ nodeIndex += 1;
797
+ }
798
+ }
799
+ });
800
+ it('is updated periodically', async () => {
801
+ function getStatsFromContainer() {
802
+ const nodes = statisticsContainer.childNodes;
803
+ width = nodes[0].childNodes[1].textContent.replace(' (px)', '');
804
+ height = nodes[1].childNodes[1].textContent.replace(' (px)', '');
805
+ bufferingTime =
806
+ nodes[13].childNodes[1].textContent.replace(' (s)', '');
807
+ }
808
+
809
+ /** @type {!string} */
810
+ let width;
811
+ /** @type {!string} */
812
+ let height;
813
+ /** @type {!string} */
814
+ let bufferingTime;
815
+ /** @type {!string} */
816
+ let lastBufferingTime;
817
+
818
+ const manifest =
819
+ shaka.test.ManifestGenerator.generate((manifest) => {
820
+ manifest.addVariant(/* id= */ 0, (variant) => {
821
+ variant.addVideo(1, (stream) => {
822
+ stream.size(1920, 1080);
823
+ });
824
+ });
825
+ });
826
+
827
+ shaka.media.ManifestParser.registerParserByMime(
828
+ fakeMimeType, () => new shaka.test.FakeManifestParser(manifest));
829
+
830
+ await player.load(
831
+ /* uri= */ 'fake', /* startTime= */ 0, fakeMimeType);
832
+
833
+ // Placeholder statistics are available before toggle
834
+ getStatsFromContainer();
835
+ expect(width).toBe('NaN');
836
+ expect(height).toBe('NaN');
837
+ expect(bufferingTime).toBe('NaN');
838
+
839
+ // Statistics are displayed on toggle
840
+ statisticsButton.click();
841
+ await Util.delay(0.2);
842
+
843
+ getStatsFromContainer();
844
+ expect(width).toBe('1920');
845
+ expect(height).toBe('1080');
846
+ expect(bufferingTime).toBeGreaterThanOrEqual(0.1);
847
+
848
+ // Statistics are updated over time
849
+ lastBufferingTime = bufferingTime;
850
+ await Util.delay(0.2);
851
+
852
+ getStatsFromContainer();
853
+ expect(bufferingTime).toBeGreaterThan(lastBufferingTime);
854
+
855
+ // Statistics stop updating when the container is hidden
856
+ statisticsButton.click();
857
+ lastBufferingTime = bufferingTime;
858
+ await Util.delay(0.2);
859
+
860
+ getStatsFromContainer();
861
+ expect(bufferingTime).toBe(lastBufferingTime);
862
+ });
863
+ });
614
864
  });
615
865
 
616
866