shaka-player 4.2.6 → 4.2.8

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 (51) hide show
  1. package/.github/workflows/selenium-lab-tests.yaml +22 -0
  2. package/CHANGELOG.md +36 -0
  3. package/dist/deps.js +4 -3
  4. package/dist/locales.js +30 -30
  5. package/dist/shaka-player.compiled.d.ts +10 -0
  6. package/dist/shaka-player.compiled.debug.d.ts +10 -0
  7. package/dist/shaka-player.compiled.debug.externs.js +12 -0
  8. package/dist/shaka-player.compiled.debug.js +177 -166
  9. package/dist/shaka-player.compiled.debug.map +4 -4
  10. package/dist/shaka-player.compiled.externs.js +12 -0
  11. package/dist/shaka-player.compiled.js +672 -666
  12. package/dist/shaka-player.compiled.map +4 -4
  13. package/dist/shaka-player.ui.d.ts +10 -0
  14. package/dist/shaka-player.ui.debug.d.ts +10 -0
  15. package/dist/shaka-player.ui.debug.externs.js +12 -0
  16. package/dist/shaka-player.ui.debug.js +189 -179
  17. package/dist/shaka-player.ui.debug.map +4 -4
  18. package/dist/shaka-player.ui.externs.js +12 -0
  19. package/dist/shaka-player.ui.js +820 -815
  20. package/dist/shaka-player.ui.map +4 -4
  21. package/lib/dash/segment_template.js +17 -5
  22. package/lib/hls/hls_parser.js +53 -16
  23. package/lib/media/media_source_engine.js +34 -4
  24. package/lib/media/segment_index.js +5 -10
  25. package/lib/media/segment_reference.js +39 -0
  26. package/lib/media/streaming_engine.js +15 -3
  27. package/lib/player.js +7 -1
  28. package/lib/polyfill/media_capabilities.js +4 -1
  29. package/lib/text/simple_text_displayer.js +2 -64
  30. package/lib/text/text_utils.js +88 -0
  31. package/lib/text/ui_text_displayer.js +24 -9
  32. package/lib/text/vtt_text_parser.js +256 -95
  33. package/lib/text/web_vtt_generator.js +2 -52
  34. package/lib/util/dom_utils.js +3 -1
  35. package/lib/util/platform.js +8 -0
  36. package/lib/util/stream_utils.js +6 -0
  37. package/lib/util/timer.js +17 -0
  38. package/package.json +1 -1
  39. package/support.html +1 -0
  40. package/test/dash/dash_parser_manifest_unit.js +0 -36
  41. package/test/hls/hls_live_unit.js +8 -30
  42. package/test/hls/hls_parser_unit.js +30 -0
  43. package/test/media/segment_index_unit.js +28 -0
  44. package/test/player_integration.js +27 -0
  45. package/test/player_unit.js +4 -2
  46. package/test/test/util/layout_tests.js +433 -0
  47. package/test/test/util/util.js +0 -120
  48. package/test/{ui → text}/text_displayer_layout_unit.js +59 -235
  49. package/test/text/ui_text_displayer_unit.js +72 -4
  50. package/test/text/vtt_text_parser_unit.js +123 -6
  51. package/test/text/web_vtt_layout_integration.js +528 -0
@@ -520,20 +520,24 @@ describe('HlsParser live', () => {
520
520
  });
521
521
  });
522
522
 
