shaka-player 3.1.4 → 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 (214) 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 +363 -0
  6. package/CONTRIBUTORS +4 -2
  7. package/README.md +4 -4
  8. package/build/types/core +3 -0
  9. package/build/types/offline +1 -0
  10. package/build/types/polyfill +2 -0
  11. package/build/types/ui +2 -0
  12. package/demo/common/asset.js +9 -7
  13. package/demo/common/assets.js +28 -1
  14. package/demo/common/message_ids.js +15 -2
  15. package/demo/config.js +35 -2
  16. package/demo/index.html +1 -1
  17. package/demo/locales/en.json +14 -1
  18. package/demo/locales/source.json +56 -4
  19. package/demo/main.js +2 -1
  20. package/dist/controls.css +26 -2
  21. package/dist/controls.css.map +1 -1
  22. package/dist/demo.compiled.debug.js +24 -21
  23. package/dist/demo.compiled.debug.map +3 -3
  24. package/dist/demo.compiled.js +24 -21
  25. package/dist/demo.compiled.map +3 -3
  26. package/dist/deps.js +33 -25
  27. package/dist/locales.js +31 -29
  28. package/dist/receiver.compiled.debug.js +14 -12
  29. package/dist/receiver.compiled.debug.map +3 -3
  30. package/dist/receiver.compiled.js +14 -12
  31. package/dist/receiver.compiled.map +3 -3
  32. package/dist/shaka-player.compiled.d.ts +258 -20
  33. package/dist/shaka-player.compiled.debug.d.ts +258 -20
  34. package/dist/shaka-player.compiled.debug.externs.js +1005 -695
  35. package/dist/shaka-player.compiled.debug.js +720 -640
  36. package/dist/shaka-player.compiled.debug.map +4 -4
  37. package/dist/shaka-player.compiled.externs.js +1005 -695
  38. package/dist/shaka-player.compiled.js +850 -817
  39. package/dist/shaka-player.compiled.map +4 -4
  40. package/dist/shaka-player.ui.d.ts +274 -21
  41. package/dist/shaka-player.ui.debug.d.ts +274 -21
  42. package/dist/shaka-player.ui.debug.externs.js +766 -429
  43. package/dist/shaka-player.ui.debug.js +863 -765
  44. package/dist/shaka-player.ui.debug.map +4 -4
  45. package/dist/shaka-player.ui.externs.js +766 -429
  46. package/dist/shaka-player.ui.js +1042 -996
  47. package/dist/shaka-player.ui.map +4 -4
  48. package/docs/design/bg-fetch-after.gv +29 -0
  49. package/docs/design/bg-fetch-after.gv.png +0 -0
  50. package/docs/design/bg-fetch-before.gv +18 -0
  51. package/docs/design/bg-fetch-before.gv.png +0 -0
  52. package/docs/design/bg-fetch.md +134 -0
  53. package/docs/tutorials/blob-url.md +15 -0
  54. package/docs/tutorials/index.json +1 -0
  55. package/docs/tutorials/ui-customization.md +66 -0
  56. package/docs/tutorials/ui.md +5 -5
  57. package/externs/aria.js +6 -0
  58. package/externs/ima.js +3 -0
  59. package/externs/shaka/ads.js +29 -1
  60. package/externs/shaka/manifest.js +18 -2
  61. package/externs/shaka/manifest_parser.js +10 -1
  62. package/externs/shaka/net.js +20 -4
  63. package/externs/shaka/offline.js +36 -2
  64. package/externs/shaka/player.js +182 -13
  65. package/externs/shaka/text.js +24 -10
  66. package/externs/texttrack.js +3 -0
  67. package/externs/webcrypto.js +18 -0
  68. package/lib/abr/ewma.js +14 -0
  69. package/lib/abr/ewma_bandwidth_estimator.js +16 -2
  70. package/lib/abr/simple_abr_manager.js +6 -0
  71. package/lib/ads/ad_manager.js +16 -15
  72. package/lib/ads/client_side_ad.js +8 -0
  73. package/lib/ads/client_side_ad_manager.js +14 -6
  74. package/lib/ads/server_side_ad.js +7 -0
  75. package/lib/ads/server_side_ad_manager.js +19 -2
  76. package/lib/cast/cast_proxy.js +8 -0
  77. package/lib/cast/cast_utils.js +7 -1
  78. package/lib/cea/cea608_data_channel.js +56 -56
  79. package/lib/cea/cea608_memory.js +33 -33
  80. package/lib/cea/cea708_service.js +43 -43
  81. package/lib/cea/cea708_window.js +38 -38
  82. package/lib/cea/cea_decoder.js +13 -14
  83. package/lib/cea/cea_utils.js +23 -23
  84. package/lib/cea/dtvcc_packet_builder.js +7 -7
  85. package/lib/cea/i_caption_decoder.js +2 -3
  86. package/lib/cea/i_cea_parser.js +2 -2
  87. package/lib/cea/mp4_cea_parser.js +6 -6
  88. package/lib/dash/dash_parser.js +53 -14
  89. package/lib/dash/segment_base.js +25 -1
  90. package/lib/dash/segment_list.js +18 -9
  91. package/lib/dash/segment_template.js +23 -10
  92. package/lib/hls/hls_parser.js +210 -30
  93. package/lib/media/closed_caption_parser.js +1 -2
  94. package/lib/media/drm_engine.js +162 -192
  95. package/lib/media/quality_observer.js +291 -0
  96. package/lib/media/segment_index.js +9 -1
  97. package/lib/media/segment_reference.js +37 -5
  98. package/lib/media/streaming_engine.js +48 -10
  99. package/lib/net/http_fetch_plugin.js +31 -11
  100. package/lib/net/http_plugin_utils.js +1 -0
  101. package/lib/net/http_xhr_plugin.js +40 -14
  102. package/lib/net/networking_engine.js +82 -18
  103. package/lib/offline/download_info.js +68 -0
  104. package/lib/offline/download_manager.js +14 -7
  105. package/lib/offline/indexeddb/base_storage_cell.js +35 -0
  106. package/lib/offline/indexeddb/storage_mechanism.js +1 -1
  107. package/lib/offline/indexeddb/v1_storage_cell.js +1 -0
  108. package/lib/offline/indexeddb/v2_storage_cell.js +1 -0
  109. package/lib/offline/indexeddb/v5_storage_cell.js +5 -0
  110. package/lib/offline/manifest_converter.js +3 -1
  111. package/lib/offline/storage.js +404 -297
  112. package/lib/offline/stored_content_utils.js +2 -0
  113. package/lib/player.js +622 -153
  114. package/lib/polyfill/aria.js +82 -0
  115. package/lib/polyfill/random_uuid.js +52 -0
  116. package/lib/routing/walker.js +2 -3
  117. package/lib/text/cue.js +12 -0
  118. package/lib/text/srt_text_parser.js +4 -4
  119. package/lib/text/text_engine.js +2 -2
  120. package/lib/text/ttml_text_parser.js +25 -2
  121. package/lib/text/ui_text_displayer.js +2 -2
  122. package/lib/text/vtt_text_parser.js +2 -2
  123. package/lib/text/web_vtt_generator.js +55 -12
  124. package/lib/util/cmcd_manager.js +826 -0
  125. package/lib/util/error.js +12 -0
  126. package/lib/util/language_utils.js +12 -0
  127. package/lib/util/manifest_parser_utils.js +1 -0
  128. package/lib/util/mp4_box_parsers.js +20 -20
  129. package/lib/util/multi_map.js +8 -0
  130. package/lib/util/mutex.js +52 -0
  131. package/lib/util/periods.js +98 -64
  132. package/lib/util/player_configuration.js +31 -1
  133. package/lib/util/stream_utils.js +140 -85
  134. package/lib/util/xml_utils.js +31 -12
  135. package/package.json +1 -1
  136. package/roadmap.md +24 -22
  137. package/shaka-player.uncompiled.js +4 -0
  138. package/test/cast/cast_utils_unit.js +1 -0
  139. package/test/cea/cea608_memory_unit.js +1 -1
  140. package/test/cea/cea708_service_unit.js +8 -8
  141. package/test/cea/cea708_window_unit.js +6 -6
  142. package/test/cea/cea_decoder_unit.js +1 -1
  143. package/test/cea/dtvcc_packet_builder_unit.js +2 -2
  144. package/test/dash/dash_parser_content_protection_unit.js +2 -0
  145. package/test/dash/dash_parser_live_unit.js +101 -1
  146. package/test/dash/dash_parser_manifest_unit.js +78 -2
  147. package/test/dash/dash_parser_segment_base_unit.js +2 -0
  148. package/test/dash/dash_parser_segment_list_unit.js +2 -1
  149. package/test/dash/dash_parser_segment_template_unit.js +2 -1
  150. package/test/hls/hls_live_unit.js +2 -0
  151. package/test/hls/hls_parser_unit.js +252 -0
  152. package/test/media/drm_engine_integration.js +86 -12
  153. package/test/media/drm_engine_unit.js +310 -450
  154. package/test/media/quality_observer_unit.js +139 -0
  155. package/test/media/segment_reference_unit.js +16 -1
  156. package/test/media/streaming_engine_integration.js +2 -1
  157. package/test/media/streaming_engine_unit.js +2 -0
  158. package/test/net/http_plugin_unit.js +20 -15
  159. package/test/offline/manifest_convert_unit.js +2 -0
  160. package/test/offline/storage_integration.js +117 -23
  161. package/test/player_integration.js +151 -0
  162. package/test/player_src_equals_integration.js +52 -0
  163. package/test/player_unit.js +11 -33
  164. package/test/test/assets/chapters.srt +11 -0
  165. package/test/test/assets/chapters.vtt +10 -0
  166. package/test/test/util/cea_utils.js +12 -12
  167. package/test/test/util/dash_parser_util.js +4 -0
  168. package/test/test/util/fake_ad.js +7 -0
  169. package/test/test/util/fake_ad_manager.js +7 -0
  170. package/test/test/util/fake_drm_engine.js +9 -2
  171. package/test/test/util/manifest_generator.js +5 -2
  172. package/test/test/util/manifest_parser_util.js +3 -1
  173. package/test/test/util/test_scheme.js +14 -1
  174. package/test/text/ttml_text_parser_unit.js +47 -0
  175. package/test/text/web_vtt_generator_unit.js +81 -7
  176. package/test/ui/ui_unit.js +250 -0
  177. package/test/util/cmcd_manager_unit.js +277 -0
  178. package/test/util/stream_utils_unit.js +118 -10
  179. package/ui/airplay_button.js +2 -1
  180. package/ui/audio_language_selection.js +9 -5
  181. package/ui/cast_button.js +5 -6
  182. package/ui/constants.js +0 -3
  183. package/ui/context_menu.js +109 -0
  184. package/ui/controls.js +41 -7
  185. package/ui/controls.less +1 -0
  186. package/ui/enums.js +4 -0
  187. package/ui/externs/ui.js +28 -1
  188. package/ui/fast_forward_button.js +16 -8
  189. package/ui/fullscreen_button.js +2 -3
  190. package/ui/language_utils.js +1 -1
  191. package/ui/less/buttons.less +0 -4
  192. package/ui/less/containers.less +74 -0
  193. package/ui/less/overflow_menu.less +0 -10
  194. package/ui/less/tooltip.less +97 -0
  195. package/ui/locales/en.json +1 -0
  196. package/ui/locales/source.json +4 -0
  197. package/ui/loop_button.js +6 -3
  198. package/ui/mute_button.js +3 -4
  199. package/ui/overflow_menu.js +7 -28
  200. package/ui/pip_button.js +10 -7
  201. package/ui/play_button.js +2 -2
  202. package/ui/playback_rate_selection.js +11 -15
  203. package/ui/presentation_time.js +1 -3
  204. package/ui/resolution_selection.js +11 -7
  205. package/ui/rewind_button.js +17 -9
  206. package/ui/seek_bar.js +6 -6
  207. package/ui/settings_menu.js +36 -4
  208. package/ui/small_play_button.js +1 -0
  209. package/ui/spacer.js +1 -1
  210. package/ui/statistics_button.js +253 -0
  211. package/ui/text_selection.js +19 -15
  212. package/ui/ui.js +30 -0
  213. package/ui/ui_utils.js +1 -1
  214. package/ui/volume_bar.js +2 -4
