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
@@ -34,11 +34,8 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
34
34
  */
35
35
  let beforeScreenshot;
36
36
 
37
- // Legacy Edge seems to have inconsistent font kerning. A one-pixel offset in
38
- // the position of one character appears about 60% of the time, requiring us
39
- // to have this change tolerance in our tests. So far, all past bugs in our
40
- // implementation that we have tests for would exceed this threshold by a lot.
41
- const threshold = 160; // px
37
+ // A minimum similarity score for screenshots, between 0 and 1.
38
+ const minSimilarity = 0.95;
42
39
 
43
40
  const originalCast = window.chrome && window.chrome.cast;
44
41
 
@@ -196,6 +193,7 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
196
193
 
197
194
  // The video must be played a little now, after the cues were appended,
198
195
  // but before the screenshot.
196
+ video.playbackRate = 1;
199
197
  video.play();
200
198
  await waiter.failOnTimeout(false).timeoutAfter(5)
201
199
  .waitForMovement(video);
@@ -203,10 +201,13 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
203
201
 
204
202
  // Seek to a time when cues should be showing.
205
203
  video.currentTime = time;
204
+ // Get into a playing state, but without movement.
205
+ video.playbackRate = 0;
206
+ video.play();
206
207
 
207
208
  // Add a short delay to ensure that the system has caught up and that
208
209
  // native text displayers have been updated by the browser.
209
- await Util.shortDelay();
210
+ await Util.delay(0.1);
210
211
  };
211
212
  });
212
213
 
@@ -475,6 +476,6 @@ filterDescribe('TextDisplayer layout', supportsScreenshots, () => {
475
476
  return Util.checkScreenshot(
476
477
  /* element= */ videoContainer,
477
478
  prefix + '-' + baseName,
478
- threshold);
479
+ minSimilarity);
479
480
  }
480
481
  });
