shaka-player 3.3.8 → 3.3.9

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 (43) hide show
  1. package/.github/workflows/build-and-test.yaml +67 -15
  2. package/.github/workflows/compute-incremental-coverage.py +251 -0
  3. package/.github/workflows/report-incremental-coverage.yaml +39 -0
  4. package/.github/workflows/selenium-lab-tests.yaml +44 -1
  5. package/AUTHORS +1 -0
  6. package/CHANGELOG.md +15 -0
  7. package/CONTRIBUTORS +1 -0
  8. package/dist/controls.css +1 -1
  9. package/dist/locales.js +32 -32
  10. package/dist/shaka-player.compiled.d.ts +1 -1
  11. package/dist/shaka-player.compiled.debug.d.ts +1 -1
  12. package/dist/shaka-player.compiled.debug.externs.js +1 -1
  13. package/dist/shaka-player.compiled.debug.js +85 -79
  14. package/dist/shaka-player.compiled.debug.map +3 -3
  15. package/dist/shaka-player.compiled.externs.js +1 -1
  16. package/dist/shaka-player.compiled.js +317 -313
  17. package/dist/shaka-player.compiled.map +3 -3
  18. package/dist/shaka-player.ui.d.ts +1 -1
  19. package/dist/shaka-player.ui.debug.d.ts +1 -1
  20. package/dist/shaka-player.ui.debug.externs.js +1 -1
  21. package/dist/shaka-player.ui.debug.js +100 -94
  22. package/dist/shaka-player.ui.debug.map +3 -3
  23. package/dist/shaka-player.ui.externs.js +1 -1
  24. package/dist/shaka-player.ui.js +492 -489
  25. package/dist/shaka-player.ui.map +3 -3
  26. package/karma.conf.js +3 -11
  27. package/lib/ads/client_side_ad_manager.js +1 -1
  28. package/lib/media/drm_engine.js +1 -1
  29. package/lib/media/streaming_engine.js +1 -0
  30. package/lib/player.js +1 -6
  31. package/lib/polyfill/media_capabilities.js +2 -0
  32. package/lib/text/cue.js +0 -5
  33. package/lib/text/simple_text_displayer.js +21 -9
  34. package/lib/text/ui_text_displayer.js +83 -19
  35. package/lib/util/buffer_utils.js +3 -0
  36. package/lib/util/object_utils.js +1 -0
  37. package/lib/util/stream_utils.js +23 -3
  38. package/lib/util/string_utils.js +63 -16
  39. package/package.json +5 -2
  40. package/test/text/simple_text_displayer_unit.js +27 -0
  41. package/test/text/ui_text_displayer_unit.js +65 -0
  42. package/test/util/string_utils_unit.js +46 -8
  43. package/ui/text_selection.js +2 -2