523
- it('sets timestamp offset for segments with discontinuity', async () => {
523
+ it('sets discontinuity sequence numbers', async () => {
524
524
  const ref1 = makeReference(
525
525
  'test:/main.mp4', 0, 2, /* syncTime= */ null,
526
526
  /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
527
527
  /* timestampOffset= */ 0);
528
+ ref1.discontinuitySequence = 30;
528
529
 
529
- // Expect the timestamp offset to be set for the segment after the
530
- // EXT-X-DISCONTINUITY tag.
531
530
  const ref2 = makeReference(
532
531
  'test:/main2.mp4', 2, 4, /* syncTime= */ null,
533
532
  /* baseUri= */ '', /* startByte= */ 0, /* endByte= */ null,
534
533
  /* timestampOffset= */ 0);
534
+ ref2.discontinuitySequence = 31;
535
535
 
536
- await testInitialManifest(master, mediaWithDiscontinuity, [ref1, ref2]);
536
+ const manifest = await testInitialManifest(
537
+ master, mediaWithDiscontinuity, [ref1, ref2]);
538
+
539
+ await testUpdate(
540
+ manifest, mediaWithUpdatedDiscontinuitySegment, [ref2]);
537
541
  });
538
542
 
539
543
  // Test for https://github.com/shaka-project/shaka-player/issues/4223
@@ -725,32 +729,6 @@ describe('HlsParser live', () => {
725
729
  shaka.net.NetworkingEngine.RequestType.MANIFEST);
726
730
  });
727
731
 
728
- it('reuses cached timestamp offset for segments with discontinuity',
729
- async () => {
730
- const ref1 = makeReference(
731
- 'test:/main.mp4', 0, 2, /* syncTime= */ null);
732
- const ref2 = makeReference(
733
- 'test:/main2.mp4', 2, 4, /* syncTime= */ null);
734
-
735
- const manifest = await testInitialManifest(
736
- master, mediaWithDiscontinuity, [ref1, ref2]);
737
-
738
- fakeNetEngine.request.calls.reset();
739
- await testUpdate(
740
- manifest, mediaWithUpdatedDiscontinuitySegment, [ref2]);
741
-
742
- // Only one request should be made, and it's for the playlist.
743
- // Expect to use the cached timestamp offset for the main2.mp4
744
- // segment, without fetching the start time again.
745
- expect(fakeNetEngine.request).toHaveBeenCalledTimes(1);
746
- fakeNetEngine.expectRequest(
747
- 'test:/video',
748
- shaka.net.NetworkingEngine.RequestType.MANIFEST);
749
- fakeNetEngine.expectNoRequest(
750
- 'test:/main.mp4',
751
- shaka.net.NetworkingEngine.RequestType.SEGMENT);
752
- });
753
-
754
732
  it('request playlist delta updates to skip segments', async () => {
755
733
  const mediaWithDeltaUpdates = [
756
734
  '#EXTM3U\n',
@@ -2524,6 +2524,9 @@ describe('HlsParser', () => {
2524
2524
  '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
2525
2525
  '#EXTINF:5,\n',
2526
2526
  '#EXT-X-BYTERANGE:121090@616\n',
2527
+ 'main.mp4\n',
2528
+ '#EXTINF:5,\n',
2529
+ '#EXT-X-BYTERANGE:121090@616\n',
2527
2530
  'main.mp4',
2528
2531
  ].join('');
2529
2532
 
@@ -2535,6 +2538,10 @@ describe('HlsParser', () => {
2535
2538
  '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
2536
2539
  '#EXTINF:5,\n',
2537
2540
  '#EXT-X-BYTERANGE:121090@616\n',
2541
+ 'main.mp4\n',
2542
+ '#EXTINF:5,\n',
2543
+ '#EXT-X-BYTERANGE:121090@616\n',
2544
+ '#EXT-X-KEY:METHOD=NONE\n',
2538
2545
  'main.mp4',
2539
2546
  ].join('');
2540
2547
 
@@ -2544,6 +2551,9 @@ describe('HlsParser', () => {
2544
2551
  '#EXT-X-KEY:METHOD=AES-128,',
2545
2552
  'URI="800k.key"\n',
2546
2553
  '#EXTINF:5,\n',
2554
+ 'main.ts\n',
2555
+ '#EXTINF:5,\n',
2556
+ '#EXT-X-KEY:METHOD=NONE\n',
2547
2557
  'main.ts',
2548
2558
  ].join('');
2549
2559
 
@@ -2596,6 +2606,26 @@ describe('HlsParser', () => {
2596
2606
 
2597
2607
  const actual = await parser.start('test:/master', playerInterface);
2598
2608
  expect(actual).toEqual(manifest);
2609
+
2610
+ const mp4AesEncryptionVideo = actual.variants[1].video;
2611
+ await mp4AesEncryptionVideo.createSegmentIndex();
2612
+ goog.asserts.assert(mp4AesEncryptionVideo.segmentIndex != null,
2613
+ 'Null segmentIndex!');
2614
+
2615
+ const firstMp4Segment = mp4AesEncryptionVideo.segmentIndex.get(0);
2616
+ expect(firstMp4Segment.hlsAes128Key).toBeDefined();
2617
+ const secondMp4Segment = mp4AesEncryptionVideo.segmentIndex.get(1);
2618
+ expect(secondMp4Segment.hlsAes128Key).toBeNull();
2619
+
2620
+ const tsAesEncryptionVideo = actual.variants[2].video;
2621
+ await tsAesEncryptionVideo.createSegmentIndex();
2622
+ goog.asserts.assert(tsAesEncryptionVideo.segmentIndex != null,
2623
+ 'Null segmentIndex!');
2624
+
2625
+ const firstTsSegment = tsAesEncryptionVideo.segmentIndex.get(0);
2626
+ expect(firstTsSegment.hlsAes128Key).toBeDefined();
2627
+ const secondTsSegment = tsAesEncryptionVideo.segmentIndex.get(1);
2628
+ expect(secondTsSegment.hlsAes128Key).toBeNull();
2599
2629
  });
2600
2630
 
2601
2631
  it('fails on AES-128 if WebCrypto APIs are not available', async () => {
@@ -532,6 +532,34 @@ describe('SegmentIndex', /** @suppress {accessControls} */ () => {
532
532
  goog.asserts.assert(position1 != null, 'Position should not be null!');
533
533
  expect(index1.get(position1)).toBe(references2[1]);
534
534
  });
535
+
536
+ it('does not duplicate references with rounding errors', () => {
537
+ /** @type {!Array.<!shaka.media.SegmentReference>} */
538
+ const references1 = [
539
+ makeReference(uri(10), 10, 20),
540
+ makeReference(uri(20), 20, 30),
541
+ ];
542
+ const index1 = new shaka.media.SegmentIndex(references1);
543
+
544
+ // 0.24 microseconds: an insignificant rounding error.
545
+ const tinyError = 0.24e-6;
546
+
547
+ /** @type {!Array.<!shaka.media.SegmentReference>} */
548
+ const references2 = [
549
+ makeReference(uri(10), 10, 20),
550
+ // The difference between this and the equivalent old reference is an
551
+ // insignificant rounding error.
552
+ makeReference(uri(20), 20 + tinyError, 30 + tinyError),
553
+ makeReference(uri(30), 30 + tinyError, 40),
554
+ ];
555
+
556
+ index1.merge(references2);
557
+ expect(index1.references.length).toBe(3);
558
+ expect(index1.references[0]).toEqual(references1[0]);
559
+ // The new references replaced the old one.
560
+ expect(index1.references[1]).toEqual(references2[1]);
561
+ expect(index1.references[2]).toEqual(references2[2]);
562
+ });
535
563
  });
536
564
 
537
565
  describe('evict', () => {
@@ -72,6 +72,33 @@ describe('Player', () => {
72
72
  });
73
73
  }); // describe('attach')
74
74
 
75
+ describe('destroy', () => {
76
+ // Regression test for:
77
+ // https://github.com/shaka-project/shaka-player/issues/4850
78
+ it('does not leave any lingering timers', async () => {
79
+ shaka.util.Timer.activeTimers.clear();
80
+
81
+ // Unlike the other tests in this file, this uses an uncompiled build of
82
+ // Shaka, so that we don't need to expose shaka.util.Timer.activeTimers.
83
+ player = new shaka.Player(video);
84
+ waiter.setPlayer(player);
85
+
86
+ // Play the video for a little while.
87
+ await player.load('test:sintel');
88
+ video.play();
89
+ await waiter.waitUntilPlayheadReachesOrFailOnTimeout(video, 1, 10);
90
+
91
+ // Destroy the player.
92
+ await player.destroy();
93
+
94
+ // Are there any timers left?
95
+ for (const timer of shaka.util.Timer.activeTimers.keys()) {
96
+ const stackTrace = shaka.util.Timer.activeTimers.get(timer);
97
+ fail('Lingering timer exists! Stack trace of creation: ' + stackTrace);
98
+ }
99
+ });
100
+ });
101
+
75
102
  describe('updateStartTime() in manifestparsed event handler', () => {
76
103
  it('does not get segments prior to startTime', async () => {
77
104
  player.addEventListener('manifestparsed', () => {
@@ -3390,7 +3390,9 @@ describe('Player', () => {
3390
3390
  variant.addAudio(0, (stream) => {
3391
3391
  stream.channelsCount = 6;
3392
3392
  stream.audioSamplingRate = 48000;
3393
- stream.codecs = 'ac-3';
3393
+ // ac-3 is rewritten as ec-3 on Tizen, so for the stability of this
3394
+ // test case, use ec-3.
3395
+ stream.codecs = 'ec-3';
3394
3396
  });
3395
3397
  });
3396
3398
 
@@ -3415,7 +3417,7 @@ describe('Player', () => {
3415
3417
  expect(abrManager.variants.length).toBe(1);
3416
3418
  // It should be the 6-channel variant, based on our preference.
3417
3419
  expect(abrManager.variants[0].audio.channelsCount).toBe(6);
3418
- expect(abrManager.variants[0].audio.codecs).toBe('ac-3');
3420
+ expect(abrManager.variants[0].audio.codecs).toBe('ec-3');
3419
3421
  });
3420
3422
  });
3421
3423
 
@@ -0,0 +1,433 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ // These helpers have many async interface methods that only need to do async
8
+ // work in some implementations. Disable the normal eslint requirement that
9
+ // async functions always use await.
10
+ /* eslint-disable require-await */
11
+
12
+
13
+ // A minimum similarity score for screenshots, between 0 and 1.
14
+ const minSimilarity = 0.95;
15
+
16
+ const originalCast = window.chrome && window.chrome.cast;
17
+
18
+ shaka.test.LayoutTests = class {
19
+ /** @param {string} prefix */
20
+ constructor(prefix) {
21
+ /** @type {string} */
22
+ this.prefix = prefix;
23
+ }
24
+
25
+ /**
26
+ * Waits for a particular font to be loaded. Useful in screenshot tests to
27
+ * make sure we have consistent results with regard to the web fonts we load
28
+ * in the UI.
29
+ *
30
+ * @param {string} name
31
+ * @return {!Promise}
32
+ */
33
+ async waitForFont(name) {
34
+ await new Promise((resolve, reject) => {
35
+ // https://github.com/zachleat/fontfaceonload
36
+ // eslint-disable-next-line new-cap
37
+ FontFaceOnload(name, {
38
+ success: resolve,
39
+ error: () => {
40
+ reject(new Error('Timeout waiting for font ' + name + ' to load'));
41
+ },
42
+ timeout: 10 * 1000, // ms
43
+ });
44
+ });
45
+
46
+ // Wait one extra tick to make sure the font rendering on the page has been
47
+ // updated. Without this, we saw some rare test flake in Firefox on Mac.
48
+ await shaka.test.Util.shortDelay();
49
+ }
50
+
51
+ /**
52
+ * Checks with Karma to see if this browser can take a screenshot.
53
+ *
54
+ * Only WebDriver-connected browsers can take a screenshot, and only Karma
55
+ * knows if the browser is connected via WebDriver. So this must be checked
56
+ * in Karma via an HTTP request.
57
+ *
58
+ * @return {!Promise.<boolean>}
59
+ */
60
+ static async supported() {
61
+ // We need our own ID for Karma to look up the WebDriver connection.
62
+ // For manually-connected browsers, this ID may not exist. In those cases,
63
+ // this method is expected to return false.
64
+ const parentUrlParams = window.parent.location.search;
65
+
66
+ const buffer = await shaka.test.Util.fetch(
67
+ '/screenshot/isSupported' + parentUrlParams);
68
+ const json = shaka.util.StringUtils.fromUTF8(buffer);
69
+ const ok = /** @type {boolean} */(JSON.parse(json));
70
+ return ok;
71
+ }
72
+
73
+ /**
74
+ * Asks Karma to take a screenshot for us via the WebDriver connection and
75
+ * compare it to the "official" screenshot for this test and platform. Sets
76
+ * an expectation that the new screenshot does not differ from the official
77
+ * screenshot more than a fixed threshold.
78
+ *
79
+ * Only works on browsers connected via WebDriver. Use supportsScreenshots()
80
+ * to filter screenshot-dependent tests.
81
+ *
82
+ * @param {HTMLElement} element The HTML element to screenshot. Must be
83
+ * within the bounds of the viewport.
84
+ * @param {string} name An identifier for the screenshot. Use alphanumeric
85
+ * plus dash and underscore only.
86
+ * @param {number} minSimilarity A minimum similarity score between 0 and 1.
87
+ * @return {!Promise}
88
+ */
89
+ static async checkScreenshot(element, name, minSimilarity=1) {
90
+ // Make sure the DOM is up-to-date and layout has settled before continuing.
91
+ // Without this delay, or with a shorter delay, we sometimes get missing
92
+ // elements in our UITextDisplayer tests on some platforms.
93
+ await shaka.test.Util.delay(0.1);
94
+
95
+ // We need our own ID for Karma to look up the WebDriver connection.
96
+ // By this point, we should have passed supportsScreenshots(), so the ID
97
+ // should definitely be there.
98
+ const parentUrlParams = window.parent.location.search;
99
+ goog.asserts.assert(parentUrlParams.includes('id='), 'No ID in URL!');
100
+
101
+ // Tests run in an iframe. So we also need the coordinates of that iframe
102
+ // within the page, so that the screenshot can be consistently cropped to
103
+ // the element we care about.
104
+ const iframe = /** @type {HTMLIFrameElement} */(
105
+ window.parent.document.getElementById('context'));
106
+ const iframeRect = iframe.getBoundingClientRect();
107
+ const elementRect = element.getBoundingClientRect();
108
+ const x = iframeRect.left + elementRect.left;
109
+ const y = iframeRect.top + elementRect.top;
110
+ const width = elementRect.width;
111
+ const height = elementRect.height;
112
+
113
+ // Furthermore, the screenshot may not be at the scale you expect. Measure
114
+ // the browser window size in JavaScript and communicate that to Karma, too,
115
+ // so it can convert coordinates before cropping. This value, as opposed to
116
+ // document.body.getBoundingClientRect(), seems to most accurately reflect
117
+ // the size of the screenshot area.
118
+ const bodyWidth = window.parent.innerWidth;
119
+ const bodyHeight = window.parent.innerHeight;
120
+
121
+ // In addition to the id param from the top-level window, pass these
122
+ // parameters to the screenshot endpoint in Karma.
123
+ const params = {x, y, width, height, bodyWidth, bodyHeight, name};
124
+
125
+ let paramsString = '';
126
+ for (const k in params) {
127
+ paramsString += '&' + k + '=' + params[k];
128
+ }
129
+
130
+ const buffer = await shaka.test.Util.fetch(
131
+ '/screenshot/diff' + parentUrlParams + paramsString);
132
+ const json = shaka.util.StringUtils.fromUTF8(buffer);
133
+ const similarity = /** @type {number} */(JSON.parse(json));
134
+
135
+ // If the minimum similarity is not met, you can review the new screenshot
136
+ // and the diff image in the screenshots folder. Look for images that end
137
+ // with "-new" and "-diff". (NOTE: The diff is a pixel-wise diff for human
138
+ // review, and is not produced with the same structural similarity
139
+ // algorithm used to detect changes in the test.) If cropping doesn't work
140
+ // right, you can view the full-page screenshot in the image that ends with
141
+ // "-full".
142
+ expect(similarity).withContext(name).not.toBeLessThan(minSimilarity);
143
+ }
144
+
145
+ /** @param {!HTMLElement} element */
146
+ positionElementForScreenshot(element) {
147
+ // The element we screenshot will be 16:9 and small.
148
+ element.style.width = '320px';
149
+ element.style.height = '180px';
150
+
151
+ // The background is green so we can better see the background color of
152
+ // the text spans within the subtitles, and so it is easier to identify
153
+ // cropping issues.
154
+ element.style.backgroundColor = 'green';
155
+
156
+ // Make sure the element is in the top-left corner of the iframe that
157
+ // contains the tests.
158
+ element.style.top = '0';
159
+ element.style.left = '0';
160
+ element.style.position = 'fixed';
161
+ element.style.margin = '0';
162
+ element.style.padding = '0';
163
+ }
164
+ };
165
+
166
+ shaka.test.TextLayoutTests = class extends shaka.test.LayoutTests {
167
+ /** @param {string} prefix */
168
+ constructor(prefix) {
169
+ super(prefix);
170
+
171
+ /** @type {HTMLElement} */
172
+ this.videoContainer = null;
173
+
174
+ /** @type {shaka.extern.TextDisplayer} */
175
+ this.textDisplayer = null;
176
+ }
177
+
178
+ /** @override */
179
+ static async supported() {
180
+ const baseSupported = await super.supported();
181
+ if (!baseSupported) {
182
+ return false;
183
+ }
184
+
185
+ // Due to a Safari implementation bug, the browser only does the correct
186
+ // thing for a timing edge case on Safari 16+. Skip the tests on earlier
187
+ // versions.
188
+ const safariVersion = shaka.util.Platform.safariVersion();
189
+ if (safariVersion && safariVersion < 16) {
190
+ return false;
191
+ }
192
+
193
+ // Due to updates in the rendering and/or default styles in Chrome, the
194
+ // screenshots for native rendering only match in Chrome 106+.
195
+ const chromeVersion = shaka.util.Platform.chromeVersion();
196
+ if (chromeVersion && chromeVersion < 106) {
197
+ return false;
198
+ }
199
+
200
+ return true;
201
+ }
202
+
203
+ /** @return {!Promise} */
204
+ async beforeAll() {}
205
+
206
+ /** @return {!Promise} */
207
+ async beforeEach() {}
208
+
209
+ recreateTextDisplayer() {}
210
+
211
+ /**
212
+ * @param {number} time
213
+ * @return {!Promise}
214
+ */
215
+ async beforeScreenshot(time) {}
216
+
217
+ /** @return {!Promise} */
218
+ async afterEach() {
219
+ await this.textDisplayer.destroy();
220
+ this.textDisplayer = null;
221
+ }
222
+
223
+ /** @return {!Promise} */
224
+ async afterAll() {}
225
+
226
+ /**
227
+ * @param {string} baseName The base name of the screenshot.
228
+ * @param {number=} time The time to seek to in the screenshot. Defaults to
229
+ * 0.1, when most of our tests will be showing cues (timed 0-1).
230
+ * @return {!Promise}
231
+ */
232
+ async checkScreenshot(baseName, time=0.1) {
233
+ await this.beforeScreenshot(time);
234
+
235
+ return shaka.test.LayoutTests.checkScreenshot(
236
+ /* element= */ this.videoContainer,
237
+ this.prefix + '-' + baseName,
238
+ minSimilarity);
239
+ }
240
+ };
241
+
242
+ shaka.test.DomTextLayoutTests = class extends shaka.test.TextLayoutTests {
243
+ /** @param {string} prefix */
244
+ constructor(prefix) {
245
+ super(prefix);
246
+
247
+ /** @type {HTMLLinkElement} */
248
+ this.cssLink = null;
249
+
250
+ /** @type {shaka.test.FakeVideo} */
251
+ this.mockVideo = null;
252
+ }
253
+
254
+ /** @override */
255
+ async beforeAll() {
256
+ // Disable cast so the UI controls don't create cast sessions.
257
+ if (window.chrome) {
258
+ window.chrome['cast'] = null;
259
+ }
260
+
261
+ // Add css file
262
+ this.cssLink = /** @type {!HTMLLinkElement} */(
263
+ document.createElement('link'));
264
+ await shaka.test.UiUtils.setupCSS(this.cssLink);
265
+
266
+ // There's no actual video inside this container, but subtitles will be
267
+ // positioned within this space.
268
+ this.videoContainer = /** @type {!HTMLElement} */(
269
+ document.createElement('div'));
270
+ document.body.appendChild(this.videoContainer);
271
+
272
+ this.positionElementForScreenshot(this.videoContainer);
273
+
274
+ // Some of the styles in our CSS are only applied within this class. Add
275
+ // this explicitly, since we don't instantiate controls in all of the
276
+ // tests.
277
+ this.videoContainer.classList.add('shaka-video-container');
278
+
279
+ await this.waitForFont('Roboto');
280
+ }
281
+
282
+ /** @override */
283
+ async beforeEach() {
284
+ this.mockVideo = new shaka.test.FakeVideo();
285
+
286
+ this.recreateTextDisplayer();
287
+ }
288
+
289
+ /** @override */
290
+ recreateTextDisplayer() {
291
+ this.textDisplayer = new shaka.text.UITextDisplayer(
292
+ /** @type {!HTMLMediaElement} */(this.mockVideo),
293
+ this.videoContainer);
294
+ this.textDisplayer.setTextVisibility(true);
295
+ }
296
+
297
+ /** @override */
298
+ async beforeScreenshot(time) {
299
+ // Set the faked time.
300
+ this.mockVideo.currentTime = time;
301
+
302
+ // Trigger the display update logic to notice the time change by
303
+ // appending an empty array.
304
+ this.textDisplayer.append([]);
305
+ }
306
+
307
+ /** @override */
308
+ async afterEach() {
309
+ await super.afterEach();
310
+ this.mockVideo = null;
311
+ }
312
+
313
+ /** @override */
314
+ async afterAll() {
315
+ document.body.removeChild(this.videoContainer);
316
+ this.videoContainer = null;
317
+
318
+ document.head.removeChild(this.cssLink);
319
+ this.cssLink = null;
320
+
321
+ if (window.chrome) {
322
+ window.chrome['cast'] = originalCast;
323
+ }
324
+ }
325
+ };
326
+
327
+ shaka.test.NativeTextLayoutTests = class extends shaka.test.TextLayoutTests {
328
+ /** @param {string} prefix */
329
+ constructor(prefix) {
330
+ super(prefix);
331
+
332
+ /** @type {HTMLVideoElement} */
333
+ this.video = null;
334
+
335
+ /** @type {shaka.util.EventManager} */
336
+ this.eventManager = null;
337
+
338
+ /** @type {shaka.test.Waiter} */
339
+ this.waiter = null;
340
+ }
341
+
342
+ /** @override */
343
+ async beforeAll() {
344
+ this.video = shaka.test.UiUtils.createVideoElement();
345
+
346
+ // On some platforms, such as Chrome on Android, we may see a "cast"
347
+ // button overlayed if this isn't set.
348
+ this.video.disableRemotePlayback = true;
349
+
350
+ document.body.appendChild(this.video);
351
+
352
+ this.positionElementForScreenshot(this.video);
353
+
354
+ this.eventManager = new shaka.util.EventManager();
355
+ this.waiter = new shaka.test.Waiter(this.eventManager);
356
+
357
+ const canPlay = this.waiter.failOnTimeout(false).timeoutAfter(10)
358
+ .waitForEvent(this.video, 'canplay');
359
+
360
+ // Video content is required to show native subtitles. This is a small
361
+ // green frame.
362
+ this.video.src = '/base/test/test/assets/green-pixel.mp4';
363
+ await canPlay;
364
+ expect(this.video.duration).toBeGreaterThan(0);
365
+ expect(this.video.videoWidth).toBeGreaterThan(0);
366
+
367
+ // There is no separate container, so assign the video to the base class
368
+ // container. This is where screenshots will be taken by the base class.
369
+ this.videoContainer = this.video;
370
+ }
371
+
372
+ /** @override */
373
+ async beforeEach() {
374
+ this.recreateTextDisplayer();
375
+ }
376
+
377
+ /** @override */
378
+ recreateTextDisplayer() {
379
+ this.textDisplayer = new shaka.text.SimpleTextDisplayer(this.video);
380
+ this.textDisplayer.setTextVisibility(true);
381
+ }
382
+
383
+ /** @override */
384
+ async beforeScreenshot(time) {
385
+ // On Firefox, Safari, and legacy Edge, the video must be played a little
386
+ // _after_ appending cues in order to consistently show subtitles
387
+ // natively on the video element.
388
+
389
+ // Seek to the beginning so that we can reasonably wait for movement
390
+ // after playing below. If somehow the playhead ends up at the end of
391
+ // the video, we should seek back before we play.
392
+ this.video.currentTime = 0;
393
+
394
+ // The this.video must be played a little now, after the cues were appended,
395
+ // but before the screenshot.
396
+ this.video.playbackRate = 1;
397
+ this.video.play();
398
+ await this.waiter.failOnTimeout(false).timeoutAfter(5)
399
+ .waitForMovement(this.video);
400
+ this.video.pause();
401
+
402
+ // Seek to a time when cues should be showing.
403
+ this.video.currentTime = time;
404
+ // Get into a playing state, but without movement.
405
+ this.video.playbackRate = 0;
406
+ this.video.play();
407
+
408
+ // Add a short delay to ensure that the system has caught up and that
409
+ // native text displayers have been updated by the browser.
410
+ await shaka.test.Util.delay(0.1);
411
+ }
412
+
413
+ /** @override */
414
+ async afterEach() {
415
+ await super.afterEach();
416
+ }
417
+
418
+ /** @override */
419
+ async afterAll() {
420
+ document.body.removeChild(this.video);
421
+ this.video = null;
422
+ // The video container and the video are the same in this class.
423
+ this.videoContainer = null;
424
+
425
+ this.eventManager.release();
426
+ this.eventManager = null;
427
+ this.waiter = null;
428
+ }
429
+ };
430
+
431
+ // If we don't re-enable this rule by the end of the file, eslint exits with an
432
+ // error code.
433
+ /* eslint-enable require-await */