shaka-player 3.0.11 → 3.0.15

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 (142) hide show
  1. package/AUTHORS +5 -0
  2. package/CHANGELOG.md +132 -0
  3. package/CONTRIBUTORS +5 -0
  4. package/README.md +3 -0
  5. package/build/apps.py +2 -2
  6. package/build/check.py +2 -2
  7. package/build/conformance.textproto +0 -9
  8. package/build/generateExterns.js +1 -1
  9. package/build/shakaBuildHelpers.py +6 -5
  10. package/build/subprocessWindowsPatch.py +103 -0
  11. package/demo/common/assets.js +1 -1
  12. package/demo/common/message_ids.js +1 -0
  13. package/demo/config.js +2 -1
  14. package/demo/demo.less +4 -0
  15. package/demo/demo_utils.js +1 -0
  16. package/demo/input_container.js +2 -1
  17. package/demo/locales/en.json +1 -0
  18. package/demo/locales/source.json +4 -0
  19. package/dist/controls.css +2 -2
  20. package/dist/controls.css.map +1 -1
  21. package/dist/demo.compiled.debug.js +63 -60
  22. package/dist/demo.compiled.debug.map +3 -3
  23. package/dist/demo.compiled.js +63 -60
  24. package/dist/demo.compiled.map +3 -3
  25. package/dist/demo.css +1 -1
  26. package/dist/demo.css.map +1 -1
  27. package/dist/deps.js +9 -9
  28. package/dist/locales.js +35 -33
  29. package/dist/receiver.compiled.debug.js +59 -56
  30. package/dist/receiver.compiled.debug.map +3 -3
  31. package/dist/receiver.compiled.js +59 -56
  32. package/dist/receiver.compiled.map +3 -3
  33. package/dist/shaka-player.compiled.d.ts +131 -5
  34. package/dist/shaka-player.compiled.debug.d.ts +131 -5
  35. package/dist/shaka-player.compiled.debug.externs.js +142 -8
  36. package/dist/shaka-player.compiled.debug.js +330 -312
  37. package/dist/shaka-player.compiled.debug.map +4 -4
  38. package/dist/shaka-player.compiled.externs.js +142 -8
  39. package/dist/shaka-player.compiled.js +707 -702
  40. package/dist/shaka-player.compiled.map +4 -4
  41. package/dist/shaka-player.ui.d.ts +131 -5
  42. package/dist/shaka-player.ui.debug.d.ts +131 -5
  43. package/dist/shaka-player.ui.debug.externs.js +144 -10
  44. package/dist/shaka-player.ui.debug.js +354 -333
  45. package/dist/shaka-player.ui.debug.map +4 -4
  46. package/dist/shaka-player.ui.externs.js +144 -10
  47. package/dist/shaka-player.ui.js +860 -855
  48. package/dist/shaka-player.ui.map +4 -4
  49. package/docs/tutorials/fairplay.md +1 -1
  50. package/docs/tutorials/faq.md +25 -0
  51. package/externs/aria.js +18 -0
  52. package/externs/ima.js +4 -0
  53. package/externs/shaka/ads.js +2 -0
  54. package/karma.conf.js +1 -0
  55. package/lib/ads/ad_manager.js +29 -11
  56. package/lib/ads/client_side_ad_manager.js +76 -86
  57. package/lib/ads/server_side_ad_manager.js +26 -11
  58. package/lib/cast/cast_proxy.js +3 -2
  59. package/lib/cast/cast_receiver.js +6 -2
  60. package/lib/cast/cast_sender.js +2 -2
  61. package/lib/dash/dash_parser.js +63 -1
  62. package/lib/dash/segment_list.js +1 -1
  63. package/lib/dash/segment_template.js +41 -14
  64. package/lib/hls/hls_parser.js +41 -11
  65. package/lib/media/content_workarounds.js +18 -0
  66. package/lib/media/drm_engine.js +8 -0
  67. package/lib/media/gap_jumping_controller.js +8 -4
  68. package/lib/media/media_source_engine.js +7 -0
  69. package/lib/media/playhead.js +1 -1
  70. package/lib/media/presentation_timeline.js +3 -2
  71. package/lib/media/segment_index.js +41 -11
  72. package/lib/media/streaming_engine.js +34 -21
  73. package/lib/net/networking_engine.js +2 -1
  74. package/lib/offline/download_manager.js +1 -2
  75. package/lib/player.js +99 -44
  76. package/lib/polyfill/encryption_scheme.js +2 -0
  77. package/lib/polyfill/fullscreen.js +2 -0
  78. package/lib/polyfill/mathround.js +2 -0
  79. package/lib/polyfill/mediasource.js +2 -0
  80. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  81. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  82. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  83. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  84. package/lib/polyfill/pip_webkit.js +2 -0
  85. package/lib/polyfill/video_play_promise.js +2 -0
  86. package/lib/polyfill/videoplaybackquality.js +2 -0
  87. package/lib/polyfill/vttcue.js +2 -0
  88. package/lib/text/mp4_ttml_parser.js +3 -0
  89. package/lib/text/ttml_text_parser.js +5 -0
  90. package/lib/text/ui_text_displayer.js +37 -3
  91. package/lib/util/error.js +1 -4
  92. package/lib/util/fake_event.js +53 -7
  93. package/lib/util/mime_utils.js +33 -0
  94. package/lib/util/periods.js +25 -3
  95. package/lib/util/stream_utils.js +11 -0
  96. package/package.json +3 -3
  97. package/test/cast/cast_proxy_unit.js +12 -6
  98. package/test/cast/cast_receiver_integration.js +1 -1
  99. package/test/cast/cast_utils_unit.js +1 -1
  100. package/test/dash/dash_parser_manifest_unit.js +73 -0
  101. package/test/demo/demo_unit.js +0 -1
  102. package/test/hls/hls_parser_unit.js +45 -1
  103. package/test/media/drm_engine_unit.js +56 -0
  104. package/test/media/playhead_unit.js +27 -1
  105. package/test/media/presentation_timeline_unit.js +19 -0
  106. package/test/media/segment_index_unit.js +79 -6
  107. package/test/media/streaming_engine_unit.js +3 -3
  108. package/test/player_integration.js +38 -0
  109. package/test/player_unit.js +19 -1
  110. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  111. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  112. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  113. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  114. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  115. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  116. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  117. package/test/test/assets/3675/dash_0.mpd +30 -0
  118. package/test/test/assets/3675/dash_1.mpd +30 -0
  119. package/test/test/assets/3675/dash_2.mpd +30 -0
  120. package/test/test/assets/3675/dash_3.mpd +30 -0
  121. package/test/test/assets/3675/dash_4.mpd +30 -0
  122. package/test/test/assets/3675/dash_5.mpd +26 -0
  123. package/test/test/assets/3675/init-stream0.m4s +0 -0
  124. package/test/test/util/fake_ad_manager.js +4 -1
  125. package/test/test/util/fake_media_source_engine.js +4 -0
  126. package/test/test/util/simple_fakes.js +5 -3
  127. package/test/test/util/waiter.js +36 -2
  128. package/test/text/ttml_text_parser_unit.js +9 -0
  129. package/test/text/ui_text_displayer_unit.js +19 -3
  130. package/test/ui/localization_unit.js +15 -14
  131. package/test/ui/ui_unit.js +4 -0
  132. package/ui/big_play_button.js +10 -0
  133. package/ui/cast_button.js +2 -3
  134. package/ui/controls.js +9 -7
  135. package/ui/less/containers.less +3 -0
  136. package/ui/locales/en.json +1 -0
  137. package/ui/locales/ru.json +6 -6
  138. package/ui/locales/source.json +4 -0
  139. package/ui/localization.js +28 -51
  140. package/ui/pip_button.js +2 -3
  141. package/ui/play_button.js +10 -10
  142. package/ui/small_play_button.js +12 -0