package/karma.conf.js CHANGED
@@ -160,10 +160,6 @@ module.exports = (config) => {
160
160
  'node_modules/es6-promise-polyfill/promise.js',
161
161
  // Babel polyfill, required for async/await
162
162
  'node_modules/@babel/polyfill/dist/polyfill.js',
163
- // TextDecoder polyfill, required for TextDecoder/TextEncoder on IE and
164
- // legacy Edge
165
- // eslint-disable-next-line max-len
166
- 'node_modules/fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js',
167
163
 
168
164
  // muxjs module next
169
165
  'node_modules/mux.js/dist/mux.min.js',
@@ -306,13 +302,6 @@ module.exports = (config) => {
306
302
  // Force failure when running empty test-suites.
307
303
  failOnEmptyTestSuite: true,
308
304
 
309
- coverageReporter: {
310
- includeAllSources: true,
311
- reporters: [
312
- {type: 'text'},
313
- ],
314
- },
315
-
316
305
  specReporter: {
317
306
  suppressSkipped: true,
318
307
  showBrowser: true,
@@ -405,9 +394,12 @@ module.exports = (config) => {
405
394
 
406
395
  config.set({
407
396
  coverageReporter: {
397
+ includeAllSources: true,
408
398
  reporters: [
409
399
  {type: 'html', dir: 'coverage'},
410
400
  {type: 'cobertura', dir: 'coverage', file: 'coverage.xml'},
401
+ {type: 'json-summary', dir: 'coverage', file: 'coverage.json'},
402
+ {type: 'json', dir: 'coverage', file: 'coverage-details.json'},
411
403
  ],
412
404
  },
413
405
  });
@@ -409,7 +409,7 @@ shaka.ads.ClientSideAdManager = class {
409
409
  onAdComplete_(e) {
410
410
  this.onEvent_(new shaka.util.FakeEvent(shaka.ads.AdManager.AD_STOPPED,
411
411
  (new Map()).set('originalEvent', e)));
412
- if (this.ad_.isLinear()) {
412
+ if (this.ad_ && this.ad_.isLinear()) {
413
413
  this.adContainer_.removeAttribute('ad-active');
414
414
  this.video_.play();
415
415
  }
@@ -1495,7 +1495,7 @@ shaka.media.DrmEngine = class {
1495
1495
  if (shaka.media.DrmEngine.isPlayReadyKeySystem(
1496
1496
  this.currentDrmInfo_.keySystem) &&
1497
1497
  keyId.byteLength == 16 &&
1498
- shaka.util.Platform.isEdge()) {
1498
+ (shaka.util.Platform.isEdge() || shaka.util.Platform.isPS4())) {
1499
1499
  // Read out some fields in little-endian:
1500
1500
  const dataView = shaka.util.BufferUtils.toDataView(keyId);
1501
1501
  const part0 = dataView.getUint32(0, /* LE= */ true);
@@ -1770,6 +1770,7 @@ shaka.media.StreamingEngine = class {
1770
1770
  *
1771
1771
  * @return {!Promise.<BufferSource>}
1772
1772
  * @private
1773
+ * @suppress {strictMissingProperties}
1773
1774
  */
1774
1775
  async fetch_(mediaState, reference, streamDataCallback) {
1775
1776
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
package/lib/player.js CHANGED
@@ -851,14 +851,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
851
851
  if (!window.Promise) {
852
852
  shaka.log.alwaysWarn('A Promise implementation or polyfill is required');
853
853
  }
854
- if (!window.TextDecoder || !window.TextEncoder) {
855
- shaka.log.alwaysWarn(
856
- 'A TextDecoder/TextEncoder implementation or polyfill is required');
857
- }
858
854
 
859
855
  // Basic features needed for the library to be usable.
860
856
  const basicSupport = !!window.Promise && !!window.Uint8Array &&
861
- !!window.TextDecoder && !!window.TextEncoder &&
862
857
  // eslint-disable-next-line no-restricted-syntax
863
858
  !!Array.prototype.forEach;
864
859
  if (!basicSupport) {
@@ -6634,7 +6629,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6634
6629
  * @define {string} A version number taken from git at compile time.
6635
6630
  * @export
6636
6631
  */
6637
- shaka.Player.version = 'v3.3.8-uncompiled';
6632
+ shaka.Player.version = 'v3.3.9-uncompiled';
6638
6633
 
6639
6634
  // Initialize the deprecation system using the version string we just set
6640
6635
  // on the player.
@@ -41,6 +41,8 @@ shaka.polyfill.MediaCapabilities = class {
41
41
  !shaka.util.Platform.isApple() &&
42
42
  !shaka.util.Platform.isPS4() &&
43
43
  !shaka.util.Platform.isPS5() &&
44
+ !shaka.util.Platform.isWebOS() &&
45
+ !shaka.util.Platform.isTizen() &&
44
46
  navigator.mediaCapabilities) {
45
47
  shaka.log.info(
46
48
  'MediaCapabilities: Native mediaCapabilities support found.');
package/lib/text/cue.js CHANGED
@@ -41,14 +41,9 @@ shaka.text.Cue = class {
41
41
  */
42
42
  this.endTime = endTime;
43
43
 
44
- // The explicit type here is to work around an apparent compiler bug where
45
- // the compiler seems to get confused about the type. Since we are
46
- // overriding the interface type, it shouldn't be necessary to do this.
47
- // Compiler version 20200517.
48
44
  /**
49
45
  * @override
50
46
  * @exportInterface
51
- * @type {string}
52
47
  */
53
48
  this.payload = payload;
54
49
 
@@ -127,16 +127,28 @@ shaka.text.SimpleTextDisplayer = class {
127
127
  // We don't want to modify the array or objects passed in, since we don't
128
128
  // technically own them. So we build a new array and replace certain items
129
129
  // in it if they need to be flattened.
130
- const flattenedCues = cues.map((cue) => {
131
- if (cue.nestedCues.length) {
132
- const flatCue = cue.clone();
133
- flatCue.nestedCues = [];
134
- flatCue.payload = flattenPayload(cue);
135
- return flatCue;
136
- } else {
137
- return cue;
130
+ // We also don't want to flatten the text payloads starting at a container
131
+ // element; otherwise, for containers encapsulating multiple caption lines,
132
+ // the lines would merge into a single cue. This is undesirable when a
133
+ // subset of the captions are outside of the append time window. To fix
134
+ // this, we only call flattenPayload() starting at elements marked as
135
+ // isContainer = false.
136
+ const getCuesToFlatten = (cues, result) => {
137
+ for (const cue of cues) {
138
+ if (cue.isContainer) {
139
+ // Recurse to find the actual text payload cues.
140
+ getCuesToFlatten(cue.nestedCues, result);
141
+ } else {
142
+ // Flatten the payload.
143
+ const flatCue = cue.clone();
144
+ flatCue.nestedCues = [];
145
+ flatCue.payload = flattenPayload(cue);
146
+ result.push(flatCue);
147
+ }
138
148
  }
139
- });
149
+ return result;
150
+ };
151
+ const flattenedCues = getCuesToFlatten(cues, []);
140
152
 
141
153
  // Convert cues.
142
154
  const textTrackCues = [];
@@ -78,6 +78,7 @@ shaka.text.UITextDisplayer = class {
78
78
  * the cue (e.g. the HTML element to put nested cues inside).
79
79
  * @private {Map.<!shaka.extern.Cue, !{
80
80
  * cueElement: !HTMLElement,
81
+ * regionElement: HTMLElement,
81
82
  * wrapper: !HTMLElement
82
83
  * }>}
83
84
  */
@@ -98,6 +99,9 @@ shaka.text.UITextDisplayer = class {
98
99
  });
99
100
  this.resizeObserver_.observe(this.textContainer_);
100
101
  }
102
+
103
+ /** @private {Map.<string, !HTMLElement>} */
104
+ this.regionElements_ = new Map();
101
105
  }
102
106
 
103
107
 
@@ -235,6 +239,11 @@ shaka.text.UITextDisplayer = class {
235
239
  // even ones which are going to be planted again.
236
240
  toUproot.push(cueRegistry.cueElement);
237
241
 
242
+ // Also uproot all displayed region elements.
243
+ if (cueRegistry.regionElement) {
244
+ toUproot.push(cueRegistry.regionElement);
245
+ }
246
+
238
247
  // If the cue should not be displayed, remove it entirely.
239
248
  if (!shouldBeDisplayed) {
240
249
  // Since something has to be removed, we will need to update the DOM.
@@ -269,8 +278,12 @@ shaka.text.UITextDisplayer = class {
269
278
  goog.asserts.assert(topCue == cue, 'Parent cues should be kept in order');
270
279
  }
271
280
  if (updateDOM) {
272
- for (const cueElement of toUproot) {
273
- container.removeChild(cueElement);
281
+ for (const element of toUproot) {
282
+ // NOTE: Because we uproot shared region elements, too, we might hit an
283
+ // element here that has no parent because we've already processed it.
284
+ if (element.parentElement) {
285
+ element.parentElement.removeChild(element);
286
+ }
274
287
  }
275
288
  toPlant.sort((a, b) => {
276
289
  if (a.startTime != b.startTime) {
@@ -282,7 +295,12 @@ shaka.text.UITextDisplayer = class {
282
295
  for (const cue of toPlant) {
283
296
  const cueRegistry = this.currentCuesMap_.get(cue);
284
297
  goog.asserts.assert(cueRegistry, 'cueRegistry should exist.');
285
- container.appendChild(cueRegistry.cueElement);
298
+ if (cueRegistry.regionElement) {
299
+ container.appendChild(cueRegistry.regionElement);
300
+ cueRegistry.regionElement.appendChild(cueRegistry.cueElement);
301
+ } else {
302
+ container.appendChild(cueRegistry.cueElement);
303
+ }
286
304
  }
287
305
  }
288
306
  }
@@ -299,11 +317,15 @@ shaka.text.UITextDisplayer = class {
299
317
 
300
318
  const currentTime = this.video_.currentTime;
301
319
  if (!this.isTextVisible_ || forceUpdate) {
302
- if (this.currentCuesMap_.size > 0) {
303
- // Clear away any existing cues.
304
- shaka.util.Dom.removeAllChildren(this.textContainer_);
305
- this.currentCuesMap_.clear();
320
+ // Remove child elements from all regions.
321
+ for (const regionElement of this.regionElements_.values()) {
322
+ shaka.util.Dom.removeAllChildren(regionElement);
306
323
  }
324
+ // Remove all top-level elements in the text container.
325
+ shaka.util.Dom.removeAllChildren(this.textContainer_);
326
+ // Clear the element maps.
327
+ this.currentCuesMap_.clear();
328
+ this.regionElements_.clear();
307
329
  }
308
330
  if (this.isTextVisible_) {
309
331
  // Log currently attached cue elements for verification, later.
@@ -333,6 +355,54 @@ shaka.text.UITextDisplayer = class {
333
355
  }
334
356
  }
335
357
 
358
+ /**
359
+ * Get or create a region element corresponding to the cue region. These are
360
+ * cached by ID.
361
+ *
362
+ * @param {!shaka.extern.Cue} cue
363
+ * @return {!HTMLElement}
364
+ * @private
365
+ */
366
+ getRegionElement_(cue) {
367
+ const region = cue.region;
368
+
369
+ if (this.regionElements_.has(region.id)) {
370
+ return this.regionElements_.get(region.id);
371
+ }
372
+
373
+ const regionElement = shaka.util.Dom.createHTMLElement('span');
374
+
375
+ const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
376
+ const heightUnit = region.heightUnits == percentageUnit ? '%' : 'px';
377
+ const widthUnit = region.widthUnits == percentageUnit ? '%' : 'px';
378
+ const viewportAnchorUnit =
379
+ region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
380
+
381
+ regionElement.id = 'shaka-text-region---' + region.id;
382
+ regionElement.classList.add('shaka-text-region');
383
+
384
+ regionElement.style.height = region.height + heightUnit;
385
+ regionElement.style.width = region.width + widthUnit;
386
+ regionElement.style.position = 'absolute';
387
+ regionElement.style.top = region.viewportAnchorY + viewportAnchorUnit;
388
+ regionElement.style.left = region.viewportAnchorX + viewportAnchorUnit;
389
+
390
+ regionElement.style.display = 'flex';
391
+ regionElement.style.flexDirection = 'column';
392
+ regionElement.style.alignItems = 'center';
393
+
394
+ if (cue.displayAlign == shaka.text.Cue.displayAlign.BEFORE) {
395
+ regionElement.style.justifyContent = 'flex-start';
396
+ } else if (cue.displayAlign == shaka.text.Cue.displayAlign.CENTER) {
397
+ regionElement.style.justifyContent = 'center';
398
+ } else {
399
+ regionElement.style.justifyContent = 'flex-end';
400
+ }
401
+
402
+ this.regionElements_.set(region.id, regionElement);
403
+ return regionElement;
404
+ }
405
+
336
406
  /**
337
407
  * Creates the object for a cue.
338
408
  *
@@ -360,6 +430,11 @@ shaka.text.UITextDisplayer = class {
360
430
  this.setCaptionStyles_(cueElement, cue, parents, needWrapper);
361
431
  }
362
432
 
433
+ let regionElement = null;
434
+ if (cue.region && cue.region.id) {
435
+ regionElement = this.getRegionElement_(cue);
436
+ }
437
+
363
438
  let wrapper = cueElement;
364
439
  if (needWrapper) {
365
440
  // Create a wrapper element which will serve to contain all children into
@@ -371,7 +446,7 @@ shaka.text.UITextDisplayer = class {
371
446
  cueElement.appendChild(wrapper);
372
447
  }
373
448
 
374
- this.currentCuesMap_.set(cue, {cueElement, wrapper});
449
+ this.currentCuesMap_.set(cue, {cueElement, wrapper, regionElement});
375
450
  }
376
451
 
377
452
  /**
@@ -531,17 +606,6 @@ shaka.text.UITextDisplayer = class {
531
606
  }
532
607
  }
533
608
  }
534
- } else if (cue.region && cue.region.id) {
535
- const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
536
- const heightUnit = cue.region.heightUnits == percentageUnit ? '%' : 'px';
537
- const widthUnit = cue.region.widthUnits == percentageUnit ? '%' : 'px';
538
- const viewportAnchorUnit =
539
- cue.region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
540
- style.height = cue.region.height + heightUnit;
541
- style.width = cue.region.width + widthUnit;
542
- style.position = 'absolute';
543
- style.top = cue.region.viewportAnchorY + viewportAnchorUnit;
544
- style.left = cue.region.viewportAnchorX + viewportAnchorUnit;
545
609
  }
546
610
 
547
611
  style.lineHeight = cue.lineHeight;
@@ -20,6 +20,7 @@ shaka.util.BufferUtils = class {
20
20
  * @param {?BufferSource} arr2
21
21
  * @return {boolean}
22
22
  * @export
23
+ * @suppress {strictMissingProperties}
23
24
  */
24
25
  static equal(arr1, arr2) {
25
26
  const BufferUtils = shaka.util.BufferUtils;
@@ -135,8 +136,10 @@ shaka.util.BufferUtils = class {
135
136
  static view_(data, offset, length, Type) {
136
137
  const buffer = shaka.util.BufferUtils.unsafeGetArrayBuffer_(data);
137
138
  // Absolute end of the |data| view within |buffer|.
139
+ /** @suppress {strictMissingProperties} */
138
140
  const dataEnd = (data.byteOffset || 0) + data.byteLength;
139
141
  // Absolute start of the result within |buffer|.
142
+ /** @suppress {strictMissingProperties} */
140
143
  const rawStart = (data.byteOffset || 0) + offset;
141
144
  const start = Math.max(0, Math.min(rawStart, dataEnd));
142
145
  // Absolute end of the result within |buffer|.
@@ -22,6 +22,7 @@ shaka.util.ObjectUtils = class {
22
22
  const seenObjects = new Set();
23
23
  // This recursively clones the value |val|, using the captured variable
24
24
  // |seenObjects| to track the objects we have already cloned.
25
+ /** @suppress {strictMissingProperties} */
25
26
  const clone = (val) => {
26
27
  switch (typeof val) {
27
28
  case 'undefined':
@@ -578,8 +578,16 @@ shaka.util.StreamUtils = class {
578
578
  // VideoConfiguration
579
579
  mediaDecodingConfig.video = {
580
580
  contentType: fullType,
581
- width: video.width || 1,
582
- height: video.height || 1,
581
+
582
+ // NOTE: Some decoders strictly check the width and height fields and
583
+ // won't decode smaller than 64x64. So if we don't have this info (as
584
+ // is the case in some of our simpler tests), assume a 64x64 resolution
585
+ // to fill in this required field for MediaCapabilities.
586
+ //
587
+ // This became an issue specifically on Firefox on M1 Macs.
588
+ width: video.width || 64,
589
+ height: video.height || 64,
590
+
583
591
  bitrate: video.bandwidth || variant.bandwidth || 1,
584
592
  // framerate must be greater than 0, otherwise the config is invalid.
585
593
  framerate: video.frameRate || 1,
@@ -742,7 +750,19 @@ shaka.util.StreamUtils = class {
742
750
  */
743
751
  static patchVp9(codec) {
744
752
  if (codec == 'vp9') {
745
- return 'vp09.00.10.08';
753
+ // This means profile 0, level 4.1, 8-bit color. This supports 1080p @
754
+ // 60Hz. See https://en.wikipedia.org/wiki/VP9#Levels
755
+ //
756
+ // If we don't have more detailed codec info, assume this profile and
757
+ // level because it's high enough to likely accommodate the parameters we
758
+ // do have, such as width and height. If an implementation is checking
759
+ // the profile and level very strictly, we want older VP9 content to
760
+ // still work to some degree. But we don't want to set a level so high
761
+ // that it is rejected by a hardware decoder that can't handle the
762
+ // maximum requirements of the level.
763
+ //
764
+ // This became an issue specifically on Firefox on M1 Macs.
765
+ return 'vp09.00.41.08';
746
766
  }
747
767
  return codec;
748
768
  }
@@ -37,6 +37,7 @@ shaka.util.StringUtils = class {
37
37
  if (uint8[0] == 0xef && uint8[1] == 0xbb && uint8[2] == 0xbf) {
38
38
  uint8 = uint8.subarray(3);
39
39
  }
40
+
40
41
  if (window.TextDecoder && !shaka.util.Platform.isPS4()) {
41
42
  // Use the TextDecoder interface to decode the text. This has the
42
43
  // advantage compared to the previously-standard decodeUriComponent that
@@ -51,23 +52,69 @@ shaka.util.StringUtils = class {
51
52
  }
52
53
  return decoded;
53
54
  } else {
54
- // http://stackoverflow.com/a/13691499
55
- const utf8 = shaka.util.StringUtils.fromCharCode(uint8);
56
- // This converts each character in the string to an escape sequence. If
57
- // the character is in the ASCII range, it is not converted; otherwise it
58
- // is converted to a URI escape sequence.
59
- // Example: '\x67\x35\xe3\x82\xac' -> 'g#%E3%82%AC'
60
- const escaped = escape(utf8);
61
- // Decode the escaped sequence. This will interpret UTF-8 sequences into
62
- // the correct character.
63
- // Example: 'g#%E3%82%AC' -> 'g#€'
64
- try {
65
- return decodeURIComponent(escaped);
66
- } catch (e) {
67
- throw new shaka.util.Error(
68
- shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.TEXT,
69
- shaka.util.Error.Code.BAD_ENCODING);
55
+ // Homebrewed UTF-8 decoder based on
56
+ // https://en.wikipedia.org/wiki/UTF-8#Encoding
57
+ // Unlike decodeURIComponent, won't throw on bad encoding.
58
+ // In this way, it is similar to TextDecoder.
59
+
60
+ let decoded = '';
61
+ for (let i = 0; i < uint8.length; ++i) {
62
+ // By default, the "replacement character" codepoint.
63
+ let codePoint = 0xFFFD;
64
+
65
+ // Top bit is 0, 1-byte encoding.
66
+ if ((uint8[i] & 0x80) == 0) {
67
+ codePoint = uint8[i];
68
+
69
+ // Top 3 bits of byte 0 are 110, top 2 bits of byte 1 are 10,
70
+ // 2-byte encoding.
71
+ } else if (uint8.length >= i + 2 &&
72
+ (uint8[i] & 0xe0) == 0xc0 &&
73
+ (uint8[i + 1] & 0xc0) == 0x80) {
74
+ codePoint = ((uint8[i] & 0x1f) << 6) |
75
+ ((uint8[i + 1] & 0x3f));
76
+ i += 1; // Consume one extra byte.
77
+
78
+ // Top 4 bits of byte 0 are 1110, top 2 bits of byte 1 and 2 are 10,
79
+ // 3-byte encoding.
80
+ } else if (uint8.length >= i + 3 &&
81
+ (uint8[i] & 0xf0) == 0xe0 &&
82
+ (uint8[i + 1] & 0xc0) == 0x80 &&
83
+ (uint8[i + 2] & 0xc0) == 0x80) {
84
+ codePoint = ((uint8[i] & 0x0f) << 12) |
85
+ ((uint8[i + 1] & 0x3f) << 6) |
86
+ ((uint8[i + 2] & 0x3f));
87
+ i += 2; // Consume two extra bytes.
88
+
89
+ // Top 5 bits of byte 0 are 11110, top 2 bits of byte 1, 2 and 3 are 10,
90
+ // 4-byte encoding.
91
+ } else if (uint8.length >= i + 4 &&
92
+ (uint8[i] & 0xf1) == 0xf0 &&
93
+ (uint8[i + 1] & 0xc0) == 0x80 &&
94
+ (uint8[i + 2] & 0xc0) == 0x80 &&
95
+ (uint8[i + 3] & 0xc0) == 0x80) {
96
+ codePoint = ((uint8[i] & 0x07) << 18) |
97
+ ((uint8[i + 1] & 0x3f) << 12) |
98
+ ((uint8[i + 2] & 0x3f) << 6) |
99
+ ((uint8[i + 3] & 0x3f));
100
+ i += 3; // Consume three extra bytes.
101
+ }
102
+
103
+ // JavaScript strings are a series of UTF-16 characters.
104
+ if (codePoint <= 0xffff) {
105
+ decoded += String.fromCharCode(codePoint);
106
+ } else {
107
+ // UTF-16 surrogate-pair encoding, based on
108
+ // https://en.wikipedia.org/wiki/UTF-16#Description
109
+ const baseCodePoint = codePoint - 0x10000;
110
+ const highPart = baseCodePoint >> 10;
111
+ const lowPart = baseCodePoint & 0x3ff;
112
+ decoded += String.fromCharCode(0xd800 + highPart);
113
+ decoded += String.fromCharCode(0xdc00 + lowPart);
114
+ }
70
115
  }
116
+
117
+ return decoded;
71
118
  }
72
119
  }
73
120
 
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.3.8",
4
+ "version": "3.3.9",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -94,6 +94,9 @@
94
94
  "prepublishOnly": "python build/checkversion.py && python build/all.py --force"
95
95
  },
96
96
  "dependencies": {
97
- "eme-encryption-scheme-polyfill": "^2.0.3"
97
+ "eme-encryption-scheme-polyfill": "^2.1.1"
98
+ },
99
+ "engines": {
100
+ "node": ">=14"
98
101
  }
99
102
  }
@@ -127,6 +127,33 @@ describe('SimpleTextDisplayer', () => {
127
127
  [shakaCue]);
128
128
  });
129
129
 
130
+ it('flattens nested cue payloads correctly', () => {
131
+ const level0ContainerCue = new shaka.text.Cue(10, 30, '');
132
+ level0ContainerCue.isContainer = true;
133
+
134
+ const level1NonContainerCueA = new shaka.text.Cue(10, 20, '');
135
+ const level1NonContainerCueB = new shaka.text.Cue(20, 30, '');
136
+
137
+ // Add a trailing whitespace character to get a space-delimited expected
138
+ // result.
139
+ const cueANestedCue0 = new shaka.text.Cue(10, 20, 'Cue A Test0 ');
140
+ const cueANestedCue1 = new shaka.text.Cue(10, 20, 'Cue A Test1');
141
+ const cueBNestedCue0 = new shaka.text.Cue(20, 30, 'Cue B Test0 ');
142
+ const cueBNestedCue1 = new shaka.text.Cue(20, 30, 'Cue B Test1');
143
+
144
+ level1NonContainerCueA.nestedCues = [cueANestedCue0, cueANestedCue1];
145
+ level1NonContainerCueB.nestedCues = [cueBNestedCue0, cueBNestedCue1];
146
+ level0ContainerCue.nestedCues =
147
+ [level1NonContainerCueA, level1NonContainerCueB];
148
+
149
+ verifyHelper(
150
+ [
151
+ {startTime: 10, endTime: 20, text: 'Cue A Test0 Cue A Test1'},
152
+ {startTime: 20, endTime: 30, text: 'Cue B Test0 Cue B Test1'},
153
+ ],
154
+ [level0ContainerCue]);
155
+ });
156
+
130
157
  // Regression test for b/159050711
131
158
  it('maintains the styles of the parent cue', () => {
132
159
  const shakaCue = new shaka.text.Cue(10, 20, '');
@@ -50,6 +50,7 @@ describe('UITextDisplayer', () => {
50
50
  });
51
51
 
52
52
  beforeEach(() => {
53
+ video.currentTime = 0;
53
54
  textDisplayer = new shaka.text.UITextDisplayer(video, videoContainer);
54
55
  });
55
56
 
@@ -396,4 +397,68 @@ describe('UITextDisplayer', () => {
396
397
  expect(parentCueElements.length).toBe(1);
397
398
  expect(parentCueElements[0].textContent).toBe('');
398
399
  });
400
+
401
+ it('creates separate elements for cue regions', () => {
402
+ const cueRegion = new shaka.text.CueRegion();
403
+ cueRegion.id = 'regionId';
404
+ cueRegion.height = 80;
405
+ cueRegion.heightUnits = shaka.text.CueRegion.units.PERCENTAGE;
406
+ cueRegion.width = 80;
407
+ cueRegion.widthUnits = shaka.text.CueRegion.units.PERCENTAGE;
408
+ cueRegion.viewportAnchorX = 10;
409
+ cueRegion.viewportAnchorY = 10;
410
+ cueRegion.viewportAnchorUnits = shaka.text.CueRegion.units.PERCENTAGE;
411
+
412
+ // These all attach to the same region, but only one region element should
413
+ // be created.
414
+ const cues = [
415
+ new shaka.text.Cue(0, 100, ''),
416
+ new shaka.text.Cue(0, 100, ''),
417
+ new shaka.text.Cue(0, 100, ''),
418
+ ];
419
+ for (const cue of cues) {
420
+ cue.displayAlign = shaka.text.Cue.displayAlign.CENTER;
421
+ cue.region = cueRegion;
422
+ }
423
+
424
+ textDisplayer.setTextVisibility(true);
425
+ textDisplayer.append(cues);
426
+ updateCaptions();
427
+
428
+ const textContainer = videoContainer.querySelector('.shaka-text-container');
429
+ const allRegionElements = textContainer.querySelectorAll(
430
+ '.shaka-text-region');
431
+
432
+ // Verify that the nested cues are all attached to a single region element.
433
+ expect(allRegionElements.length).toBe(1);
434
+ const regionElement = allRegionElements[0];
435
+ const children = Array.from(regionElement.childNodes).filter(
436
+ (e) => e.nodeType == Node.ELEMENT_NODE);
437
+ expect(children.length).toBe(3);
438
+
439
+ // Verify styles applied to the region element.
440
+ const regionCssObj = parseCssText(regionElement.style.cssText);
441
+ const expectRegionCssObj = {
442
+ 'position': 'absolute',
443
+ 'height': '80%',
444
+ 'width': '80%',
445
+ 'top': '10%',
446
+ 'left': '10%',
447
+ 'display': 'flex',
448
+ 'flex-direction': 'column',
449
+ 'align-items': 'center',
450
+ 'justify-content': 'center',
451
+ };
452
+ expect(regionCssObj).toEqual(jasmine.objectContaining(expectRegionCssObj));
453
+
454
+ for (const caption of children) {
455
+ // Verify that styles applied to the nested cues _DO NOT_ include region
456
+ // placement.
457
+ const cueCssObj = parseCssText(caption.style.cssText);
458
+ expect(Object.keys(cueCssObj)).not.toContain('height');
459
+ expect(Object.keys(cueCssObj)).not.toContain('width');
460
+ expect(Object.keys(cueCssObj)).not.toContain('top');
461
+ expect(Object.keys(cueCssObj)).not.toContain('left');
462
+ }
463
+ });
399
464
  });