@@ -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,291 @@
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 = 'br=52317,bs,cid="xyz",com.test-exists,' +
37
+ 'com.test-hello="world",com.test-testing=1234,' +
38
+ 'com.test-token=s,d=6067,mtp=10000,' +
39
+ 'nor="..%2Ftesting%2F3.m4v",nrr="0-99",' +
40
+ 'sid="c936730c-031e-4a73-976f-92bc34039c60"';
41
+ expect(query).toBe(result);
42
+ });
43
+
44
+ it('escapes reserve character in string values', () => {
45
+ const query = CmcdManager.toQuery({
46
+ 'com.test-escape': 'Double "Quotes"',
47
+ });
48
+ const result = 'com.test-escape="Double \\"Quotes\\""';
49
+ expect(query).toBe(result);
50
+ });
51
+ });
52
+
53
+ describe('Header serialization', () => {
54
+ it('produces all header shards', () => {
55
+ const header = CmcdManager.toHeaders(data);
56
+ expect(header).toEqual({
57
+ 'CMCD-Object': 'br=52317,d=6067',
58
+ 'CMCD-Request': 'com.test-exists,com.test-hello="world",' +
59
+ 'com.test-testing=1234,com.test-token=s,mtp=10000,' +
60
+ 'nor="..%2Ftesting%2F3.m4v",nrr="0-99"',
61
+ 'CMCD-Session': 'cid="xyz",sid="c936730c-031e-4a73-976f-92bc34039c60"',
62
+ 'CMCD-Status': 'bs',
63
+ });
64
+ });
65
+
66
+ it('ignores empty shards', () => {
67
+ expect(CmcdManager.toHeaders({br: 200})).toEqual({
68
+ 'CMCD-Object': 'br=200',
69
+ });
70
+ });
71
+ });
72
+
73
+ describe('CmcdManager instance', () => {
74
+ const ObjectUtils = shaka.util.ObjectUtils;
75
+
76
+ const playerInterface = {
77
+ isLive: () => false,
78
+ getBandwidthEstimate: () => 10000000,
79
+ getBufferedInfo: () => ({
80
+ video: [
81
+ {start: 0, end: 5},
82
+ {start: 6, end: 31.234},
83
+ {start: 35, end: 40},
84
+ ],
85
+ }),
86
+ getVariantTracks: () => /** @type {Array.<shaka.extern.Track>} */([
87
+ {
88
+ type: 'variant',
89
+ bandwidth: 50000,
90
+ videoBandwidth: 40000,
91
+ audioBandWidth: 10000,
92
+ },
93
+ {
94
+ type: 'variant',
95
+ bandwidth: 5000000,
96
+ videoBandwidth: 4000000,
97
+ audioBandWidth: 1000000,
98
+ },
99
+ ]),
100
+ getPlaybackRate: () => 1,
101
+ getCurrentTime: () => 10,
102
+ };
103
+
104
+ const sid = '2ed2d1cd-970b-48f2-bfb3-50a79e87cfa3';
105
+ const config = {
106
+ enabled: false,
107
+ sessionId: '',
108
+ contentId: 'testing',
109
+ useHeaders: false,
110
+ };
111
+
112
+ /** @type shaka.util.CmcdManager */
113
+ let cmcdManager = new CmcdManager(playerInterface, config);
114
+
115
+ const request = {
116
+ uris: ['https://test.com/test.mpd'],
117
+ method: 'GET',
118
+ body: null,
119
+ headers: {
120
+ testing: '1234',
121
+ },
122
+ allowCrossSiteCredentials: false,
123
+ retryParameters: /** @type {shaka.extern.RetryParameters} */({}),
124
+ licenseRequestType: null,
125
+ sessionId: null,
126
+ streamDataCallback: null,
127
+ };
128
+
129
+ const manifestInfo = {
130
+ format: shaka.util.CmcdManager.StreamingFormat.DASH,
131
+ };
132
+
133
+ const segmentInfo = {
134
+ type: 'video',
135
+ init: false,
136
+ duration: 3.33,
137
+ mimeType: 'application/mp4',
138
+ codecs: 'avc1.42001e',
139
+ bandwidth: 5234167,
140
+ };
141
+
142
+ describe('configuration', () => {
143
+ it('does not modify requests when disabled', () => {
144
+ const r = ObjectUtils.cloneObject(request);
145
+
146
+ cmcdManager.applyManifestData(r, manifestInfo);
147
+ expect(r.uris[0]).toBe(request.uris[0]);
148
+
149
+ cmcdManager.applySegmentData(r, segmentInfo);
150
+ expect(r.uris[0]).toBe(request.uris[0]);
151
+ });
152
+
153
+ it('generates a session id if not provided', () => {
154
+ config.enabled = true;
155
+ cmcdManager = new CmcdManager(playerInterface, config);
156
+
157
+ const r = ObjectUtils.cloneObject(request);
158
+
159
+ cmcdManager.applyManifestData(r, manifestInfo);
160
+ const regex = new RegExp(`sid%3D%22${uuidRegex}%22`, 'i');
161
+ expect(regex.test(r.uris[0])).toBe(true);
162
+ });
163
+ });
164
+
165
+ describe('query mode', () => {
166
+ beforeAll(() => {
167
+ config.sessionId = sid;
168
+ cmcdManager = new CmcdManager(playerInterface, config);
169
+ });
170
+
171
+ it('modifies manifest request uris', () => {
172
+ const r = ObjectUtils.cloneObject(request);
173
+ cmcdManager.applyManifestData(r, manifestInfo);
174
+ const uri = 'https://test.com/test.mpd?CMCD=cid%3D%22testing%22%2C' +
175
+ 'mtp%3D10000%2Cot%3Dm%2Csf%3Dd%2C' +
176
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
177
+ expect(r.uris[0]).toBe(uri);
178
+ });
179
+
180
+ it('modifies segment request uris', () => {
181
+ const r = ObjectUtils.cloneObject(request);
182
+ cmcdManager.applySegmentData(r, segmentInfo);
183
+ const uri = 'https://test.com/test.mpd?CMCD=bl%3D21200%2Cbr%3D5234%2Ccid%3D%22' +
184
+ 'testing%22%2Cd%3D3330%2Cmtp%3D10000%2Cot%3Dv%2Csf%3Dd%2C' +
185
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Cst%3Dv%2Csu%2C' +
186
+ 'tb%3D4000';
187
+ expect(r.uris[0]).toBe(uri);
188
+ });
189
+
190
+ it('modifies text request uris', () => {
191
+ const r = ObjectUtils.cloneObject(request);
192
+ cmcdManager.applyTextData(r);
193
+ const uri = 'https://test.com/test.mpd?CMCD=cid%3D%22' +
194
+ 'testing%22%2Cmtp%3D10000%2Cot%3Dc%2Csf%3Dd%2C' +
195
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
196
+ expect(r.uris[0]).toBe(uri);
197
+ });
198
+ });
199
+
200
+ describe('header mode', () => {
201
+ beforeAll(() => {
202
+ config.useHeaders = true;
203
+ cmcdManager = new CmcdManager(playerInterface, config);
204
+ });
205
+
206
+ it('modifies manifest request headers', () => {
207
+ const r = ObjectUtils.cloneObject(request);
208
+ cmcdManager.applyManifestData(r, manifestInfo);
209
+ expect(r.headers).toEqual({
210
+ 'testing': '1234',
211
+ 'CMCD-Object': 'ot=m',
212
+ 'CMCD-Request': 'mtp=10000,su',
213
+ 'CMCD-Session': 'cid="testing",sf=d,' +
214
+ 'sid="2ed2d1cd-970b-48f2-bfb3-50a79e87cfa3"',
215
+ });
216
+ });
217
+
218
+ it('modifies segment request headers', () => {
219
+ const r = ObjectUtils.cloneObject(request);
220
+ cmcdManager.applySegmentData(r, segmentInfo);
221
+ expect(r.headers).toEqual({
222
+ 'testing': '1234',
223
+ 'CMCD-Object': 'br=5234,d=3330,ot=v,tb=4000',
224
+ 'CMCD-Request': 'bl=21200,mtp=10000,su',
225
+ 'CMCD-Session': 'cid="testing",sf=d,' +
226
+ 'sid="2ed2d1cd-970b-48f2-bfb3-50a79e87cfa3",st=v',
227
+ });
228
+ });
229
+ });
230
+
231
+ describe('src= mode', () => {
232
+ it('modifies media stream uris', () => {
233
+ const r = cmcdManager
234
+ .appendSrcData('https://test.com/test.mp4', 'video/mp4');
235
+ const uri = 'https://test.com/test.mp4?CMCD=cid%3D%22testing%22%2C' +
236
+ 'mtp%3D10000%2Cot%3Dav%2Csf%3Dd%2C' +
237
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
238
+ expect(r).toBe(uri);
239
+ });
240
+
241
+ it('modifies manifest stream uris', () => {
242
+ const r = cmcdManager
243
+ .appendSrcData('https://test.com/test.m3u8', 'application/x-mpegurl');
244
+ const uri = 'https://test.com/test.m3u8?CMCD=cid%3D%22testing%22%2C' +
245
+ 'mtp%3D10000%2Cot%3Dm%2Csf%3Dd%2C' +
246
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
247
+ expect(r).toBe(uri);
248
+ });
249
+
250
+ it('modifies text track uris', () => {
251
+ const r = cmcdManager.appendTextTrackData('https://test.com/test.vtt');
252
+ const uri = 'https://test.com/test.vtt?CMCD=cid%3D%22testing%22%2C' +
253
+ 'mtp%3D10000%2Cot%3Dc%2Csf%3Dd%2C' +
254
+ 'sid%3D%222ed2d1cd-970b-48f2-bfb3-50a79e87cfa3%22%2Csu';
255
+ expect(r).toBe(uri);
256
+ });
257
+ });
258
+
259
+ describe('adheres to the spec', () => {
260
+ beforeAll(() => {
261
+ cmcdManager.setBuffering(false);
262
+ cmcdManager.setBuffering(true);
263
+ });
264
+
265
+ it('sends bs only once', () => {
266
+ let r = ObjectUtils.cloneObject(request);
267
+ cmcdManager.applySegmentData(r, segmentInfo);
268
+ expect(r.headers['CMCD-Status']).toContain('bs');
269
+
270
+ r = ObjectUtils.cloneObject(request);
271
+ cmcdManager.applySegmentData(r, segmentInfo);
272
+ expect(r.headers['CMCD-Status']).not.toContain('bs');
273
+ });
274
+
275
+ it('sends su until buffering is complete', () => {
276
+ let r = ObjectUtils.cloneObject(request);
277
+ cmcdManager.applySegmentData(r, segmentInfo);
278
+ expect(r.headers['CMCD-Request']).toContain(',su');
279
+
280
+ r = ObjectUtils.cloneObject(request);
281
+ cmcdManager.applySegmentData(r, segmentInfo);
282
+ expect(r.headers['CMCD-Request']).toContain(',su');
283
+
284
+ cmcdManager.setBuffering(false);
285
+ r = ObjectUtils.cloneObject(request);
286
+ cmcdManager.applySegmentData(r, segmentInfo);
287
+ expect(r.headers['CMCD-Request']).not.toContain(',su');
288
+ });
289
+ });
290
+ });
291
+ });