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
@@ -103,6 +103,25 @@ shaka.util.XmlUtils = class {
103
103
  }
104
104
 
105
105
 
106
+ /**
107
+ * Gets a namespace-qualified attribute.
108
+ * @param {!Element} elem The element to get from.
109
+ * @param {!Array.<string>} nsList The lis of namespace URIs.
110
+ * @param {string} name The local name of the attribute.
111
+ * @return {?string} The attribute's value, or null if not present.
112
+ */
113
+ static getAttributeNSList(elem, nsList, name) {
114
+ // Some browsers return the empty string when the attribute is missing,
115
+ // so check if it exists first. See: https://mzl.la/2L7F0UK
116
+ for (const ns of nsList) {
117
+ if (elem.hasAttributeNS(ns, name)) {
118
+ return elem.getAttributeNS(ns, name);
119
+ }
120
+ }
121
+ return null;
122
+ }
123
+
124
+
106
125
  /**
107
126
  * Gets the text contents of a node.
108
127
  * @param {!Node} elem The XML element.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shaka-player",
3
3
  "description": "DASH/EME video player library",
4
- "version": "3.2.2",
4
+ "version": "3.3.2",
5
5
  "homepage": "https://github.com/google/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -11,7 +11,6 @@
11
11
  }
12
12
  ],
13
13
  "devDependencies": {
14
- "@chiragrupani/karma-chromium-edge-launcher": "^2.1.0",
15
14
  "@teppeis/clutz": "^1.0.29-4c95e12.v20190929",
16
15
  "awesomplete": "^1.1.4",
17
16
  "babel-core": "^6.26.3",
@@ -36,15 +35,12 @@
36
35
  "jsdoc": "github:joeyparrish/jsdoc#45c271fa",
37
36
  "karma": "^6.2.0",
38
37
  "karma-babel-preprocessor": "^7.0.0",
39
- "karma-chrome-launcher": "^2.2.0",
40
38
  "karma-coverage": "^1.1.2",
41
- "karma-edge-launcher": "^0.4.2",
42
- "karma-firefox-launcher": "^1.1.0",
43
39
  "karma-ie-launcher": "^1.0.0",
44
40
  "karma-jasmine": "^3.3.1",
45
41
  "karma-jasmine-ajax": "^0.1.13",
42
+ "karma-local-wd-launcher": "^1.4.1",
46
43
  "karma-opera-launcher": "^1.0.0",
47
- "karma-safari-launcher": "^1.0.0",
48
44
  "karma-sourcemap-loader": "^0.3.7",
49
45
  "karma-spec-reporter": "0.0.32",
50
46
  "karma-webdriver-launcher": "^1.0.8",
@@ -55,6 +51,7 @@
55
51
  "pwacompat": "^2.0.10",
56
52
  "rimraf": "^2.6.3",
57
53
  "sprintf-js": "^1.1.2",
54
+ "ssim.js": "^3.5.0",
58
55
  "stylelint": "^13.8.0",
59
56
  "stylelint-config-standard": "^20.0.0",
60
57
  "tippy.js": "^4.3.1",
@@ -44,6 +44,7 @@ goog.require('shaka.polyfill.PatchedMediaKeysMs');
44
44
  goog.require('shaka.polyfill.PatchedMediaKeysNop');
45
45
  goog.require('shaka.polyfill.PatchedMediaKeysWebkit');
46
46
  goog.require('shaka.polyfill.PiPWebkit');
47
+ goog.require('shaka.polyfill.RandomUUID');
47
48
  goog.require('shaka.polyfill.VTTCue');
48
49
  goog.require('shaka.polyfill.VideoPlayPromise');
49
50
  goog.require('shaka.polyfill.VideoPlaybackQuality');
@@ -70,6 +71,7 @@ goog.require('shaka.ui.AdPosition');
70
71
  goog.require('shaka.ui.AirPlayButton');
71
72
  goog.require('shaka.ui.BigPlayButton');
72
73
  goog.require('shaka.ui.CastButton');
74
+ goog.require('shaka.ui.ContextMenu');
73
75
  goog.require('shaka.ui.Element');
74
76
  goog.require('shaka.ui.FastForwardButton');
75
77
  goog.require('shaka.ui.FullscreenButton');
@@ -85,6 +87,7 @@ goog.require('shaka.ui.RewindButton');
85
87
  goog.require('shaka.ui.SkipAdButton');
86
88
  goog.require('shaka.ui.SmallPlayButton');
87
89
  goog.require('shaka.ui.Spacer');
90
+ goog.require('shaka.ui.StatisticsButton');
88
91
  goog.require('shaka.ui.TextSelection');
89
92
  goog.require('shaka.ui.VolumeBar');
90
93
  goog.require('shaka.util.Dom');
@@ -42,6 +42,8 @@ describe('DashParser ContentProtection', () => {
42
42
 
43
43
  const playerInterface = {
44
44
  networkingEngine: netEngine,
45
+ modifyManifestRequest: (request, manifestInfo) => {},
46
+ modifySegmentRequest: (request, segmentInfo) => {},
45
47
  filter: (manifest) => Promise.resolve(),
46
48
  makeTextStreamsForClosedCaptions: (manifest) => {},
47
49
  onTimelineRegionAdded: fail, // Should not have any EventStream elements.
@@ -39,6 +39,8 @@ describe('DashParser Live', () => {
39
39
  parser.configure(shaka.util.PlayerConfiguration.createDefault().manifest);
40
40
  playerInterface = {
41
41
  networkingEngine: fakeNetEngine,
42
+ modifyManifestRequest: (request, manifestInfo) => {},
43
+ modifySegmentRequest: (request, segmentInfo) => {},
42
44
  filter: (manifest) => Promise.resolve(),
43
45
  makeTextStreamsForClosedCaptions: (manifest) => {},
44
46
  onTimelineRegionAdded: fail, // Should not have any EventStream elements.
@@ -364,7 +366,7 @@ describe('DashParser Live', () => {
364
366
  });
365
367
  }
366
368
 
367
- it('can add Periods', async () => {
369
+ it('can add Periods with SegmentTemplate', async () => {
368
370
  const template1 = [
369
371
  '<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
370
372
  ' suggestedPresentationDelay="PT5S"',
@@ -440,6 +442,104 @@ describe('DashParser Live', () => {
440
442
  // two segments (10-14s).
441
443
  expect(stream.segmentIndex.find(9)).not.toBe(null);
442
444
  expect(stream.segmentIndex.find(13)).not.toBe(null);
445
+
446
+ stream.closeSegmentIndex();
447
+ await stream.createSegmentIndex();
448
+
449
+ expect(stream.segmentIndex.find(9)).not.toBe(null);
450
+ expect(stream.segmentIndex.find(13)).not.toBe(null);
451
+ });
452
+
453
+ it('can add Periods with SegmentList', async () => {
454
+ const list1 = [
455
+ '<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
456
+ ' suggestedPresentationDelay="PT5S"',
457
+ ' minimumUpdatePeriod="PT%(updateTime)dS">',
458
+ ' <Period id="1">',
459
+ ' <AdaptationSet mimeType="video/mp4">',
460
+ ' <Representation id="1" bandwidth="500">',
461
+ ' <BaseURL>http://example.com</BaseURL>',
462
+ ' <SegmentList>',
463
+ ' <SegmentURL media="s1.mp4" />',
464
+ ' <SegmentURL media="s2.mp4" />',
465
+ ' <SegmentURL media="s3.mp4" />',
466
+ ' <SegmentTimeline>',
467
+ ' <S d="10" t="0" r="2"/>',
468
+ ' </SegmentTimeline>',
469
+ ' </SegmentList>',
470
+ ' </Representation>',
471
+ ' </AdaptationSet>',
472
+ ' </Period>',
473
+ '</MPD>',
474
+ ].join('\n');
475
+ const list2 = [
476
+ '<MPD type="dynamic" availabilityStartTime="1970-01-01T00:00:00Z"',
477
+ ' suggestedPresentationDelay="PT5S"',
478
+ ' minimumUpdatePeriod="PT%(updateTime)dS">',
479
+ ' <Period id="1" duration="PT40S">',
480
+ ' <AdaptationSet mimeType="video/mp4">',
481
+ ' <Representation id="1" bandwidth="500">',
482
+ ' <BaseURL>http://example.com</BaseURL>',
483
+ ' <SegmentList>',
484
+ ' <SegmentURL media="s1.mp4" />',
485
+ ' <SegmentURL media="s2.mp4" />',
486
+ ' <SegmentURL media="s3.mp4" />',
487
+ ' <SegmentURL media="s4.mp4" />',
488
+ ' <SegmentTimeline>',
489
+ ' <S d="10" t="0" r="3"/>',
490
+ ' </SegmentTimeline>',
491
+ ' </SegmentList>',
492
+ ' </Representation>',
493
+ ' </AdaptationSet>',
494
+ ' </Period>',
495
+ ' <Period id="2">',
496
+ ' <AdaptationSet mimeType="video/mp4">',
497
+ ' <Representation id="2" bandwidth="500">',
498
+ ' <BaseURL>http://example.com</BaseURL>',
499
+ ' <SegmentList>',
500
+ ' <SegmentURL media="s1.mp4" />',
501
+ ' <SegmentURL media="s2.mp4" />',
502
+ ' <SegmentTimeline>',
503
+ ' <S d="10" t="0" r="1"/>',
504
+ ' </SegmentTimeline>',
505
+ ' </SegmentList>',
506
+ ' </Representation>',
507
+ ' </AdaptationSet>',
508
+ ' </Period>',
509
+ '</MPD>',
510
+ ].join('\n');
511
+ const firstManifest = sprintf(list1, {updateTime: updateTime});
512
+ const secondManifest = sprintf(list2, {updateTime: updateTime});
513
+
514
+ fakeNetEngine.setResponseText('dummy://foo', firstManifest);
515
+ // First three segments should exist.
516
+ Date.now = () => 5;
517
+
518
+ const manifest = await parser.start('dummy://foo', playerInterface);
519
+ const variant = manifest.variants[0];
520
+ const stream = variant.video;
521
+ await stream.createSegmentIndex();
522
+
523
+ // First three segments exist, but not the fourth.
524
+ expect(stream.segmentIndex.find(25)).not.toBe(null);
525
+ expect(stream.segmentIndex.find(45)).toBe(null);
526
+
527
+ fakeNetEngine.setResponseText('dummy://foo', secondManifest);
528
+ Date.now = () => 25;
529
+
530
+ await updateManifest();
531
+
532
+ // The update should have affected the same variant object we captured
533
+ // before. Now the entire first period should exist (0-40s), plus the next
534
+ // two segments of the second period(40-60s).
535
+ expect(stream.segmentIndex.find(25)).not.toBe(null);
536
+ expect(stream.segmentIndex.find(45)).not.toBe(null);
537
+
538
+ stream.closeSegmentIndex();
539
+ await stream.createSegmentIndex();
540
+
541
+ expect(stream.segmentIndex.find(25)).not.toBe(null);
542
+ expect(stream.segmentIndex.find(45)).not.toBe(null);
443
543
  });
444
544
 
445
545
  it('uses redirect URL for manifest BaseURL and updates', async () => {
@@ -1289,6 +1389,49 @@ describe('DashParser Live', () => {
1289
1389
  .toHaveBeenCalledWith(
1290
1390
  jasmine.objectContaining({id: '2', startTime: 10, endTime: 25}));
1291
1391
  });
1392
+
1393
+ it('will not add timeline regions outside the DVR window', async () => {
1394
+ const manifest = [
1395
+ '<MPD type="dynamic" minimumUpdatePeriod="PT' + updateTime + 'S"',
1396
+ ' availabilityStartTime="1970-01-01T00:00:00Z"',
1397
+ ' suggestedPresentationDelay="PT0S"',
1398
+ ' timeShiftBufferDepth="PT30S">',
1399
+ ' <Period id="1">',
1400
+ ' <EventStream schemeIdUri="http://example.com" timescale="1">',
1401
+ ' <Event id="0" presentationTime="0" duration="10"/>',
1402
+ ' <Event id="1" presentationTime="10" duration="10"/>',
1403
+ ' <Event id="2" presentationTime="20" duration="10"/>',
1404
+ ' <Event id="3" presentationTime="30" duration="10"/>',
1405
+ ' <Event id="4" presentationTime="40" duration="10"/>',
1406
+ ' </EventStream>',
1407
+ ' <AdaptationSet mimeType="video/mp4">',
1408
+ ' <Representation bandwidth="1">',
1409
+ ' <SegmentTemplate startNumber="1" media="s$Number$.mp4"',
1410
+ ' duration="2" />',
1411
+ ' </Representation>',
1412
+ ' </AdaptationSet>',
1413
+ ' </Period>',
1414
+ '</MPD>',
1415
+ ].join('\n');
1416
+
1417
+ // 45 seconds after the epoch, which is also 45 seconds after the
1418
+ // presentation started. Only the last 30 seconds are available, from
1419
+ // time 15 to 45.
1420
+ Date.now = () => 45 * 1000;
1421
+
1422
+ fakeNetEngine.setResponseText('dummy://foo', manifest);
1423
+ await parser.start('dummy://foo', playerInterface);
1424
+
1425
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledTimes(4);
1426
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith(
1427
+ jasmine.objectContaining({id: '1'}));
1428
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith(
1429
+ jasmine.objectContaining({id: '2'}));
1430
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith(
1431
+ jasmine.objectContaining({id: '3'}));
1432
+ expect(onTimelineRegionAddedSpy).toHaveBeenCalledWith(
1433
+ jasmine.objectContaining({id: '4'}));
1434
+ });
1292
1435
  });
1293
1436
 
1294
1437
  it('honors clockSyncUri for in-progress recordings', async () => {
@@ -57,6 +57,8 @@ describe('DashParser Manifest', () => {
57
57
  onEventSpy = jasmine.createSpy('onEvent');
58
58
  playerInterface = {
59
59
  networkingEngine: fakeNetEngine,
60
+ modifyManifestRequest: (request, manifestInfo) => {},
61
+ modifySegmentRequest: (request, segmentInfo) => {},
60
62
  filter: (manifest) => Promise.resolve(),
61
63
  makeTextStreamsForClosedCaptions: (manifest) => {},
62
64
  onTimelineRegionAdded: fail, // Should not have any EventStream elements.
@@ -983,7 +985,8 @@ describe('DashParser Manifest', () => {
983
985
 
984
986
  it('duplicate Representation ids with live', async () => {
985
987
  const source = [
986
- '<MPD minBufferTime="PT75S" type="dynamic">',
988
+ '<MPD minBufferTime="PT75S" type="dynamic"',
989
+ ' availabilityStartTime="1970-01-01T00:00:00Z">',
987
990
  ' <Period id="1" duration="PT30S">',
988
991
  ' <AdaptationSet mimeType="video/mp4">',
989
992
  ' <Representation id="1" bandwidth="1">',
@@ -1872,7 +1875,7 @@ describe('DashParser Manifest', () => {
1872
1875
  expect(textStream.kind).toBe('caption');
1873
1876
  });
1874
1877
 
1875
- it('converts Roles element to "forced"', async () => {
1878
+ it('converts Roles element to "forced" (old role)', async () => {
1876
1879
  const manifestText = [
1877
1880
  '<MPD minBufferTime="PT75S">',
1878
1881
  ' <Period id="1" duration="PT30S">',
@@ -1904,6 +1907,38 @@ describe('DashParser Manifest', () => {
1904
1907
  expect(textStream.forced).toBe(true);
1905
1908
  });
1906
1909
 
1910
+ it('converts Roles element to "forced"', async () => {
1911
+ const manifestText = [
1912
+ '<MPD minBufferTime="PT75S">',
1913
+ ' <Period id="1" duration="PT30S">',
1914
+ ' <AdaptationSet id="1" contentType="text">',
1915
+ ' <Accessibility schemeIdUri="urn:mpeg:dash:role:2011" ',
1916
+ ' value="captions" />',
1917
+ ' <Accessibility schemeIdUri="urn:mpeg:dash:role:2011" ',
1918
+ ' value="forced-subtitle" />',
1919
+ ' <Accessibility schemeIdUri="foobar" value="bar" />',
1920
+ ' <Representation id="text-en" mimeType="text/webvtt">',
1921
+ ' <BaseURL>t-en.vtt</BaseURL>',
1922
+ ' </Representation>',
1923
+ ' </AdaptationSet>',
1924
+ ' <AdaptationSet id="1" mimeType="video/mp4">',
1925
+ ' <Representation id="video-sd" width="640" height="480">',
1926
+ ' <BaseURL>v-sd.mp4</BaseURL>',
1927
+ ' <SegmentBase indexRange="100-200" />',
1928
+ ' </Representation>',
1929
+ ' </AdaptationSet>',
1930
+ ' </Period>',
1931
+ '</MPD>',
1932
+ ].join('\n');
1933
+
1934
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
1935
+ /** @type {shaka.extern.Manifest} */
1936
+ const manifest = await parser.start('dummy://foo', playerInterface);
1937
+ const textStream = manifest.textStreams[0];
1938
+ expect(textStream.roles).toEqual(['captions', 'forced-subtitle']);
1939
+ expect(textStream.forced).toBe(true);
1940
+ });
1941
+
1907
1942
  it('supports HDR signaling via profiles', async () => {
1908
1943
  // (DASH-IF IOP v4.3 10.3.3.)
1909
1944
  const hdrProfile =
@@ -35,6 +35,8 @@ describe('DashParser SegmentBase', () => {
35
35
 
36
36
  playerInterface = {
37
37
  networkingEngine: fakeNetEngine,
38
+ modifyManifestRequest: (request, manifestInfo) => {},
39
+ modifySegmentRequest: (request, segmentInfo) => {},
38
40
  filter: (manifest) => Promise.resolve(),
39
41
  makeTextStreamsForClosedCaptions: (manifest) => {},
40
42
  onTimelineRegionAdded: fail, // Should not have any EventStream elements.
@@ -344,6 +344,8 @@ describe('DashParser SegmentList', () => {
344
344
 
345
345
  const playerInterface = {
346
346
  networkingEngine: networkingEngine,
347
+ modifyManifestRequest: (request, manifestInfo) => {},
348
+ modifySegmentRequest: (request, segmentInfo) => {},
347
349
  filter: () => {},
348
350
  makeTextStreamsForClosedCaptions: (manifest) => {},
349
351
  onTimelineRegionAdded: fail, // Should not have any EventStream elements.
@@ -374,4 +376,3 @@ describe('DashParser SegmentList', () => {
374
376
  expect(actual).toEqual(expected);
375
377
  });
376
378
  });
377
-
@@ -46,6 +46,8 @@ describe('DashParser SegmentTemplate', () => {
46
46
 
47
47
  playerInterface = {
48
48
  networkingEngine: fakeNetEngine,
49
+ modifyManifestRequest: (request, manifestInfo) => {},
50
+ modifySegmentRequest: (request, segmentInfo) => {},
49
51
  filter: (manifest) => Promise.resolve(),
50
52
  makeTextStreamsForClosedCaptions: (manifest) => {},
51
53
  onTimelineRegionAdded: fail, // Should not have any EventStream elements.
@@ -585,4 +587,3 @@ describe('DashParser SegmentTemplate', () => {
585
587
  });
586
588
  });
587
589
  });
588
-
@@ -116,6 +116,8 @@ describe('HlsParser live', () => {
116
116
 
117
117
  config = shaka.util.PlayerConfiguration.createDefault().manifest;
118
118
  playerInterface = {
119
+ modifyManifestRequest: (request, manifestInfo) => {},
120
+ modifySegmentRequest: (request, segmentInfo) => {},
119
121
  filter: () => Promise.resolve(),
120
122
  makeTextStreamsForClosedCaptions: (manifest) => {},
121
123
  networkingEngine: fakeNetEngine,
@@ -94,6 +94,8 @@ describe('HlsParser', () => {
94
94
  config = shaka.util.PlayerConfiguration.createDefault().manifest;
95
95
  onEventSpy = jasmine.createSpy('onEvent');
96
96
  playerInterface = {
97
+ modifyManifestRequest: (request, manifestInfo) => {},
98
+ modifySegmentRequest: (request, segmentInfo) => {},
97
99
  filter: () => Promise.resolve(),
98
100
  makeTextStreamsForClosedCaptions: (manifest) => {},
99
101
  networkingEngine: fakeNetEngine,
@@ -209,6 +211,80 @@ describe('HlsParser', () => {
209
211
  expect(actual).toEqual(manifest);
210
212
  });
211
213
 
214
+ it('parses manifest attributes with space', async () => {
215
+ const master = [
216
+ '#EXTM3U\n',
217
+ '#EXT-X-MEDIA:TYPE=AUDIO, GROUP-ID="aud1", LANGUAGE="eng", ',
218
+ 'CHANNELS="16/JOC", URI="audio"\n',
219
+ '#EXT-X-MEDIA:TYPE=SUBTITLES, GROUP-ID="sub1", LANGUAGE="eng", ',
220
+ 'URI="text"\n',
221
+ '#EXT-X-MEDIA:TYPE=SUBTITLES, GROUP-ID="sub2", LANGUAGE="es", ',
222
+ 'URI="text2"\n',
223
+ '#EXT-X-STREAM-INF:BANDWIDTH=200, CODECS="avc1,mp4a", ',
224
+ 'RESOLUTION=960x540, FRAME-RATE=60, AUDIO="aud1"\n',
225
+ 'video\n',
226
+ ].join('');
227
+
228
+ const media = [
229
+ '#EXTM3U\n',
230
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
231
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
232
+ '#EXTINF:5,\n',
233
+ '#EXT-X-BYTERANGE:121090@616\n',
234
+ 'main.mp4',
235
+ ].join('');
236
+
237
+ const textMedia = [
238
+ '#EXTM3U\n',
239
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
240
+ '#EXTINF:5,\n',
241
+ '#EXT-X-BYTERANGE:121090@616\n',
242
+ 'main.vtt',
243
+ ].join('');
244
+
245
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
246
+ manifest.anyTimeline();
247
+ manifest.addPartialVariant((variant) => {
248
+ variant.language = 'en';
249
+ variant.bandwidth = 200;
250
+ variant.addPartialStream(ContentType.VIDEO, (stream) => {
251
+ stream.frameRate = 60;
252
+ stream.mime('video/mp4', 'avc1');
253
+ stream.size(960, 540);
254
+ });
255
+ variant.addPartialStream(ContentType.AUDIO, (stream) => {
256
+ stream.language = 'en';
257
+ stream.channelsCount = 16;
258
+ stream.spatialAudio = true;
259
+ stream.mime('audio/mp4', 'mp4a');
260
+ });
261
+ });
262
+ manifest.addPartialTextStream((stream) => {
263
+ stream.language = 'en';
264
+ stream.kind = TextStreamKind.SUBTITLE;
265
+ stream.mime('text/vtt', '');
266
+ });
267
+ manifest.addPartialTextStream((stream) => {
268
+ stream.language = 'es';
269
+ stream.kind = TextStreamKind.SUBTITLE;
270
+ stream.mime('text/vtt', '');
271
+ });
272
+ });
273
+
274
+ fakeNetEngine
275
+ .setResponseText('test:/master', master)
276
+ .setResponseText('test:/audio', media)
277
+ .setResponseText('test:/video', media)
278
+ .setResponseText('test:/text', textMedia)
279
+ .setResponseText('test:/text2', textMedia)
280
+ .setResponseText('test:/main.vtt', vttText)
281
+ .setResponseValue('test:/init.mp4', initSegmentData)
282
+ .setResponseValue('test:/main.mp4', segmentData);
283
+
284
+ const actual = await parser.start('test:/master', playerInterface);
285
+ expect(actual).toEqual(manifest);
286
+ });
287
+
212
288
  it('prioritize AVERAGE-BANDWIDTH to BANDWIDTH', async () => {
213
289
  const master = [
214
290
  '#EXTM3U\n',
@@ -31,7 +31,7 @@ describe('DrmEngine', () => {
31
31
  navigator.requestMediaKeySystemAccess;
32
32
  const originalLogError = shaka.log.error;
33
33
  const originalBatchTime = shaka.media.DrmEngine.KEY_STATUS_BATCH_TIME;
34
- const originalDecodingInfo = window.shakaMediaCapabilities.decodingInfo;
34
+ const originalDecodingInfo = navigator.mediaCapabilities.decodingInfo;
35
35
 
36
36
  /** @type {!jasmine.Spy} */
37
37
  let decodingInfoSpy;
@@ -76,7 +76,7 @@ describe('DrmEngine', () => {
76
76
 
77
77
  beforeEach(() => {
78
78
  decodingInfoSpy = jasmine.createSpy('decodingInfo');
79
- window.shakaMediaCapabilities.decodingInfo =
79
+ navigator.mediaCapabilities.decodingInfo =
80
80
  shaka.test.Util.spyFunc(decodingInfoSpy);
81
81
 
82
82
  logErrorSpy = jasmine.createSpy('shaka.log.error');
@@ -153,7 +153,7 @@ describe('DrmEngine', () => {
153
153
 
154
154
  navigator.requestMediaKeySystemAccess =
155
155
  originalRequestMediaKeySystemAccess;
156
- window.shakaMediaCapabilities.decodingInfo = originalDecodingInfo;
156
+ navigator.mediaCapabilities.decodingInfo = originalDecodingInfo;
157
157
  shaka.log.error = originalLogError;
158
158
  });
159
159
 
@@ -0,0 +1,142 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ goog.require('shaka.media.QualityObserver');
8
+ goog.require('shaka.test.Util');
9
+
10
+
11
+ describe('QualityObserver', () => {
12
+ /** @type {!shaka.media.QualityObserver} */
13
+ let observer;
14
+
15
+ /** @type {!jasmine.Spy} */
16
+ let onQualityChange;
17
+
18
+ const createQualityInfo = (contentType, bandwidth) => {
19
+ return {
20
+ bandwidth: bandwidth,
21
+ audioSamplingRate: 444000,
22
+ codecs: 'my codec',
23
+ contentType: contentType,
24
+ frameRate: 30,
25
+ height: 720,
26
+ mimeType: 'mime type',
27
+ channelsCount: 2,
28
+ pixelAspectRatio: '1:1',
29
+ width: 1280,
30
+ };
31
+ };
32
+ let emptyBuffer = true;
33
+ let bufferStart = 0;
34
+ let bufferEnd = 0;
35
+
36
+ const quality1 = createQualityInfo('video', 1);
37
+ const quality2 = createQualityInfo('video', 2);
38
+
39
+ const getBufferedInfo = () => {
40
+ if (emptyBuffer) {
41
+ return {
42
+ video: [],
43
+ };
44
+ }
45
+ return {
46
+ video: [{start: bufferStart, end: bufferEnd}],
47
+ };
48
+ };
49
+
50
+ beforeEach(() => {
51
+ onQualityChange = jasmine.createSpy('onQualityChange');
52
+ observer = new shaka.media.QualityObserver(getBufferedInfo);
53
+ observer.addEventListener('qualitychange', (event) => {
54
+ shaka.test.Util.spyFunc(onQualityChange)(
55
+ event['quality'], event['position']);
56
+ });
57
+ emptyBuffer = true;
58
+ bufferStart = 0;
59
+ bufferEnd = 0;
60
+ });
61
+
62
+ it('does not call onQualityChange when there are no quality changes', () => {
63
+ observer.poll(10, false);
64
+ expect(onQualityChange).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('calls onQualityChange when position is after 1st quality change', () => {
68
+ observer.addMediaQualityChange(quality1, 10);
69
+ emptyBuffer = false;
70
+ bufferStart = 10;
71
+ bufferEnd = 20;
72
+ observer.poll(10, false);
73
+ expect(onQualityChange).toHaveBeenCalledWith(quality1, 10);
74
+ });
75
+
76
+ it('does not call onQualityChange when pos advances with no change', () => {
77
+ observer.addMediaQualityChange(quality1, 10);
78
+ emptyBuffer = false;
79
+ bufferStart = 10;
80
+ bufferEnd = 20;
81
+ observer.poll(10, false);
82
+ expect(onQualityChange).toHaveBeenCalledWith(quality1, 10);
83
+ observer.poll(11, false);
84
+ expect(onQualityChange).toHaveBeenCalledTimes(1);
85
+ });
86
+
87
+ it('does not call onQualityChange on seek to unbuffered position', () => {
88
+ observer.addMediaQualityChange(quality1, 10);
89
+ emptyBuffer = false;
90
+ bufferStart = 10;
91
+ bufferEnd = 20;
92
+ observer.poll(15, false);
93
+ expect(onQualityChange).toHaveBeenCalledOnceMoreWith([quality1, 15]);
94
+ observer.addMediaQualityChange(quality2, 20);
95
+ observer.poll(25, true);
96
+ expect(onQualityChange).not.toHaveBeenCalledOnceMore();
97
+ bufferEnd = 30;
98
+ observer.poll(26, false);
99
+ expect(onQualityChange).toHaveBeenCalledOnceMoreWith([quality2, 26]);
100
+ });
101
+
102
+ it('calls onQualityChange when position advances over 2nd quality change',
103
+ () => {
104
+ observer.addMediaQualityChange(quality1, 10);
105
+ emptyBuffer = false;
106
+ bufferStart = 10;
107
+ bufferEnd = 20;
108
+ observer.poll(10, false);
109
+ expect(onQualityChange).toHaveBeenCalledOnceMoreWith([quality1, 10]);
110
+ observer.addMediaQualityChange(quality2, 20);
111
+ bufferStart = 10;
112
+ bufferEnd = 30;
113
+ observer.poll(20, false);
114
+ expect(onQualityChange).toHaveBeenCalledOnceMoreWith([quality2, 20]);
115
+ });
116
+
117
+ it('calls onQualityChange when position moves back over a quality chanage',
118
+ () => {
119
+ observer.addMediaQualityChange(quality1, 10);
120
+ emptyBuffer = false;
121
+ bufferStart = 10;
122
+ bufferEnd = 20;
123
+ observer.addMediaQualityChange(quality2, 20);
124
+ bufferStart = 10;
125
+ bufferEnd = 30;
126
+ observer.poll(25, false);
127
+ expect(onQualityChange).toHaveBeenCalledOnceMoreWith([quality2, 25]);
128
+ observer.poll(15, false);
129
+ expect(onQualityChange).toHaveBeenCalledOnceMoreWith([quality1, 15]);
130
+ });
131
+
132
+ it('uses last applied quality when there are two at the same position',
133
+ () => {
134
+ observer.addMediaQualityChange(quality1, 10);
135
+ observer.addMediaQualityChange(quality2, 10);
136
+ emptyBuffer = false;
137
+ bufferStart = 10;
138
+ bufferEnd = 20;
139
+ observer.poll(15, false);
140
+ expect(onQualityChange).toHaveBeenCalledWith(quality2, 15);
141
+ });
142
+ });