@@ -88,6 +88,9 @@ shaka.text.Mp4TtmlParser = class {
88
88
 
89
89
  shaka.text.TextEngine.registerParser(
90
90
  'application/mp4; codecs="stpp"', () => new shaka.text.Mp4TtmlParser());
91
+ shaka.text.TextEngine.registerParser(
92
+ 'application/mp4; codecs="stpp.ttml"',
93
+ () => new shaka.text.Mp4TtmlParser());
91
94
  shaka.text.TextEngine.registerParser(
92
95
  'application/mp4; codecs="stpp.ttml.im1t"',
93
96
  () => new shaka.text.Mp4TtmlParser());
@@ -277,6 +277,11 @@ shaka.text.TtmlTextParser = class {
277
277
  !cueElement.hasAttribute('begin') &&
278
278
  !cueElement.hasAttribute('end');
279
279
 
280
+ if (cueElement.nodeType == Node.COMMENT_NODE) {
281
+ // The comments do not contain information that interests us here.
282
+ return null;
283
+ }
284
+
280
285
  if (
281
286
  cueElement.nodeType != Node.ELEMENT_NODE ||
282
287
  /* Disregards empty elements without time attributes nor content
@@ -11,6 +11,7 @@ goog.require('goog.asserts');
11
11
  goog.require('shaka.text.Cue');
12
12
  goog.require('shaka.text.CueRegion');
13
13
  goog.require('shaka.util.Dom');
14
+ goog.require('shaka.util.EventManager');
14
15
  goog.require('shaka.util.Timer');
15
16
 
16
17
 
@@ -70,6 +71,22 @@ shaka.text.UITextDisplayer = class {
70
71
 
71
72
  /** private {Map.<!shaka.extern.Cue, !HTMLElement>} */
72
73
  this.currentCuesMap_ = new Map();
74
+
75
+ /** @private {shaka.util.EventManager} */
76
+ this.eventManager_ = new shaka.util.EventManager();
77
+
78
+ this.eventManager_.listen(document, 'fullscreenchange', () => {
79
+ this.updateCaptions_(/* forceUpdate= */ true);
80
+ });
81
+
82
+ /** @private {ResizeObserver} */
83
+ this.resizeObserver_ = null;
84
+ if ('ResizeObserver' in window) {
85
+ this.resizeObserver_ = new ResizeObserver(() => {
86
+ this.updateCaptions_(/* forceUpdate= */ true);
87
+ });
88
+ this.resizeObserver_.observe(this.textContainer_);
89
+ }
73
90
  }
74
91
 
75
92
 
@@ -114,6 +131,17 @@ shaka.text.UITextDisplayer = class {
114
131
  }
115
132
 
116
133
  this.currentCuesMap_.clear();
134
+
135
+ // Tear-down the event manager to ensure messages stop moving around.
136
+ if (this.eventManager_) {
137
+ this.eventManager_.release();
138
+ this.eventManager_ = null;
139
+ }
140
+
141
+ if (this.resizeObserver_) {
142
+ this.resizeObserver_.disconnect();
143
+ this.resizeObserver_ = null;
144
+ }
117
145
  }
118
146
 
119
147
 
@@ -128,9 +156,14 @@ shaka.text.UITextDisplayer = class {
128
156
  }
129
157
 
130
158
  // Remove the cues out of the time range.
159
+ const oldNumCues = this.cues_.length;
131
160
  this.cues_ = this.cues_.filter(
132
161
  (cue) => cue.startTime < start || cue.endTime >= end);
133
- this.updateCaptions_();
162
+ // If anything was actually removed in this process, force the captions to
163
+ // update. This makes sure that the currently-displayed cues will stop
164
+ // displaying if removed (say, due to the user changing languages).
165
+ const forceUpdate = oldNumCues > this.cues_.length;
166
+ this.updateCaptions_(forceUpdate);
134
167
 
135
168
  return true;
136
169
  }
