shaka-player 2.5.1 → 2.5.5

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 (166) hide show
  1. package/.csslintrc +13 -0
  2. package/AUTHORS +3 -0
  3. package/CHANGELOG.md +156 -0
  4. package/CONTRIBUTORS +3 -0
  5. package/README.md +3 -0
  6. package/build/README.md +5 -5
  7. package/build/apps.py +3 -1
  8. package/build/build.py +5 -2
  9. package/build/check.py +17 -0
  10. package/build/compiler.py +49 -0
  11. package/build/conformance.textproto +1 -0
  12. package/build/docs.py +0 -6
  13. package/build/generateExterns.js +72 -20
  14. package/build/install-linux-prereqs.sh +4 -4
  15. package/build/shakaBuildHelpers.py +7 -8
  16. package/build/test.py +21 -9
  17. package/build/types/core +1 -0
  18. package/build/types/ui +6 -1
  19. package/demo/asset_card.js +129 -41
  20. package/demo/cast_receiver/receiver_app.css +27 -13
  21. package/demo/close_button.js +10 -15
  22. package/demo/common/asset.js +6 -6
  23. package/demo/common/assets.js +72 -14
  24. package/demo/config.js +28 -25
  25. package/demo/custom.js +20 -16
  26. package/demo/demo.less +107 -16
  27. package/demo/demo_uncompiled.js +33 -0
  28. package/demo/demo_utils.js +4 -3
  29. package/demo/front.js +12 -8
  30. package/demo/index.html +14 -17
  31. package/demo/input.js +30 -23
  32. package/demo/input_container.js +15 -11
  33. package/demo/load.js +5 -7
  34. package/demo/main.js +138 -49
  35. package/demo/search.js +40 -36
  36. package/demo/service_worker.js +1 -1
  37. package/demo/tooltip.js +16 -24
  38. package/dist/.csslintstamp +0 -0
  39. package/dist/controls.css +1 -1
  40. package/dist/controls.css.map +1 -1
  41. package/dist/demo.compiled.debug.js +165 -152
  42. package/dist/demo.compiled.debug.map +4 -4
  43. package/dist/demo.compiled.js +165 -152
  44. package/dist/demo.compiled.map +4 -4
  45. package/dist/demo.css +1 -1
  46. package/dist/demo.css.map +1 -1
  47. package/dist/deps.js +28 -11
  48. package/dist/receiver.compiled.debug.js +11 -11
  49. package/dist/receiver.compiled.debug.map +3 -3
  50. package/dist/receiver.compiled.js +11 -11
  51. package/dist/receiver.compiled.map +3 -3
  52. package/dist/shaka-player.compiled.debug.externs.js +232 -102
  53. package/dist/shaka-player.compiled.debug.js +189 -165
  54. package/dist/shaka-player.compiled.debug.map +4 -4
  55. package/dist/shaka-player.compiled.externs.js +232 -102
  56. package/dist/shaka-player.compiled.js +567 -553
  57. package/dist/shaka-player.compiled.map +4 -4
  58. package/dist/shaka-player.ui.debug.externs.js +338 -113
  59. package/dist/shaka-player.ui.debug.js +296 -276
  60. package/dist/shaka-player.ui.debug.map +4 -4
  61. package/dist/shaka-player.ui.externs.js +338 -113
  62. package/dist/shaka-player.ui.js +685 -671
  63. package/dist/shaka-player.ui.map +4 -4
  64. package/docs/tutorials/drm-config.md +1 -0
  65. package/docs/tutorials/fairplay.md +77 -0
  66. package/docs/tutorials/index.json +1 -0
  67. package/docs/tutorials/manifest-parser.md +1 -1
  68. package/docs/tutorials/ui-customization.md +5 -2
  69. package/docs/tutorials/ui.md +10 -4
  70. package/docs/tutorials/welcome.md +2 -2
  71. package/externs/mediasession.js +16 -1
  72. package/externs/shaka/net.js +14 -4
  73. package/externs/shaka/player.js +136 -90
  74. package/externs/tippy.js +30 -0
  75. package/externs/webkitmediakeys.js +4 -0
  76. package/karma.conf.js +30 -24
  77. package/lib/cast/cast_proxy.js +94 -23
  78. package/lib/cast/cast_receiver.js +8 -1
  79. package/lib/dash/content_protection.js +33 -1
  80. package/lib/dash/dash_parser.js +13 -5
  81. package/lib/hls/hls_parser.js +20 -4
  82. package/lib/media/drm_engine.js +29 -2
  83. package/lib/media/media_source_engine.js +9 -6
  84. package/lib/media/play_rate_controller.js +14 -3
  85. package/lib/media/playhead.js +8 -3
  86. package/lib/media/streaming_engine.js +68 -20
  87. package/lib/media/transmuxer.js +2 -2
  88. package/lib/media/video_wrapper.js +8 -3
  89. package/lib/net/data_uri_plugin.js +5 -2
  90. package/lib/net/http_fetch_plugin.js +3 -3
  91. package/lib/net/http_plugin_utils.js +2 -4
  92. package/lib/net/http_xhr_plugin.js +2 -2
  93. package/lib/net/networking_engine.js +1 -0
  94. package/lib/offline/offline_scheme.js +7 -2
  95. package/lib/player.js +18 -8
  96. package/lib/polyfill/input_event.js +7 -11
  97. package/lib/polyfill/mediasource.js +1 -1
  98. package/lib/polyfill/patchedmediakeys_apple.js +2 -72
  99. package/lib/text/cue.js +1 -1
  100. package/lib/text/ttml_text_parser.js +1 -1
  101. package/lib/util/config_utils.js +1 -0
  102. package/lib/util/error.js +45 -11
  103. package/lib/util/event_manager.js +211 -100
  104. package/lib/util/fairplay_utils.js +122 -0
  105. package/lib/util/i_releasable.js +1 -0
  106. package/lib/util/player_configuration.js +7 -0
  107. package/lib/util/pssh.js +41 -0
  108. package/lib/util/string_utils.js +42 -6
  109. package/lib/util/timer.js +1 -1
  110. package/package.json +33 -33
  111. package/roadmap.md +96 -0
  112. package/shaka-player.uncompiled.js +5 -1
  113. package/test/dash/dash_parser_content_protection_unit.js +35 -1
  114. package/test/dash/dash_parser_manifest_unit.js +60 -0
  115. package/test/hls/hls_live_unit.js +7 -1
  116. package/test/hls/hls_parser_unit.js +50 -0
  117. package/test/media/drm_engine_unit.js +10 -4
  118. package/test/media/media_source_engine_integration.js +12 -0
  119. package/test/media/media_source_engine_unit.js +3 -1
  120. package/test/media/playhead_unit.js +7 -5
  121. package/test/media/streaming_engine_unit.js +46 -1
  122. package/test/media/transmuxer_integration.js +13 -0
  123. package/test/net/data_uri_plugin_unit.js +13 -2
  124. package/test/net/http_plugin_unit.js +17 -12
  125. package/test/net/networking_engine_unit.js +17 -8
  126. package/test/offline/offline_scheme_unit.js +18 -8
  127. package/test/player_external.js +3 -2
  128. package/test/player_integration.js +94 -3
  129. package/test/player_src_equals_integration.js +2 -2
  130. package/test/player_unit.js +21 -8
  131. package/test/test/boot.js +20 -5
  132. package/test/test/externs/jasmine.js +48 -0
  133. package/test/test/util/expect_async_polyfill.js +145 -0
  134. package/test/test/util/fake_networking_engine.js +1 -0
  135. package/test/test/util/manifest_generator.js +2 -1
  136. package/test/test/util/test_scheme.js +34 -2
  137. package/test/test/util/ui_utils.js +41 -0
  138. package/test/test/util/util.js +16 -8
  139. package/test/ui/ui_unit.js +130 -4
  140. package/ui/audio_language_selection.js +17 -96
  141. package/ui/big_play_button.js +61 -0
  142. package/ui/controls.js +226 -335
  143. package/ui/controls.less +3 -5
  144. package/ui/element.js +24 -6
  145. package/ui/externs/ui.js +62 -9
  146. package/ui/fullscreen_button.js +2 -52
  147. package/ui/less/buttons.less +10 -16
  148. package/ui/less/containers.less +75 -20
  149. package/ui/less/general.less +18 -2
  150. package/ui/less/other_elements.less +2 -1
  151. package/ui/less/overflow_menu.less +11 -10
  152. package/ui/less/range_elements.less +12 -10
  153. package/ui/less/spinner.less +2 -0
  154. package/ui/overflow_menu.js +1 -1
  155. package/ui/pip_button.js +1 -1
  156. package/ui/{play_pause_button.js → play_button.js} +17 -47
  157. package/ui/presentation_time.js +17 -12
  158. package/ui/range_element.js +176 -0
  159. package/ui/resolution_selection.js +43 -106
  160. package/ui/seek_bar.js +204 -0
  161. package/ui/settings_menu.js +113 -0
  162. package/ui/small_play_button.js +73 -0
  163. package/ui/text_displayer.js +17 -14
  164. package/ui/text_selection.js +42 -107
  165. package/ui/ui.js +17 -40
  166. package/ui/volume_bar.js +40 -68
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2016 Google Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+
19
+ /**
20
+ * @fileoverview This polyfills the expectAsync method from Jasmine 3.
21
+ */
22
+
23
+
24
+ // TODO: Remove once we upgrade to Jasmine 3.0+.
25
+ if (window.expectAsync === undefined) {
26
+ /** @extends {jasmine.MatchersAsync} */
27
+ const AsyncMatchers = class {
28
+ /**
29
+ * @param {!Promise} val
30
+ * @param {string=} context
31
+ * @param {boolean=} negated
32
+ */
33
+ constructor(val, context = '', negated = false) {
34
+ this.val_ = val;
35
+ this.negated_ = negated;
36
+ this.context_ = context ? '"' + context + '" ' : '';
37
+
38
+ // Don't use a normal property to avoid infinite recursion.
39
+ Object.defineProperty(this, 'not', {
40
+ get: () => new AsyncMatchers(val, context, !negated),
41
+ });
42
+ }
43
+
44
+ /** @override */
45
+ withContext(ctx) {
46
+ return new AsyncMatchers(this.val_, ctx, this.negated_);
47
+ }
48
+
49
+ /** @override */
50
+ async toBeResolved() {
51
+ await this.toBeResolvedShared(/* has_value= */ false);
52
+ }
53
+
54
+ /** @override */
55
+ async toBeResolvedTo(expected) {
56
+ await this.toBeResolvedShared(/* has_value= */ true, expected);
57
+ }
58
+
59
+ /**
60
+ * @param {boolean} hasValue
61
+ * @param {*=} expected
62
+ * @return {!Promise}
63
+ */
64
+ async toBeResolvedShared(hasValue, expected = undefined) {
65
+ let msg = 'Expected ' + this.context_;
66
+ if (this.negated_) {
67
+ msg += ' not';
68
+ }
69
+ msg += ' to be resolved';
70
+ if (hasValue) {
71
+ msg += ' to ' + expected;
72
+ }
73
+
74
+ try {
75
+ const actual = await this.val_;
76
+ if (this.negated_) {
77
+ if (hasValue) {
78
+ expect(actual).not.toEqual(expected);
79
+ } else {
80
+ fail(msg);
81
+ }
82
+ } else if (hasValue) {
83
+ expect(actual).toEqual(expected);
84
+ }
85
+ } catch (e) {
86
+ if (!this.negated_) {
87
+ fail(msg);
88
+ }
89
+ }
90
+ }
91
+
92
+ /** @override */
93
+ async toBeRejected() {
94
+ await this.toBeRejectedShared(/* has_value= */ false);
95
+ }
96
+
97
+ /** @override */
98
+ async toBeRejectedWith(expected) {
99
+ await this.toBeRejectedShared(/* has_value= */ true, expected);
100
+ }
101
+
102
+ /**
103
+ * @param {boolean} hasValue
104
+ * @param {*=} expected
105
+ * @return {!Promise}
106
+ */
107
+ async toBeRejectedShared(hasValue, expected = undefined) {
108
+ let msg = 'Expected ' + this.context_;
109
+ if (this.negated_) {
110
+ msg += ' not';
111
+ }
112
+ msg += ' to be rejected';
113
+ if (hasValue) {
114
+ msg += ' with ' + expected;
115
+ }
116
+
117
+ try {
118
+ await this.val_;
119
+ if (!this.negated_) {
120
+ fail(msg);
121
+ }
122
+ } catch (actual) {
123
+ if (this.negated_) {
124
+ if (hasValue) {
125
+ expect(actual).not.toEqual(expected);
126
+ } else {
127
+ fail(msg);
128
+ }
129
+ } else if (hasValue) {
130
+ expect(actual).toEqual(expected);
131
+ }
132
+ }
133
+ }
134
+ };
135
+
136
+ /**
137
+ * @param {*} obj
138
+ * @return {!AsyncMatchers}
139
+ */
140
+ window.expectAsync = (obj) => {
141
+ goog.asserts.assert(obj instanceof Promise, 'Must pass promise');
142
+ expect(obj instanceof Promise).toBe(true);
143
+ return new AsyncMatchers(obj);
144
+ };
145
+ }
@@ -120,6 +120,7 @@ shaka.test.FakeNetworkingEngine = class {
120
120
  /** @type {shaka.extern.Response} */
121
121
  const response = {
122
122
  uri: requestedUri,
123
+ originalUri: requestedUri,
123
124
  data: result,
124
125
  headers: headers,
125
126
  };
@@ -584,7 +584,8 @@ shaka.test.ManifestGenerator.prototype.useSegmentTemplate = function(
584
584
  return Math.floor(time / segmentDuration);
585
585
  };
586
586
  stream.getSegmentReference = (function(index) {
587
- if (index < 0 || index >= segmentCount) {
587
+ goog.asserts.assert(!isNaN(index), 'Invalid index requested!');
588
+ if (index < 0 || index >= segmentCount || isNaN(index)) {
588
589
  return null;
589
590
  }
590
591
  let getUris = function() { return [sprintf(template, index)]; };
@@ -33,6 +33,7 @@ shaka.test.TestScheme = function(uri, request, requestType) {
33
33
  /** @type {shaka.extern.Response} */
34
34
  let response = {
35
35
  uri: uri,
36
+ originalUri: uri,
36
37
  data: new ArrayBuffer(0),
37
38
  headers: {'content-type': 'application/x-test-manifest'},
38
39
  };
@@ -76,7 +77,7 @@ shaka.test.TestScheme = function(uri, request, requestType) {
76
77
  }
77
78
 
78
79
  /** @type {shaka.extern.Response} */
79
- let ret = {uri: uri, data: responseData, headers: {}};
80
+ const ret = {uri: uri, originalUri: uri, data: responseData, headers: {}};
80
81
  return shaka.util.AbortableOperation.completed(ret);
81
82
  };
82
83
 
@@ -125,6 +126,37 @@ shaka.test.TestScheme.DATA = {
125
126
  duration: 30,
126
127
  },
127
128
 
129
+ // Like 'sintel' above, but with a non-zero period start time.
130
+ // This helps expose edge cases around startup and live streams.
131
+ 'sintel_start_at_3': {
132
+ periodStart: 3,
133
+ video: {
134
+ initSegmentUri: '/base/test/test/assets/sintel-video-init.mp4',
135
+ mdhdOffset: 0x1ba,
136
+ segmentUri: '/base/test/test/assets/sintel-video-segment.mp4',
137
+ tfdtOffset: 0x38,
138
+ segmentDuration: 10,
139
+ presentationTimeOffset: 0,
140
+ mimeType: 'video/mp4',
141
+ codecs: 'avc1.42c01e',
142
+ },
143
+ audio: {
144
+ initSegmentUri: '/base/test/test/assets/sintel-audio-init.mp4',
145
+ mdhdOffset: 0x1b6,
146
+ segmentUri: '/base/test/test/assets/sintel-audio-segment.mp4',
147
+ tfdtOffset: 0x3c,
148
+ segmentDuration: 10.005,
149
+ presentationTimeOffset: 0,
150
+ mimeType: 'audio/mp4',
151
+ codecs: 'mp4a.40.2',
152
+ },
153
+ text: {
154
+ uri: '/base/test/test/assets/text-clip.vtt',
155
+ mimeType: 'text/vtt',
156
+ },
157
+ duration: 30,
158
+ },
159
+
128
160
  // Like 'sintel' above, but with languages and delayed setup.
129
161
  // These extra features help expose some edge cases.
130
162
  'sintel_realistic': {
@@ -476,7 +508,7 @@ shaka.test.TestScheme.createManifests = function(shaka, suffix) {
476
508
 
477
509
  let gen = new windowShaka.test.ManifestGenerator(shaka)
478
510
  .setPresentationDuration(data.duration)
479
- .addPeriod(/* startTime= */ 0)
511
+ .addPeriod(/* startTime= */ data.periodStart || 0)
480
512
  .addVariant(0);
481
513
 
482
514
  if (data[ContentType.VIDEO]) {
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2016 Google Inc.
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+
18
+
19
+ goog.provide('shaka.test.UiUtils');
20
+
21
+
22
+ shaka.test.UiUtils = class {
23
+ /**
24
+ * Simulates a native event (e.g. 'click') on the given element.
25
+ *
26
+ * @param {EventTarget} target
27
+ * @param {string} name
28
+ */
29
+ static simulateEvent(target, name) {
30
+ const type = {
31
+ 'click': 'MouseEvent',
32
+ 'dblclick': 'MouseEvent',
33
+ }[name] || 'CustomEvent';
34
+
35
+ // Note we can't use the MouseEvent constructor since it isn't supported on
36
+ // IE11.
37
+ const event = document.createEvent(type);
38
+ event.initEvent(name, true, true);
39
+ target.dispatchEvent(event);
40
+ }
41
+ };
@@ -91,18 +91,26 @@ shaka.test.Util.delay = function(seconds, realSetTimeout) {
91
91
  };
92
92
 
93
93
 
94
+ /**
95
+ * @param {!shaka.util.Error} error
96
+ * @return {*}
97
+ */
98
+ shaka.test.Util.jasmineError = function(error) {
99
+ // NOTE: Safari will add extra properties to any thrown object, and some of
100
+ // the properties we compute in debug builds are unhelpful and introduce
101
+ // inconsistency in tests. Therefore we only capture the critical fields
102
+ // below.
103
+ const {severity, category, code, data} = error;
104
+ return jasmine.objectContaining({severity, category, code, data});
105
+ };
106
+
107
+
94
108
  /**
95
109
  * @param {*} actual
96
- * @param {!Object} expected
110
+ * @param {!shaka.util.Error} expected
97
111
  */
98
112
  shaka.test.Util.expectToEqualError = function(actual, expected) {
99
- // NOTE: Safari will add extra properties to any thrown object, so we
100
- // wrap expectedError in jasmine.objectContaining to ignore them.
101
- // NOTE: We now add extra properties ourselves for the sake of formatting.
102
- // These, we delete from 'expected'.
103
- delete expected['stack'];
104
- delete expected['message'];
105
- expect(actual).toEqual(jasmine.objectContaining(expected));
113
+ expect(actual).toEqual(shaka.test.Util.jasmineError(expected));
106
114
  };
107
115
 
108
116
 
@@ -181,6 +181,27 @@ describe('UI', function() {
181
181
  videoContainer.appendChild(video);
182
182
  });
183
183
 
184
+ it('goes into fullscreen on double click', async () => {
185
+ const config = {
186
+ controlPanelElements: [
187
+ 'overflow_menu',
188
+ ],
189
+ overflowMenuButtons: [
190
+ 'quality',
191
+ ],
192
+ };
193
+ const ui = createUIThroughAPI(videoContainer, video, config);
194
+ const controls = ui.getControls();
195
+
196
+ const spy = spyOn(controls, 'toggleFullScreen');
197
+
198
+ const controlsContainer =
199
+ videoContainer.querySelector('.shaka-controls-container');
200
+ shaka.test.UiUtils.simulateEvent(controlsContainer, 'dblclick');
201
+ await shaka.test.Util.delay(0.1);
202
+ expect(spy).toHaveBeenCalledTimes(1);
203
+ });
204
+
184
205
  describe('all the controls', function() {
185
206
  /** @type {!HTMLElement} */
186
207
  let controlsContainer;
@@ -344,6 +365,8 @@ describe('UI', function() {
344
365
  describe('resolutions menu', function() {
345
366
  /** @type {!HTMLElement} */
346
367
  let resolutionsMenu;
368
+ /** @type {shaka.ui.Controls} */
369
+ let controls;
347
370
 
348
371
  beforeEach(function() {
349
372
  let config = {
@@ -354,7 +377,9 @@ describe('UI', function() {
354
377
  'quality',
355
378
  ],
356
379
  };
357
- createUIThroughAPI(videoContainer, video, config);
380
+ const ui = createUIThroughAPI(videoContainer, video, config);
381
+ controls = ui.getControls();
382
+ player = controls.getLocalPlayer();
358
383
 
359
384
  let resolutionsMenus =
360
385
  videoContainer.getElementsByClassName('shaka-resolutions');
@@ -406,8 +431,107 @@ describe('UI', function() {
406
431
  jasmine.any(Object), true);
407
432
  });
408
433
 
409
- // TODO: integration test to ensure all resolutions are
410
- // correctly represented
434
+ it('displays resolutions based on current stream', async () => {
435
+ /* eslint-disable indent */
436
+ // A manifest with different resolutions at different
437
+ // languages/channel-counts to test the current resolution list is
438
+ // filtered.
439
+ const manifest = new shaka.test.ManifestGenerator()
440
+ .addPeriod(0)
441
+ .addVariant(0)
442
+ .primary()
443
+ .language('en')
444
+ .addVideo(1).size(320, 240)
445
+ .addAudio(3).channelsCount(2)
446
+ .addVariant(4)
447
+ .language('en')
448
+ .addVideo(5).size(640, 480)
449
+ .addAudio(6).channelsCount(2)
450
+ .addVariant(7) // Duplicate with 4
451
+ .language('en')
452
+ .addVideo(8).size(640, 480)
453
+ .addAudio(9).channelsCount(2)
454
+ .addVariant(10)
455
+ .language('en')
456
+ .addVideo(11).size(1280, 720)
457
+ .addAudio(12).channelsCount(1)
458
+ .addVariant(13)
459
+ .language('es')
460
+ .addVideo(14).size(960, 540)
461
+ .addAudio(15).channelsCount(2)
462
+ .addVariant(16)
463
+ .language('fr')
464
+ .addVideo(17).size(256, 144)
465
+ .addAudio(18).channelsCount(2)
466
+ .build();
467
+ /* eslint-enable indent */
468
+ const getResolutions = () => {
469
+ const resolutionButtons = videoContainer.querySelectorAll(
470
+ 'button.explicit-resolution > span');
471
+ return Array.from(resolutionButtons)
472
+ .map((btn) => btn.innerText)
473
+ .sort();
474
+ };
475
+
476
+ await player.load(
477
+ /* uri= */ 'fake', /* startTime= */ 0, function() {
478
+ return new shaka.test.FakeManifestParser(manifest);
479
+ });
480
+ player.configure('abr.enabled', false);
481
+
482
+ const tracks = player.getVariantTracks();
483
+ const en2 =
484
+ tracks.find((t) => t.language == 'en' && t.channelsCount == 2);
485
+ const en1 =
486
+ tracks.find((t) => t.language == 'en' && t.channelsCount == 1);
487
+ const es = tracks.find((t) => t.language == 'es');
488
+
489
+ // There are 3 variants with English 2-channel, but one is a duplicate
490
+ // and shouldn't appear in the list.
491
+ goog.asserts.assert(en2, 'Unable to find tracks');
492
+ player.selectVariantTrack(en2, true);
493
+ await updateResolutionMenu();
494
+ expect(getResolutions()).toEqual(['240p', '480p']);
495
+
496
+ // There is 1 variant with English 1-channel.
497
+ goog.asserts.assert(en1, 'Unable to find tracks');
498
+ player.selectVariantTrack(en1, true);
499
+ await updateResolutionMenu();
500
+ expect(getResolutions()).toEqual(['720p']);
501
+
502
+ // There is 1 variant with Spanish 2-channel.
503
+ goog.asserts.assert(es, 'Unable to find tracks');
504
+ player.selectVariantTrack(es, true);
505
+ await updateResolutionMenu();
506
+ expect(getResolutions()).toEqual(['540p']);
507
+ });
508
+
509
+ /**
510
+ * Use internals to update the resolution menu. Our fake manifest can
511
+ * cause problems with startup where the Player will get stuck using
512
+ * "deferred" switches, so we won't get events and the resolution menu
513
+ * won't update.
514
+ *
515
+ * @suppress {accessControls}
516
+ */
517
+ async function updateResolutionMenu() {
518
+ await shaka.test.Util.delay(0.1);
519
+ // TODO(#2089): We should be able to stop once we find one, but since
520
+ // there are multiple ResolutionMenu objects, we need to update all of
521
+ // them.
522
+ let found = false;
523
+ for (const elem of controls.elements_) {
524
+ if (elem instanceof shaka.ui.OverflowMenu) {
525
+ for (const child of elem.children_) {
526
+ if (child instanceof shaka.ui.ResolutionSelection) {
527
+ child.updateResolutionSelection_();
528
+ found = true;
529
+ }
530
+ }
531
+ }
532
+ }
533
+ goog.asserts.assert(found, 'Unable to find resolution menu');
534
+ }
411
535
  });
412
536
 
413
537
  // TODO: integration test to test audio language menu.
@@ -531,7 +655,7 @@ describe('UI', function() {
531
655
 
532
656
  confirmElementFound(container, 'shaka-play-button-container');
533
657
  confirmElementFound(container, 'shaka-play-button');
534
- confirmElementFound(container, 'shaka-spinner-svg');
658
+ confirmElementFound(container, 'shaka-spinner');
535
659
  confirmElementFound(container, 'shaka-overflow-menu');
536
660
  confirmElementFound(container, 'shaka-controls-button-panel');
537
661
  confirmElementFound(container, 'shaka-seek-bar');
@@ -541,6 +665,7 @@ describe('UI', function() {
541
665
  * @param {!HTMLElement} videoContainer
542
666
  * @param {!HTMLMediaElement} video
543
667
  * @param {!Object=} config
668
+ * @return {!shaka.ui.Overlay}
544
669
  */
545
670
  function createUIThroughAPI(videoContainer, video, config) {
546
671
  player = new shaka.Player(video);
@@ -548,6 +673,7 @@ describe('UI', function() {
548
673
  config = config || {};
549
674
  const ui = new shaka.ui.Overlay(player, videoContainer, video);
550
675
  ui.configure(config);
676
+ return ui;
551
677
  }
552
678
 
553
679
  /**
@@ -18,31 +18,28 @@
18
18
 
19
19
  goog.provide('shaka.ui.AudioLanguageSelection');
20
20
 
21
- goog.require('shaka.ui.Element');
22
21
  goog.require('shaka.ui.Enums');
23
22
  goog.require('shaka.ui.LanguageUtils');
24
23
  goog.require('shaka.ui.Locales');
25
24
  goog.require('shaka.ui.Localization');
26
25
  goog.require('shaka.ui.OverflowMenu');
27
- goog.require('shaka.util.Dom');
28
-
26
+ goog.require('shaka.ui.SettingsMenu');
29
27
 
30
28
  /**
31
- * @extends {shaka.ui.Element}
29
+ * @extends {shaka.ui.SettingsMenu}
32
30
  * @final
33
31
  * @export
34
32
  */
35
- shaka.ui.AudioLanguageSelection = class extends shaka.ui.Element {
33
+ shaka.ui.AudioLanguageSelection = class extends shaka.ui.SettingsMenu {
36
34
  /**
37
35
  * @param {!HTMLElement} parent
38
36
  * @param {!shaka.ui.Controls} controls
39
37
  */
40
38
  constructor(parent, controls) {
41
- super(parent, controls);
42
-
43
- this.addLanguagesButton_();
39
+ super(parent, controls, shaka.ui.Enums.MaterialDesignIcons.LANGUAGE);
44
40
 
45
- this.addAudioLangMenu_();
41
+ this.button.classList.add('shaka-language-button');
42
+ this.menu.classList.add('shaka-audio-languages');
46
43
 
47
44
  this.eventManager.listen(
48
45
  this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
@@ -63,10 +60,6 @@ shaka.ui.AudioLanguageSelection = class extends shaka.ui.Element {
63
60
  this.updateAudioLanguages_();
64
61
  });
65
62
 
66
- this.eventManager.listen(this.languagesButton_, 'click', () => {
67
- this.onLanguagesClick_();
68
- });
69
-
70
63
  // Set up all the strings in the user's preferred language.
71
64
  this.updateLocalizedStrings_();
72
65
 
@@ -74,89 +67,17 @@ shaka.ui.AudioLanguageSelection = class extends shaka.ui.Element {
74
67
  }
75
68
 
76
69
 
77
- /**
78
- * @private
79
- */
80
- addAudioLangMenu_() {
81
- /** @private {!HTMLElement} */
82
- this.audioLangMenu_ = shaka.util.Dom.createHTMLElement('div');
83
- this.audioLangMenu_.classList.add('shaka-audio-languages');
84
- this.audioLangMenu_.classList.add('shaka-no-propagation');
85
- this.audioLangMenu_.classList.add('shaka-show-controls-on-mouse-over');
86
- this.audioLangMenu_.classList.add('shaka-settings-menu');
87
-
88
- /** @private {!HTMLElement} */
89
- this.backFromLanguageButton_ = shaka.util.Dom.createHTMLElement('button');
90
- this.backFromLanguageButton_.classList.add('shaka-back-to-overflow-button');
91
- this.audioLangMenu_.appendChild(this.backFromLanguageButton_);
92
-
93
- const backIcon = shaka.util.Dom.createHTMLElement('i');
94
- backIcon.classList.add('material-icons');
95
- backIcon.textContent = shaka.ui.Enums.MaterialDesignIcons.BACK;
96
- this.backFromLanguageButton_.appendChild(backIcon);
97
-
98
- /** @private {!HTMLElement} */
99
- this.backFromLanguageSpan_ = shaka.util.Dom.createHTMLElement('span');
100
- this.backFromLanguageButton_.appendChild(this.backFromLanguageSpan_);
101
-
102
- const controlsContainer = this.controls.getControlsContainer();
103
- controlsContainer.appendChild(this.audioLangMenu_);
104
- }
105
-
106
-
107
- /**
108
- * @private
109
- */
110
- addLanguagesButton_() {
111
- /** @private {!HTMLElement} */
112
- this.languagesButton_ = shaka.util.Dom.createHTMLElement('button');
113
- this.languagesButton_.classList.add('shaka-language-button');
114
-
115
- const icon = shaka.util.Dom.createHTMLElement('i');
116
- icon.classList.add('material-icons');
117
- icon.textContent = shaka.ui.Enums.MaterialDesignIcons.LANGUAGE;
118
- this.languagesButton_.appendChild(icon);
119
-
120
- const label = shaka.util.Dom.createHTMLElement('label');
121
- label.classList.add('shaka-overflow-button-label');
122
-
123
- /** @private {!HTMLElement} */
124
- this.languageNameSpan_ = shaka.util.Dom.createHTMLElement('span');
125
- this.languageNameSpan_.classList.add('languageSpan');
126
- label.appendChild(this.languageNameSpan_);
127
-
128
- /** @private {!HTMLElement} */
129
- this.currentAudioLanguage_ = shaka.util.Dom.createHTMLElement('span');
130
- this.currentAudioLanguage_.classList.add('shaka-current-selection-span');
131
- const language = this.player.getConfiguration().preferredAudioLanguage;
132
- this.currentAudioLanguage_.textContent =
133
- shaka.ui.LanguageUtils.getLanguageName(language, this.localization);
134
- label.appendChild(this.currentAudioLanguage_);
135
-
136
- this.languagesButton_.appendChild(label);
137
-
138
- this.parent.appendChild(this.languagesButton_);
139
- }
140
-
141
- /** @private */
142
- onLanguagesClick_() {
143
- this.controls.dispatchEvent(new shaka.util.FakeEvent('submenuopen'));
144
- shaka.ui.Utils.setDisplay(this.audioLangMenu_, true);
145
- // Focus on the currently selected language button.
146
- shaka.ui.Utils.focusOnTheChosenItem(this.audioLangMenu_);
147
- }
148
-
149
70
  /** @private */
150
71
  updateAudioLanguages_() {
151
72
  const tracks = this.player.getVariantTracks();
152
73
  const languages = this.player.getAudioLanguages();
153
74
 
154
- shaka.ui.LanguageUtils.updateLanguages(tracks, this.audioLangMenu_,
155
- languages,
156
- this.onAudioLanguageSelected_.bind(this), /* updateChosen */ true,
157
- this.currentAudioLanguage_,
158
- this.localization);
159
- shaka.ui.Utils.focusOnTheChosenItem(this.audioLangMenu_);
75
+ shaka.ui.LanguageUtils.updateLanguages(tracks, this.menu,
76
+ languages,
77
+ (lang) => this.onAudioLanguageSelected_(lang), /* updateChosen */ true,
78
+ this.currentSelection,
79
+ this.localization);
80
+ shaka.ui.Utils.focusOnTheChosenItem(this.menu);
160
81
 
161
82
  // TODO: document this event
162
83
  this.controls.dispatchEvent(
@@ -166,7 +87,7 @@ shaka.ui.AudioLanguageSelection = class extends shaka.ui.Element {
166
87
  /** @private */
167
88
  onTracksChanged_() {
168
89
  const hasVariants = this.player.getVariantTracks().length > 0;
169
- shaka.ui.Utils.setDisplay(this.languagesButton_, hasVariants);
90
+ shaka.ui.Utils.setDisplay(this.button, hasVariants);
170
91
  this.updateAudioLanguages_();
171
92
  }
172
93
 
@@ -185,13 +106,13 @@ shaka.ui.AudioLanguageSelection = class extends shaka.ui.Element {
185
106
  updateLocalizedStrings_() {
186
107
  const LocIds = shaka.ui.Locales.Ids;
187
108
 
188
- this.backFromLanguageButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
109
+ this.backButton.setAttribute(shaka.ui.Constants.ARIA_LABEL,
189
110
  this.localization.resolve(LocIds.BACK));
190
- this.languagesButton_.setAttribute(shaka.ui.Constants.ARIA_LABEL,
111
+ this.button.setAttribute(shaka.ui.Constants.ARIA_LABEL,
191
112
  this.localization.resolve(LocIds.LANGUAGE));
192
- this.languageNameSpan_.textContent =
113
+ this.nameSpan.textContent =
193
114
  this.localization.resolve(LocIds.LANGUAGE);
194
- this.backFromLanguageSpan_.textContent =
115
+ this.backSpan.textContent =
195
116
  this.localization.resolve(LocIds.LANGUAGE);
196
117
  }
197
118
  };