@@ -9,23 +9,45 @@ goog.require('shaka.text.WebVttGenerator');
9
9
 
10
10
  describe('WebVttGenerator', () => {
11
11
  it('supports no cues', () => {
12
- verifyHelper([], 'WEBVTT\n\n');
12
+ verifyHelper([], [], 'WEBVTT\n\n');
13
13
  });
14
14
 
15
15
  it('convert cues to WebVTT', () => {
16
16
  const shakaCue1 = new shaka.text.Cue(20, 40, 'Test');
17
+ shakaCue1.textAlign = shaka.text.Cue.textAlign.LEFT;
18
+ shakaCue1.writingMode = shaka.text.Cue.writingMode.VERTICAL_LEFT_TO_RIGHT;
17
19
  const shakaCue2 = new shaka.text.Cue(40, 50, 'Test2');
20
+ shakaCue2.textAlign = shaka.text.Cue.textAlign.RIGHT;
21
+ shakaCue2.writingMode = shaka.text.Cue.writingMode.VERTICAL_RIGHT_TO_LEFT;
22
+ const shakaCue3 = new shaka.text.Cue(50, 51, 'Test3');
23
+ shakaCue3.textAlign = shaka.text.Cue.textAlign.CENTER;
24
+ const shakaCue4 = new shaka.text.Cue(52, 53, 'Test4');
25
+ shakaCue4.textAlign = shaka.text.Cue.textAlign.START;
26
+ const shakaCue5 = new shaka.text.Cue(53, 54, 'Test5');
27
+ shakaCue5.textAlign = shaka.text.Cue.textAlign.END;
28
+
29
+ const adCuePoints = [];
18
30
 
19
31
  verifyHelper(
20
32
  [
21
33
  shakaCue1,
22
34
  shakaCue2,
35
+ shakaCue3,
36
+ shakaCue4,
37
+ shakaCue5,
23
38
  ],
39
+ adCuePoints,
24
40
  'WEBVTT\n\n' +
25
- '00:00:20.000 --> 00:00:40.000\n' +
41
+ '00:00:20.000 --> 00:00:40.000 align:left vertical:lr\n' +
26
42
  'Test\n\n' +
27
- '00:00:40.000 --> 00:00:50.000\n' +
28
- 'Test2\n\n');
43
+ '00:00:40.000 --> 00:00:50.000 align:right vertical:rl\n' +
44
+ 'Test2\n\n' +
45
+ '00:00:50.000 --> 00:00:51.000 align:middle\n' +
46
+ 'Test3\n\n' +
47
+ '00:00:52.000 --> 00:00:53.000 align:start\n' +
48
+ 'Test4\n\n' +
49
+ '00:00:53.000 --> 00:00:54.000 align:end\n' +
50
+ 'Test5\n\n');
29
51
  });
30
52
 
31
53
  it('creates style tags for cues with underline/italics/bold', () => {
@@ -49,19 +71,71 @@ describe('WebVttGenerator', () => {
49
71
  nestedCue4.textDecoration.push(shaka.text.Cue.textDecoration.UNDERLINE);
50
72
 
51
73
  shakaCue.nestedCues = [nestedCue1, nestedCue2, nestedCue3, nestedCue4];
74
+
75
+ const adCuePoints = [];
76
+
52
77
  verifyHelper(
53
78
  [shakaCue],
79
+ adCuePoints,
54
80
  'WEBVTT\n\n' +
55
- '00:00:10.000 --> 00:00:20.000\n' +
81
+ '00:00:10.000 --> 00:00:20.000 align:middle\n' +
56
82
  '<i><u>Test1</u></i><b><i>Test2</i></b>Test3<u>Test4</u>\n\n');
57
83
  });
58
84
 
85
+ it('computes the time with ad cue points', () => {
86
+ const shakaCue1 = new shaka.text.Cue(20, 30, 'Test');
87
+ shakaCue1.textAlign = shaka.text.Cue.textAlign.LEFT;
88
+ shakaCue1.writingMode = shaka.text.Cue.writingMode.VERTICAL_LEFT_TO_RIGHT;
89
+ const shakaCue2 = new shaka.text.Cue(40, 50, 'Test2');
90
+ shakaCue2.textAlign = shaka.text.Cue.textAlign.RIGHT;
91
+ shakaCue2.writingMode = shaka.text.Cue.writingMode.VERTICAL_RIGHT_TO_LEFT;
92
+ const shakaCue3 = new shaka.text.Cue(50, 51, 'Test3');
93
+ shakaCue3.textAlign = shaka.text.Cue.textAlign.CENTER;
94
+ const shakaCue4 = new shaka.text.Cue(52, 53, 'Test4');
95
+ shakaCue4.textAlign = shaka.text.Cue.textAlign.START;
96
+ const shakaCue5 = new shaka.text.Cue(53, 54, 'Test5');
97
+ shakaCue5.textAlign = shaka.text.Cue.textAlign.END;
98
+
99
+ const adCuePoints = [
100
+ {
101
+ start: 0,
102
+ end: 10,
103
+ },
104
+ {
105
+ start: 35,
106
+ end: 45,
107
+ },
108
+ ];
109
+
110
+ verifyHelper(
111
+ [
112
+ shakaCue1,
113
+ shakaCue2,
114
+ shakaCue3,
115
+ shakaCue4,
116
+ shakaCue5,
117
+ ],
118
+ adCuePoints,
119
+ 'WEBVTT\n\n' +
120
+ '00:00:30.000 --> 00:00:40.000 align:left vertical:lr\n' +
121
+ 'Test\n\n' +
122
+ '00:01:00.000 --> 00:01:10.000 align:right vertical:rl\n' +
123
+ 'Test2\n\n' +
124
+ '00:01:10.000 --> 00:01:11.000 align:middle\n' +
125
+ 'Test3\n\n' +
126
+ '00:01:12.000 --> 00:01:13.000 align:start\n' +
127
+ 'Test4\n\n' +
128
+ '00:01:13.000 --> 00:01:14.000 align:end\n' +
129
+ 'Test5\n\n');
130
+ });
131
+
59
132
  /**
60
133
  * @param {!Array} cues
134
+ * @param {!Array} adCuePoints
61
135
  * @param {string} text
62
136
  */
63
- function verifyHelper(cues, text) {
64
- const result = shaka.text.WebVttGenerator.convert(cues);
137
+ function verifyHelper(cues, adCuePoints, text) {
138
+ const result = shaka.text.WebVttGenerator.convert(cues, adCuePoints);
65
139
  expect(text).toBe(result);
66
140
  }
67
141
  });
@@ -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
 
@@ -0,0 +1,277 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ goog.require('shaka.util.CmcdManager');
8
+ goog.require('shaka.util.ObjectUtils');
9
+
10
+ describe('CmcdManager', () => {
11
+ const CmcdManager = shaka.util.CmcdManager;
12
+ const uuidRegex =
13
+ '[A-F\\d]{8}-[A-F\\d]{4}-4[A-F\\d]{3}-[89AB][A-F\\d]{3}-[A-F\\d]{12}';
14
+ const data = {
15
+ 'sid': 'c936730c-031e-4a73-976f-92bc34039c60',
16
+ 'cid': 'xyz',
17
+ 'su': false,
18
+ 'nor': '../testing/3.m4v',
19
+ 'nrr': '0-99',
20
+ 'd': 6066.66,
21
+ 'mtp': 10049,
22
+ 'bs': true,
23
+ 'br': 52317,
24
+ 'v': 1,
25
+ 'pr': 1,
26
+ 'com.test-hello': 'world',
27
+ 'com.test-testing': 1234,
28
+ 'com.test-exists': true,
29
+ 'com.test-notExists': false,
30
+ 'com.test-token': Symbol('s'),
31
+ };
32
+
33
+ describe('Query serialization', () => {
34
+ it('produces correctly serialized data', () => {
35
+ const query = CmcdManager.toQuery(data);
36
+ const result = 'CMCD=br%3D52317%2Cbs%2Ccid%3D%22xyz%22%2C' +
37
+ 'com.test-exists%2Ccom.test-hello%3D%22world%22%2C' +
38
+ 'com.test-testing%3D1234%2Ccom.test-token%3Ds%2C' +
39
+ 'd%3D6067%2Cmtp%3D10000%2C' +
40
+ 'nor%3D%22..%252Ftesting%252F3.m4v%22%2C' +
41
+ 'nrr%3D%220-99%22%2C' +
42
+ 'sid%3D%22c936730c-031e-4a73-976f-92bc34039c60%22';
43
+ expect(query).toBe(result);
44
+ });
45
+ });
46
+
47
+ describe('Header serialization', () => {
48
+ it('produces all header shards', () => {
49
+ const header = CmcdManager.toHeaders(data);
50
+ expect(header).toEqual({
51
+ 'CMCD-Object': 'br=52317,d=6067',
52
+ 'CMCD-Request': 'com.test-exists,com.test-hello="world",' +
53
+ 'com.test-testing=1234,com.test-token=s,mtp=10000,' +
54
+ 'nor="..%2Ftesting%2F3.m4v",nrr="0-99"',
55
+ 'CMCD-Session': 'cid="xyz",sid="c936730c-031e-4a73-976f-92bc34039c60"',
56
+ 'CMCD-Status': 'bs',
57
+ });
58
+ });
59
+
60
+ it('ignores empty shards', () => {
61
+ expect(CmcdManager.toHeaders({br: 200})).toEqual({
62
+ 'CMCD-Object': 'br=200',
63
+ });
64
+ });
65
+ });
66
+
67
+ describe('CmcdManager instance', () => {
68
+ const ObjectUtils = shaka.util.ObjectUtils;
69
+
70
+ const playerInterface = {
71
+ isLive: () => false,
72
+ getBandwidthEstimate: () => 10000000,
73
+ getBufferedInfo: () => ({
74
+ video: [
75
+ {start: 0, end: 5},
76
+ {start: 6, end: 31.234},
77
+ {start: 35, end: 40},
78
+ ],
79
+ }),
80
+ getManifest: () => /** @type {shaka.extern.Manifest} */({
81
+ variants: [
82
+ {video: {bandwidth: 50000}},
83
+ {video: {bandwidth: 5000000}},
84
+ ],
85
+ }),
86
+ getPlaybackRate: () => 1,
87
+ getCurrentTime: () => 10,
88
+ };
89
+
90
+ const sid = '2ed2d1cd-970b-48f2-bfb3-50a79e87cfa3';
91
+ const config = {
92
+ enabled: false,
93
+ sessionId: '',
94
+ contentId: 'testing',
95
+ useHeaders: false,
96
+ };
97
+
98
+ /** @type shaka.util.CmcdManager */
99
+ let cmcdManager = new CmcdManager(playerInterface, config);
100
+
101
+ const request = {
102
+ uris: ['https://test.com/test.mpd'],
103
+ method: 'GET',
104
+ body: null,
105
+ headers: {
106
+ testing: '1234',
107
+ },
108
+ allowCrossSiteCredentials: false,
109
+ retryParameters: /** @type {shaka.extern.RetryParameters} */({}),
110
+ licenseRequestType: null,
111
+ sessionId: null,
112
+ streamDataCallback: null,
113
+ };
114
+
115
+ const manifestInfo = {
116
+ format: shaka.util.CmcdManager.StreamingFormat.DASH,
117
+ };
118
+
119
+ const segmentInfo = {
120
+ type: 'video',
121
+ init: false,
122
+ duration: 3.33,
123
+ mimeType: 'application/mp4',
124
+ codecs: 'avc1.42001e',
125
+ bandwidth: 5234167,
126
+ };
127
+
128
+ describe('configuration', () => {
129
+ it('does not modify requests when disabled', () => {
130
+ const r = ObjectUtils.cloneObject(request);
131
+
132
+ cmcdManager.applyManifestData(r, manifestInfo);
133
+ expect(r.uris[0]).toBe(request.uris[0]);
134
+
135
+ cmcdManager.applySegmentData(r, segmentInfo);
136
+ expect(r.uris[0]).toBe(request.uris[0]);
137
+ });
138
+
139
+ it('generates a session id if not provided', () => {
140
+ config.enabled = true;
141
+ cmcdManager = new CmcdManager(playerInterface, config);
142
+
143
+ const r = ObjectUtils.cloneObject(request);
144
+
145
+ cmcdManager.applyManifestData(r, manifestInfo);
146
+ const regex = new RegExp(`sid%3D%22${uuidRegex}%22`, 'i');
147
+ expect(regex.test(r.uris[0])).toBe(true);
148
+ });
149
+ });
150
+
151
+ describe('query mode', () => {
152
+ beforeAll(() => {
153
+ config.sessionId = sid;
154
+ cmcdManager = new CmcdManager(playerInterface, config);
155
+ });
156
+
157
+ it('modifies manifest request uris', () => {
158
+ const r = ObjectUtils.cloneObject(request);
159
+ cmcdManager.applyManifestData(r, manifestInfo);
160
+ const uri = 'https://test.com/test.mpd?CMCD=cid%3D%22testing%22%2C' +
161
+ 'mtp%3D10000%2Cot%3Dm%2Csf%3Dd%2C' +
162
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
163
+ expect(r.uris[0]).toBe(uri);
164
+ });
165
+
166
+ it('modifies segment request uris', () => {
167
+ const r = ObjectUtils.cloneObject(request);
168
+ cmcdManager.applySegmentData(r, segmentInfo);
169
+ const uri = 'https://test.com/test.mpd?CMCD=bl%3D21200%2Cbr%3D5234%2Ccid%3D%22' +
170
+ 'testing%22%2Cd%3D3330%2Cmtp%3D10000%2Cot%3Dv%2Csf%3Dd%2C' +
171
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Cst%3Dv%2Csu%2C' +
172
+ 'tb%3D5000';
173
+ expect(r.uris[0]).toBe(uri);
174
+ });
175
+
176
+ it('modifies text request uris', () => {
177
+ const r = ObjectUtils.cloneObject(request);
178
+ cmcdManager.applyTextData(r);
179
+ const uri = 'https://test.com/test.mpd?CMCD=cid%3D%22' +
180
+ 'testing%22%2Cmtp%3D10000%2Cot%3Dc%2Csf%3Dd%2C' +
181
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
182
+ expect(r.uris[0]).toBe(uri);
183
+ });
184
+ });
185
+
186
+ describe('header mode', () => {
187
+ beforeAll(() => {
188
+ config.useHeaders = true;
189
+ cmcdManager = new CmcdManager(playerInterface, config);
190
+ });
191
+
192
+ it('modifies manifest request headers', () => {
193
+ const r = ObjectUtils.cloneObject(request);
194
+ cmcdManager.applyManifestData(r, manifestInfo);
195
+ expect(r.headers).toEqual({
196
+ 'testing': '1234',
197
+ 'CMCD-Object': 'ot=m',
198
+ 'CMCD-Request': 'mtp=10000,su',
199
+ 'CMCD-Session': 'cid="testing",sf=d,' +
200
+ 'sid="2ed2d1cd-970b-48f2-bfb3-50a79e87cfa3"',
201
+ });
202
+ });
203
+
204
+ it('modifies segment request headers', () => {
205
+ const r = ObjectUtils.cloneObject(request);
206
+ cmcdManager.applySegmentData(r, segmentInfo);
207
+ expect(r.headers).toEqual({
208
+ 'testing': '1234',
209
+ 'CMCD-Object': 'br=5234,d=3330,ot=v,tb=5000',
210
+ 'CMCD-Request': 'bl=21200,mtp=10000,su',
211
+ 'CMCD-Session': 'cid="testing",sf=d,' +
212
+ 'sid="2ed2d1cd-970b-48f2-bfb3-50a79e87cfa3",st=v',
213
+ });
214
+ });
215
+ });
216
+
217
+ describe('src= mode', () => {
218
+ it('modifies media stream uris', () => {
219
+ const r = cmcdManager
220
+ .appendSrcData('https://test.com/test.mp4', 'video/mp4');
221
+ const uri = 'https://test.com/test.mp4?CMCD=cid%3D%22testing%22%2C' +
222
+ 'mtp%3D10000%2Cot%3Dav%2Csf%3Dd%2C' +
223
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
224
+ expect(r).toBe(uri);
225
+ });
226
+
227
+ it('modifies manifest stream uris', () => {
228
+ const r = cmcdManager
229
+ .appendSrcData('https://test.com/test.m3u8', 'application/x-mpegurl');
230
+ const uri = 'https://test.com/test.m3u8?CMCD=cid%3D%22testing%22%2C' +
231
+ 'mtp%3D10000%2Cot%3Dm%2Csf%3Dd%2C' +
232
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
233
+ expect(r).toBe(uri);
234
+ });
235
+
236
+ it('modifies text track uris', () => {
237
+ const r = cmcdManager.appendTextTrackData('https://test.com/test.vtt');
238
+ const uri = 'https://test.com/test.vtt?CMCD=cid%3D%22testing%22%2C' +
239
+ 'mtp%3D10000%2Cot%3Dc%2Csf%3Dd%2C' +
240
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
241
+ expect(r).toBe(uri);
242
+ });
243
+ });
244
+
245
+ describe('adheres to the spec', () => {
246
+ beforeAll(() => {
247
+ cmcdManager.setBuffering(false);
248
+ cmcdManager.setBuffering(true);
249
+ });
250
+
251
+ it('sends bs only once', () => {
252
+ let r = ObjectUtils.cloneObject(request);
253
+ cmcdManager.applySegmentData(r, segmentInfo);
254
+ expect(r.headers['CMCD-Status']).toContain('bs');
255
+
256
+ r = ObjectUtils.cloneObject(request);
257
+ cmcdManager.applySegmentData(r, segmentInfo);
258
+ expect(r.headers['CMCD-Status']).not.toContain('bs');
259
+ });
260
+
261
+ it('sends su until buffering is complete', () => {
262
+ let r = ObjectUtils.cloneObject(request);
263
+ cmcdManager.applySegmentData(r, segmentInfo);
264
+ expect(r.headers['CMCD-Request']).toContain(',su');
265
+
266
+ r = ObjectUtils.cloneObject(request);
267
+ cmcdManager.applySegmentData(r, segmentInfo);
268
+ expect(r.headers['CMCD-Request']).toContain(',su');
269
+
270
+ cmcdManager.setBuffering(false);
271
+ r = ObjectUtils.cloneObject(request);
272
+ cmcdManager.applySegmentData(r, segmentInfo);
273
+ expect(r.headers['CMCD-Request']).not.toContain(',su');
274
+ });
275
+ });
276
+ });
277
+ });