@@ -154,9 +187,10 @@ shaka.text.UITextDisplayer = class {
154
187
 
155
188
  /**
156
189
  * Display the current captions.
190
+ * @param {boolean=} forceUpdate
157
191
  * @private
158
192
  */
159
- updateCaptions_() {
193
+ updateCaptions_(forceUpdate = false) {
160
194
  const currentTime = this.video_.currentTime;
161
195
 
162
196
  // Return true if the cue should be displayed at the current time point.
@@ -168,7 +202,7 @@ shaka.text.UITextDisplayer = class {
168
202
  // For each cue in the current cues map, if the cue's end time has passed,
169
203
  // remove the entry from the map, and remove the captions from the page.
170
204
  for (const cue of this.currentCuesMap_.keys()) {
171
- if (!shouldCueBeDisplayed(cue)) {
205
+ if (!shouldCueBeDisplayed(cue) || forceUpdate) {
172
206
  const captions = this.currentCuesMap_.get(cue);
173
207
  this.textContainer_.removeChild(captions);
174
208
  this.currentCuesMap_.delete(cue);
package/lib/util/error.js CHANGED
@@ -22,10 +22,7 @@ goog.provide('shaka.util.Error');
22
22
  * <ul>
23
23
  * <li><a href="https://developer.mozilla.org/en-US/docs/Web/API/MediaError">MediaError</a>
24
24
  * <li><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status">HTTP Codes</a>
25
- * <li>
26
- * <a href="https://docs.microsoft.com/en-us/windows/win32/wmdm/error-codes">PlayReady errors</a>
27
- * or
28
- * <a href="https://github.com/tpn/winsdk-10/blob/master/Include/10.0.16299.0/winrt/Windows.Media.Protection.PlayReadyErrors.h">more PlayReady errors</a>
25
+ * <li><a href="https://hresult.info">Edge/PlayReady errors</a>
29
26
  * </ul>
30
27
  *
31
28
  * @export
@@ -6,6 +6,8 @@
6
6
 
7
7
  goog.provide('shaka.util.FakeEvent');
8
8
 
9
+ goog.require('goog.asserts');
10
+
9
11
 
10
12
  /**
11
13
  * @summary Create an Event work-alike object based on the provided dictionary.
@@ -16,18 +18,62 @@ goog.provide('shaka.util.FakeEvent');
16
18
  */
17
19
  shaka.util.FakeEvent = class {
18
20
  /**
19
- * @param {string} type
20
- * @param {Object=} dict
21
+ * @param {!Event} event
22
+ * @return {!shaka.util.FakeEvent}
21
23
  */
22
- constructor(type, dict = {}) {
23
- // Take properties from dict if present.
24
- for (const key in dict) {
25
- Object.defineProperty(this, key, {
26
- value: dict[key],
24
+ static fromRealEvent(event) {
25
+ const fakeEvent = new shaka.util.FakeEvent(event.type);
26
+ for (const key in event) {
27
+ Object.defineProperty(fakeEvent, key, {
28
+ value: event[key],
27
29
  writable: true,
28
30
  enumerable: true,
29
31
  });
30
32
  }
33
+ return fakeEvent;
34
+ }
35
+
36
+ /**
37
+ * Allows us to tell the compiler that the dictionary "map" is actually a
38
+ * generic object, for backwards compatibility.
39
+ * @param {!Map.<string, Object>} dict
40
+ * @return {!Object}
41
+ * @suppress {invalidCasts}
42
+ * @private
43
+ */
44
+ static recastDictAsObject_(dict) {
45
+ goog.asserts.assert(!(dict instanceof Map), 'dict should not be a map');
46
+ return /** @type {!Object} */ (dict);
47
+ }
48
+
49
+ /**
50
+ * @param {string} type
51
+ * @param {Map.<string, Object>=} dict
52
+ */
53
+ constructor(type, dict) {
54
+ if (dict) {
55
+ if (dict instanceof Map) {
56
+ // Take properties from dict if present.
57
+ for (const key of dict.keys()) {
58
+ Object.defineProperty(this, key, {
59
+ value: dict.get(key),
60
+ writable: true,
61
+ enumerable: true,
62
+ });
63
+ }
64
+ } else {
65
+ // For backwards compatibility with external apps that may make use of
66
+ // this public constructor, this should still accept generic objects.
67
+ const obj = shaka.util.FakeEvent.recastDictAsObject_(dict);
68
+ for (const key in obj) {
69
+ Object.defineProperty(this, key, {
70
+ value: obj[key],
71
+ writable: true,
72
+ enumerable: true,
73
+ });
74
+ }
75
+ }
76
+ }
31
77
 
32
78
  // The properties below cannot be set by the dict. They are all provided
33
79
  // for compatibility with native events.
@@ -79,6 +79,39 @@ shaka.util.MimeUtils = class {
79
79
  return parts[0];
80
80
  }
81
81
 
82
+ /**
83
+ * Takes a full MIME type (with codecs) or basic MIME type (without codecs)
84
+ * and returns a basic MIME type (without codecs or other parameters).
85
+ *
86
+ * @param {string} mimeType
87
+ * @return {string}
88
+ */
89
+ static getBasicType(mimeType) {
90
+ return mimeType.split(';')[0];
91
+ }
92
+
93
+ /**
94
+ * Takes a MIME type and returns the codecs parameter, or an empty string if
95
+ * there is no codecs parameter.
96
+ *
97
+ * @param {string} mimeType
98
+ * @return {string}
99
+ */
100
+ static getCodecs(mimeType) {
101
+ // Parse the basic MIME type from its parameters.
102
+ const pieces = mimeType.split(/ *; */);
103
+ pieces.shift(); // Remove basic MIME type from pieces.
104
+
105
+ const codecs = pieces.find((piece) => piece.startsWith('codecs='));
106
+ if (!codecs) {
107
+ return '';
108
+ }
109
+
110
+ // The value may be quoted, so remove quotes at the beginning or end.
111
+ const value = codecs.split('=')[1].replace(/^"|"$/g, '');
112
+ return value;
113
+ }
114
+
82
115
  /**
83
116
  * Get the base and profile of a codec string. Where [0] will be the codec
84
117
  * base and [1] will be the profile.
@@ -75,7 +75,10 @@ shaka.util.PeriodCombiner = class {
75
75
 
76
76
  /** @return {!Array.<shaka.extern.Stream>} */
77
77
  getTextStreams() {
78
- return this.textStreams_;
78
+ // Return a copy of the array because makeTextStreamsForClosedCaptions
79
+ // may make changes to the contents of the array. Those changes should not
80
+ // propagate back to the PeriodCombiner.
81
+ return this.textStreams_.slice();
79
82
  }
80
83
 
81
84
  /**
@@ -551,7 +554,7 @@ shaka.util.PeriodCombiner = class {
551
554
 
552
555
  // We need to create all the per-period segment indexes and append them to
553
556
  // the output's MetaSegmentIndex.
554
- await Promise.all(matches.map((match) => match.createSegmentIndex()));
557
+ await shaka.util.PeriodCombiner.createSegmentIndexes_(matches);
555
558
 
556
559
  // Assure the compiler that matches didn't become null during the async
557
560
  // operation above.
@@ -563,6 +566,25 @@ shaka.util.PeriodCombiner = class {
563
566
  return true;
564
567
  }
565
568
 
569
+ /**
570
+ * Creates all segment indexes for an array of streams. Returns once every
571
+ * segment index is created.
572
+ *
573
+ * @param {!Array.<!shaka.extern.Stream>} streams
574
+ * @return {!Promise}
575
+ * @private
576
+ */
577
+ static createSegmentIndexes_(streams) {
578
+ const operations = [];
579
+ for (const stream of streams) {
580
+ operations.push(stream.createSegmentIndex());
581
+ if (stream.trickModeVideo && !stream.trickModeVideo.segmentIndex) {
582
+ operations.push(stream.trickModeVideo.createSegmentIndex());
583
+ }
584
+ }
585
+ return Promise.all(operations);
586
+ }
587
+
566
588
  /**
567
589
  * Create a new output Stream based on a particular input Stream. Locates
568
590
  * matching Streams in all other periods and combines them into an output
@@ -605,7 +627,7 @@ shaka.util.PeriodCombiner = class {
605
627
  if (outputStream.createSegmentIndex) {
606
628
  // For T == Stream, we need to create all the per-period segment indexes
607
629
  // in advance. concat() will add them to the output's MetaSegmentIndex.
608
- await Promise.all(matches.map((match) => match.createSegmentIndex()));
630
+ await shaka.util.PeriodCombiner.createSegmentIndexes_(matches);
609
631
  }
610
632
 
611
633
  // Assure the compiler that matches didn't become null during the async
@@ -16,6 +16,7 @@ goog.require('shaka.util.LanguageUtils');
16
16
  goog.require('shaka.util.ManifestParserUtils');
17
17
  goog.require('shaka.util.MimeUtils');
18
18
  goog.require('shaka.util.MultiMap');
19
+ goog.require('shaka.util.Platform');
19
20
 
20
21
 
21
22
  /**
@@ -296,6 +297,16 @@ shaka.util.StreamUtils = class {
296
297
  return false;
297
298
  }
298
299
 
300
+ // See: https://github.com/google/shaka-player/issues/3380
301
+ if (shaka.util.Platform.isXboxOne() && video &&
302
+ ((video.width && video.width > 1920) ||
303
+ (video.height && video.height > 1080)) &&
304
+ video.codecs.includes('avc1.')) {
305
+ shaka.log.debug('Dropping variant - video not compatible with platform',
306
+ StreamUtils.getStreamSummaryString_(video));
307
+ return false;
308
+ }
309
+
299
310
  if (audio && currentVariant && currentVariant.audio) {
300
311
  if (!StreamUtils.areStreamsCompatible_(audio, currentVariant.audio)) {
301
312
  shaka.log.debug('Droping variant - not compatible with active audio',
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.0.11",
4
+ "version": "3.0.15",
5
5
  "homepage": "https://github.com/google/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -27,8 +27,8 @@
27
27
  "eslint-plugin-shaka-rules": "file:./build/eslint-plugin-shaka-rules",
28
28
  "esprima": "^4.0.1",
29
29
  "fontfaceonload": "^1.0.2",
30
- "google-closure-compiler-java": "^20201006.0.0",
31
- "google-closure-library": "^20200830.0.0",
30
+ "google-closure-compiler-java": "^20210505.0.0",
31
+ "google-closure-library": "^20210406.0.0",
32
32
  "htmlhint": "^0.11.0",
33
33
  "jasmine-ajax": "^4.0.0",
34
34
  "jimp": "^0.11.0",
@@ -297,7 +297,8 @@ describe('CastProxy', () => {
297
297
  'timeupdate', Util.spyFunc(proxyListener));
298
298
 
299
299
  expect(proxyListener).not.toHaveBeenCalled();
300
- const fakeEvent = new FakeEvent('timeupdate', {detail: 8675309});
300
+ const fakeEvent = new FakeEvent(
301
+ 'timeupdate', (new Map()).set('detail', 8675309));
301
302
  mockVideo.on['timeupdate'](fakeEvent);
302
303
  expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({
303
304
  type: 'timeupdate',
@@ -315,7 +316,8 @@ describe('CastProxy', () => {
315
316
  mockSender.hasRemoteProperties.and.returnValue(true);
316
317
 
317
318
  expect(proxyListener).not.toHaveBeenCalled();
318
- const fakeEvent = new FakeEvent('timeupdate', {detail: 8675309});
319
+ const fakeEvent = new FakeEvent(
320
+ 'timeupdate', (new Map()).set('detail', 8675309));
319
321
  mockVideo.on['timeupdate'](fakeEvent);
320
322
  expect(proxyListener).not.toHaveBeenCalled();
321
323
  });
@@ -332,7 +334,8 @@ describe('CastProxy', () => {
332
334
  mockSender.hasRemoteProperties.and.returnValue(true);
333
335
 
334
336
  expect(proxyListener).not.toHaveBeenCalled();
335
- const fakeEvent = new FakeEvent('timeupdate', {detail: 8675309});
337
+ const fakeEvent = new FakeEvent(
338
+ 'timeupdate', (new Map()).set('detail', 8675309));
336
339
  mockSender.onRemoteEvent('video', fakeEvent);
337
340
  expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({
338
341
  type: 'timeupdate',
@@ -446,7 +449,8 @@ describe('CastProxy', () => {
446
449
  'buffering', Util.spyFunc(proxyListener));
447
450
 
448
451
  expect(proxyListener).not.toHaveBeenCalled();
449
- const fakeEvent = new FakeEvent('buffering', {detail: 8675309});
452
+ const fakeEvent = new FakeEvent(
453
+ 'buffering', (new Map()).set('detail', 8675309));
450
454
  mockPlayer.listeners['buffering'](fakeEvent);
451
455
  expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({
452
456
  type: 'buffering',
@@ -464,7 +468,8 @@ describe('CastProxy', () => {
464
468
  mockSender.hasRemoteProperties.and.returnValue(true);
465
469
 
466
470
  expect(proxyListener).not.toHaveBeenCalled();
467
- const fakeEvent = new FakeEvent('buffering', {detail: 8675309});
471
+ const fakeEvent = new FakeEvent(
472
+ 'buffering', (new Map()).set('detail', 8675309));
468
473
  mockPlayer.listeners['buffering'](fakeEvent);
469
474
  expect(proxyListener).not.toHaveBeenCalled();
470
475
  });
@@ -481,7 +486,8 @@ describe('CastProxy', () => {
481
486
  mockSender.hasRemoteProperties.and.returnValue(true);
482
487
 
483
488
  expect(proxyListener).not.toHaveBeenCalled();
484
- const fakeEvent = new FakeEvent('buffering', {detail: 8675309});
489
+ const fakeEvent = new FakeEvent(
490
+ 'buffering', (new Map()).set('detail', 8675309));
485
491
  mockSender.onRemoteEvent('player', fakeEvent);
486
492
  expect(proxyListener).toHaveBeenCalledWith(jasmine.objectContaining({
487
493
  type: 'buffering',
@@ -170,7 +170,7 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
170
170
  // Check that the update message is of a reasonable size. From previous
171
171
  // testing we found that the socket would silently reject data that got
172
172
  // too big. 6KB is safely below the limit.
173
- expect(message.length).toBeLessThan(6 * 1024);
173
+ expect(message.length).toBeLessThan(7 * 1024);
174
174
  });
175
175
 
176
176
  drmIt('has reasonable average message size', async () => {
@@ -129,7 +129,7 @@ describe('CastUtils', () => {
129
129
  expect(typeof deserialized).toBe('object');
130
130
 
131
131
  // The object can be used to construct a FakeEvent.
132
- const fakeEvent = new FakeEvent(deserialized['type'], deserialized);
132
+ const fakeEvent = FakeEvent.fromRealEvent(deserialized);
133
133
 
134
134
  // The fake event has the same type and properties as the original.
135
135
  const asObj = /** @type {!Object} */ (fakeEvent);
@@ -1796,4 +1796,77 @@ describe('DashParser Manifest', () => {
1796
1796
  expect(uri).not.toContain('/p2/');
1797
1797
  }
1798
1798
  });
1799
+
1800
+ /**
1801
+ * @param {!Array.<number>} periods Start time of multiple periods
1802
+ * @return {string}
1803
+ */
1804
+ function buildManifestWithPeriodStartTime(periods) {
1805
+ const mpdTemplate = [
1806
+ `<MPD type="dynamic"`,
1807
+ 'availabilityStartTime="1970-01-01T00:00:00Z"',
1808
+ 'timeShiftBufferDepth="PT10H">',
1809
+ ' %(periods)s',
1810
+ '</MPD>',
1811
+ ].join('\n');
1812
+ const periodTemplate = (id, period, duration) => {
1813
+ return [
1814
+ ` <Period id="${id}" start="PT${period}S">`,
1815
+ ' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
1816
+ ' <SegmentTemplate startNumber="1" media="l-$Number$.mp4">',
1817
+ ' <SegmentTimeline>',
1818
+ ` <S t="0" d="${duration}" />`,
1819
+ ' </SegmentTimeline>',
1820
+ ' </SegmentTemplate>',
1821
+ ' <Representation id="1"/>',
1822
+ ' </AdaptationSet>',
1823
+ ' </Period>',
1824
+ ].join('\n');
1825
+ };
1826
+ const periodXmls = periods.map((period, i) => {
1827
+ const duration = i+1 === periods.length ? 10 : periods[i+1] - period;
1828
+ // Period start time as ID here. If we use index then there will be
1829
+ // periods with same period ID and different start time which are invalid.
1830
+ return periodTemplate(period, period, duration);
1831
+ });
1832
+ return sprintf(mpdTemplate, {
1833
+ periods: periodXmls.join('\n'),
1834
+ });
1835
+ }
1836
+
1837
+ // Bug description: Inconsistent period start time in the manifests due
1838
+ // to failover triggered in backend servers
1839
+
1840
+ // When one of the servers is down, the manifest will be served by other
1841
+ // redundant servers. The period start time might become out of sync
1842
+ // during the switch-over/recovery.
1843
+
1844
+ it('skip periods that are earlier than max period start time', async () => {
1845
+ const sources = [
1846
+ buildManifestWithPeriodStartTime([5, 15]),
1847
+ buildManifestWithPeriodStartTime([6, 15]), // simulate out-of-sync of +1s
1848
+ buildManifestWithPeriodStartTime([4, 15]), // simulate out-of-sync of -1s
1849
+ ];
1850
+ const segments = [];
1851
+
1852
+ for (const source of sources) {
1853
+ fakeNetEngine.setResponseText('dummy://foo', source);
1854
+ /** @type {shaka.extern.Manifest} */
1855
+ // eslint-disable-next-line no-await-in-loop
1856
+ const manifest = await parser.start('dummy://foo', playerInterface);
1857
+ const video = manifest.variants[0].video;
1858
+ // eslint-disable-next-line no-await-in-loop
1859
+ await video.createSegmentIndex();
1860
+ goog.asserts.assert(video.segmentIndex, 'Null segmentIndex!');
1861
+ segments.push(Array.from(video.segmentIndex));
1862
+ }
1863
+
1864
+ // Expect identical segments
1865
+ expect(segments[0].length).toBe(2);
1866
+ expect(segments[1].length).toBe(2);
1867
+ expect(segments[0][0].startTime).toBe(5);
1868
+ expect(segments[1][0].startTime).toBe(5);
1869
+ expect(segments[0][1].startTime).toBe(15);
1870
+ expect(segments[1][1].startTime).toBe(15);
1871
+ });
1799
1872
  });
@@ -96,7 +96,6 @@ describe('Demo', () => {
96
96
  const configPrimitives = new Set(['number', 'string', 'boolean']);
97
97
  const exceptions = new Set()
98
98
  .add('preferredVariantRole')
99
- .add('preferredTextRole')
100
99
  .add('playRangeStart')
101
100
  .add('playRangeEnd');
102
101
 
@@ -202,6 +202,46 @@ describe('HlsParser', () => {
202
202
  expect(actual).toEqual(manifest);
203
203
  });
204
204
 
205
+ it('prioritize AVERAGE-BANDWIDTH to BANDWIDTH', async () => {
206
+ const master = [
207
+ '#EXTM3U\n',
208
+ '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
209
+ 'RESOLUTION=960x540,FRAME-RATE=60,',
210
+ 'AVERAGE-BANDWIDTH=100\n',
211
+ 'video\n',
212
+ ].join('');
213
+
214
+ const media = [
215
+ '#EXTM3U\n',
216
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
217
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
218
+ '#EXTINF:5,\n',
219
+ '#EXT-X-BYTERANGE:121090@616\n',
220
+ 'main.mp4',
221
+ ].join('');
222
+
223
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
224
+ manifest.anyTimeline();
225
+ manifest.addPartialVariant((variant) => {
226
+ variant.bandwidth = 100;
227
+ variant.addPartialStream(ContentType.VIDEO, (stream) => {
228
+ stream.frameRate = 60;
229
+ stream.mime('video/mp4', 'avc1');
230
+ stream.size(960, 540);
231
+ });
232
+ });
233
+ });
234
+
235
+ fakeNetEngine
236
+ .setResponseText('test:/master', master)
237
+ .setResponseText('test:/video', media)
238
+ .setResponseValue('test:/init.mp4', initSegmentData)
239
+ .setResponseValue('test:/main.mp4', segmentData);
240
+
241
+ const actual = await parser.start('test:/master', playerInterface);
242
+ expect(actual).toEqual(manifest);
243
+ });
244
+
205
245
  it('ignores duplicate CODECS', async () => {
206
246
  const master = [
207
247
  '#EXTM3U\n',
@@ -736,7 +776,7 @@ describe('HlsParser', () => {
736
776
  'RESOLUTION=960x540,FRAME-RATE=120,AUDIO="aud2"\n',
737
777
  'video2\n',
738
778
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
739
- 'URI="audio"\n',
779
+ 'DEFAULT=YES,URI="audio"\n',
740
780
  '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",LANGUAGE="fr",',
741
781
  'URI="audio2"\n',
742
782
  ].join('');
@@ -754,6 +794,7 @@ describe('HlsParser', () => {
754
794
  manifest.anyTimeline();
755
795
  manifest.addPartialVariant((variant) => {
756
796
  variant.bandwidth = 200;
797
+ variant.primary = true;
757
798
  variant.addPartialStream(ContentType.VIDEO, (stream) => {
758
799
  stream.size(960, 540);
759
800
  });
@@ -763,6 +804,7 @@ describe('HlsParser', () => {
763
804
  });
764
805
  manifest.addPartialVariant((variant) => {
765
806
  variant.bandwidth = 300;
807
+ variant.primary = false;
766
808
  variant.addPartialStream(ContentType.VIDEO, (stream) => {
767
809
  stream.size(960, 540);
768
810
  });
@@ -1171,6 +1213,8 @@ describe('HlsParser', () => {
1171
1213
  // stream.
1172
1214
  const timeline = actual.presentationTimeline;
1173
1215
  expect(timeline.getDuration()).toBe(10);
1216
+ expect(timeline.getSeekRangeStart()).toBe(0);
1217
+ expect(timeline.getSeekRangeEnd()).toBe(10);
1174
1218
 
1175
1219
  expect(actual.textStreams.length).toBe(1);
1176
1220
  expect(actual.variants.length).toBe(1);
@@ -1954,6 +1954,49 @@ describe('DrmEngine', () => {
1954
1954
  [drmInfoAudio]);
1955
1955
  expect(returned).toEqual([drmInfoDesired]);
1956
1956
  });
1957
+
1958
+ it('dedupes the merged init data based on keyId matching', () => {
1959
+ const serverCert = new Uint8Array(0);
1960
+ const drmInfoVideo = {
1961
+ keySystem: 'drm.abc',
1962
+ licenseServerUri: 'http://abc.drm/license',
1963
+ distinctiveIdentifierRequired: false,
1964
+ persistentStateRequired: true,
1965
+ videoRobustness: 'really_really_ridiculously_good',
1966
+ serverCertificate: serverCert,
1967
+ serverCertificateUri: '',
1968
+ initData: [{keyId: 'v-init'}],
1969
+ keyIds: new Set(['deadbeefdeadbeefdeadbeefdeadbeef']),
1970
+ };
1971
+ const drmInfoAudio = {
1972
+ keySystem: 'drm.abc',
1973
+ licenseServerUri: undefined,
1974
+ distinctiveIdentifierRequired: true,
1975
+ persistentStateRequired: false,
1976
+ audioRobustness: 'good',
1977
+ serverCertificate: undefined,
1978
+ serverCertificateUri: '',
1979
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
1980
+ keyIds: new Set(['eadbeefdeadbeefdeadbeefdeadbeefd']),
1981
+ };
1982
+ const drmInfoDesired = {
1983
+ keySystem: 'drm.abc',
1984
+ licenseServerUri: 'http://abc.drm/license',
1985
+ distinctiveIdentifierRequired: true,
1986
+ persistentStateRequired: true,
1987
+ audioRobustness: 'good',
1988
+ videoRobustness: 'really_really_ridiculously_good',
1989
+ serverCertificate: serverCert,
1990
+ initData: [{keyId: 'v-init'}, {keyId: 'a-init'}],
1991
+ keyIds: new Set([
1992
+ 'deadbeefdeadbeefdeadbeefdeadbeef',
1993
+ 'eadbeefdeadbeefdeadbeefdeadbeefd',
1994
+ ]),
1995
+ };
1996
+ const returned = shaka.media.DrmEngine.getCommonDrmInfos([drmInfoVideo],
1997
+ [drmInfoAudio]);
1998
+ expect(returned).toEqual([drmInfoDesired]);
1999
+ });
1957
2000
  }); // describe('getCommonDrmInfos')
1958
2001
 
1959
2002
  describe('configure', () => {
@@ -2087,6 +2130,19 @@ describe('DrmEngine', () => {
2087
2130
  await expectAsync(drmEngine.removeSession('abc'))
2088
2131
  .toBeRejectedWith(expected);
2089
2132
  });
2133
+
2134
+ // Regression test for #3534
2135
+ it('does not remove the same session again on destroy', async () => {
2136
+ updatePromise.resolve();
2137
+ expect(session1.remove).not.toHaveBeenCalled();
2138
+ await drmEngine.removeSession('abc');
2139
+ expect(session1.remove).toHaveBeenCalled();
2140
+ session1.remove.calls.reset();
2141
+ await drmEngine.destroy();
2142
+ // The session should only be removed ONCE. If it's double-removed, it
2143
+ // will make a (non-fatal) DOMException.
2144
+ expect(session1.remove).not.toHaveBeenCalled();
2145
+ });
2090
2146
  });
2091
2147
 
2092
2148
  describe('expiration', () => {