shaka-player 3.1.3 → 3.1.4

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 (94) hide show
  1. package/AUTHORS +3 -0
  2. package/CHANGELOG.md +56 -0
  3. package/CONTRIBUTORS +3 -0
  4. package/build/generateExterns.js +1 -1
  5. package/demo/common/assets.js +1 -1
  6. package/dist/controls.css +2 -2
  7. package/dist/controls.css.map +1 -1
  8. package/dist/demo.compiled.debug.js +1 -1
  9. package/dist/demo.compiled.js +1 -1
  10. package/dist/deps.js +1 -1
  11. package/dist/locales.js +35 -35
  12. package/dist/receiver.compiled.debug.js +1 -1
  13. package/dist/receiver.compiled.js +1 -1
  14. package/dist/shaka-player.compiled.d.ts +1 -1
  15. package/dist/shaka-player.compiled.debug.d.ts +1 -1
  16. package/dist/shaka-player.compiled.debug.externs.js +1 -1
  17. package/dist/shaka-player.compiled.debug.js +187 -183
  18. package/dist/shaka-player.compiled.debug.map +3 -3
  19. package/dist/shaka-player.compiled.externs.js +1 -1
  20. package/dist/shaka-player.compiled.js +799 -799
  21. package/dist/shaka-player.compiled.map +2 -2
  22. package/dist/shaka-player.ui.d.ts +1 -1
  23. package/dist/shaka-player.ui.debug.d.ts +1 -1
  24. package/dist/shaka-player.ui.debug.externs.js +3 -3
  25. package/dist/shaka-player.ui.debug.js +264 -260
  26. package/dist/shaka-player.ui.debug.map +4 -4
  27. package/dist/shaka-player.ui.externs.js +3 -3
  28. package/dist/shaka-player.ui.js +896 -895
  29. package/dist/shaka-player.ui.map +4 -4
  30. package/docs/tutorials/fairplay.md +1 -1
  31. package/karma.conf.js +1 -0
  32. package/lib/ads/client_side_ad_manager.js +46 -86
  33. package/lib/ads/server_side_ad_manager.js +7 -10
  34. package/lib/cast/cast_proxy.js +3 -2
  35. package/lib/cast/cast_receiver.js +2 -1
  36. package/lib/cast/cast_sender.js +2 -2
  37. package/lib/dash/dash_parser.js +93 -2
  38. package/lib/dash/segment_list.js +1 -1
  39. package/lib/dash/segment_template.js +1 -1
  40. package/lib/hls/hls_parser.js +30 -17
  41. package/lib/media/drm_engine.js +7 -0
  42. package/lib/media/gap_jumping_controller.js +4 -2
  43. package/lib/media/media_source_engine.js +1 -2
  44. package/lib/media/streaming_engine.js +2 -1
  45. package/lib/net/networking_engine.js +2 -1
  46. package/lib/player.js +22 -22
  47. package/lib/polyfill/media_capabilities.js +7 -1
  48. package/lib/polyfill/mediasource.js +6 -0
  49. package/lib/polyfill/orientation.js +1 -1
  50. package/lib/polyfill/patchedmediakeys_apple.js +4 -4
  51. package/lib/polyfill/patchedmediakeys_ms.js +4 -4
  52. package/lib/polyfill/patchedmediakeys_webkit.js +4 -4
  53. package/lib/text/mp4_ttml_parser.js +3 -0
  54. package/lib/text/text_engine.js +14 -2
  55. package/lib/text/ttml_text_parser.js +5 -0
  56. package/lib/text/ui_text_displayer.js +85 -37
  57. package/lib/util/fake_event.js +53 -7
  58. package/lib/util/periods.js +4 -1
  59. package/lib/util/platform.js +13 -1
  60. package/lib/util/stream_utils.js +13 -0
  61. package/package.json +1 -1
  62. package/test/cast/cast_proxy_unit.js +12 -6
  63. package/test/cast/cast_utils_unit.js +1 -1
  64. package/test/dash/dash_parser_manifest_unit.js +187 -1
  65. package/test/hls/hls_parser_unit.js +5 -1
  66. package/test/media/closed_caption_parser_unit.js +6 -1
  67. package/test/media/drm_engine_unit.js +43 -0
  68. package/test/player_integration.js +38 -0
  69. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  70. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  71. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  72. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  73. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  74. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  75. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  76. package/test/test/assets/3675/dash_0.mpd +30 -0
  77. package/test/test/assets/3675/dash_1.mpd +30 -0
  78. package/test/test/assets/3675/dash_2.mpd +30 -0
  79. package/test/test/assets/3675/dash_3.mpd +30 -0
  80. package/test/test/assets/3675/dash_4.mpd +30 -0
  81. package/test/test/assets/3675/dash_5.mpd +26 -0
  82. package/test/test/assets/3675/init-stream0.m4s +0 -0
  83. package/test/test/util/fake_ad_manager.js +1 -1
  84. package/test/text/ttml_text_parser_unit.js +9 -0
  85. package/test/text/ui_text_displayer_unit.js +29 -17
  86. package/test/ui/localization_unit.js +15 -14
  87. package/test/ui/text_displayer_layout_unit.js +13 -1
  88. package/test/util/stream_utils_unit.js +35 -0
  89. package/ui/cast_button.js +2 -3
  90. package/ui/controls.js +4 -6
  91. package/ui/less/containers.less +3 -7
  92. package/ui/locales/ru.json +6 -6
  93. package/ui/localization.js +28 -51
  94. package/ui/pip_button.js +2 -3
@@ -217,8 +217,10 @@ shaka.media.GapJumpingController = class {
217
217
  this.didFireLargeGap_ = true;
218
218
 
219
219
  // Event firing is synchronous.
220
- const event = new shaka.util.FakeEvent(
221
- 'largegap', {'currentTime': currentTime, 'gapSize': jumpSize});
220
+ const data = new Map()
221
+ .set('currentTime', currentTime)
222
+ .set('gapSize', jumpSize);
223
+ const event = new shaka.util.FakeEvent('largegap', data);
222
224
  event.cancelable = true;
223
225
  this.onEvent_(event);
224
226
 
@@ -48,8 +48,7 @@ shaka.media.MediaSourceEngine = class {
48
48
  * @param {!function(!Array.<shaka.extern.ID3Metadata>, number, ?number)=}
49
49
  * onMetadata
50
50
  */
51
- constructor(video, closedCaptionParser, textDisplayer,
52
- onMetadata) {
51
+ constructor(video, closedCaptionParser, textDisplayer, onMetadata) {
53
52
  /** @private {HTMLMediaElement} */
54
53
  this.video_ = video;
55
54
 
@@ -1662,7 +1662,8 @@ shaka.media.StreamingEngine = class {
1662
1662
 
1663
1663
  // Dispatch an event to notify the application about the emsg box.
1664
1664
  const eventName = shaka.Player.EventName.Emsg;
1665
- const event = new shaka.util.FakeEvent(eventName, {'detail': emsg});
1665
+ const data = (new Map()).set('detail', emsg);
1666
+ const event = new shaka.util.FakeEvent(eventName, data);
1666
1667
  this.playerInterface_.onEvent(event);
1667
1668
  }
1668
1669
  }
@@ -544,7 +544,8 @@ shaka.net.NetworkingEngine = class extends shaka.util.FakeEventTarget {
544
544
  }
545
545
 
546
546
  if (error.severity == shaka.util.Error.Severity.RECOVERABLE) {
547
- const event = new shaka.util.FakeEvent('retry', {'error': error});
547
+ const data = (new Map()).set('error', error);
548
+ const event = new shaka.util.FakeEvent('retry', data);
548
549
  this.dispatchEvent(event);
549
550
 
550
551
  // Move to the next URI.
package/lib/player.js CHANGED
@@ -596,7 +596,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
596
596
  enterNode: (node, has, wants) => {
597
597
  this.dispatchEvent(this.makeEvent_(
598
598
  /* name= */ shaka.Player.EventName.OnStateChange,
599
- /* data= */ {'state': node.name}));
599
+ /* data= */ (new Map()).set('state', node.name)));
600
600
 
601
601
  const action = actions.get(node);
602
602
  return action(has, wants);
@@ -627,7 +627,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
627
627
  onIdle: (node) => {
628
628
  this.dispatchEvent(this.makeEvent_(
629
629
  /* name= */ shaka.Player.EventName.OnStateIdle,
630
- /* data= */ {'state': node.name}));
630
+ /* data= */ (new Map()).set('state', node.name)));
631
631
  },
632
632
  };
633
633
 
@@ -647,7 +647,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
647
647
 
648
648
  /**
649
649
  * @param {!shaka.Player.EventName} name
650
- * @param {Object=} data
650
+ * @param {Map.<string, Object>=} data
651
651
  * @return {!shaka.util.FakeEvent}
652
652
  * @private
653
653
  */
@@ -1155,6 +1155,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1155
1155
  'mpg': 'video/mpeg',
1156
1156
  'mpeg': 'video/mpeg',
1157
1157
  'm3u8': 'application/x-mpegurl',
1158
+ 'mpd': 'application/dash+xml',
1158
1159
  'mp3': 'audio/mpeg',
1159
1160
  'aac': 'audio/aac',
1160
1161
  'flac': 'audio/flac',
@@ -2415,12 +2416,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2415
2416
  goog.asserts.assert(!endTime || startTime <= endTime,
2416
2417
  'Metadata start time should be less or equal to the end time!');
2417
2418
  const eventName = shaka.Player.EventName.Metadata;
2418
- const data = {
2419
- startTime: startTime,
2420
- endTime: endTime,
2421
- metadataType: metadataType,
2422
- payload: payload,
2423
- };
2419
+ const data = new Map()
2420
+ .set('startTime', startTime)
2421
+ .set('endTime', endTime)
2422
+ .set('metadataType', metadataType)
2423
+ .set('payload', payload);
2424
2424
  this.dispatchEvent(this.makeEvent_(eventName, data));
2425
2425
  }
2426
2426
 
@@ -2482,7 +2482,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2482
2482
  this.abrManager_.segmentDownloaded(deltaTimeMs, bytesDownloaded);
2483
2483
  }
2484
2484
  };
2485
-
2486
2485
  return new shaka.net.NetworkingEngine(onProgressUpdated_);
2487
2486
  }
2488
2487
 
@@ -4816,7 +4815,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4816
4815
  // Surface the buffering event so that the app knows if/when we are
4817
4816
  // buffering.
4818
4817
  const eventName = shaka.Player.EventName.Buffering;
4819
- this.dispatchEvent(this.makeEvent_(eventName, {'buffering': isBuffering}));
4818
+ const data = (new Map()).set('buffering', isBuffering);
4819
+ this.dispatchEvent(this.makeEvent_(eventName, data));
4820
4820
  }
4821
4821
 
4822
4822
  /**
@@ -5198,10 +5198,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5198
5198
  onVariantChanged_(from, to) {
5199
5199
  // Delay the 'variantchanged' event so StreamingEngine has time to absorb
5200
5200
  // the changes before the user tries to query it.
5201
- const event = this.makeEvent_(shaka.Player.EventName.VariantChanged, {
5202
- oldTrack: from,
5203
- newTrack: to,
5204
- });
5201
+ const data = new Map()
5202
+ .set('oldTrack', from)
5203
+ .set('newTrack', to);
5204
+ const event = this.makeEvent_(shaka.Player.EventName.VariantChanged, data);
5205
5205
  this.delayDispatchEvent_(event);
5206
5206
  }
5207
5207
 
@@ -5224,10 +5224,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5224
5224
 
5225
5225
  /** @private */
5226
5226
  onAbrStatusChanged_() {
5227
- const event = this.makeEvent_(shaka.Player.EventName.AbrStatusChanged, {
5228
- newStatus: this.config_.abr.enabled,
5229
- });
5230
- this.delayDispatchEvent_(event);
5227
+ const data = (new Map()).set('newStatus', this.config_.abr.enabled);
5228
+ this.delayDispatchEvent_(this.makeEvent_(
5229
+ shaka.Player.EventName.AbrStatusChanged, data));
5231
5230
  }
5232
5231
 
5233
5232
  /**
@@ -5244,7 +5243,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5244
5243
  }
5245
5244
 
5246
5245
  const eventName = shaka.Player.EventName.Error;
5247
- const event = this.makeEvent_(eventName, {'detail': error});
5246
+ const event = this.makeEvent_(eventName, (new Map()).set('detail', error));
5248
5247
  this.dispatchEvent(event);
5249
5248
  if (event.defaultPrevented) {
5250
5249
  error.handled = true;
@@ -5273,7 +5272,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5273
5272
  eventElement: region.eventElement,
5274
5273
  };
5275
5274
 
5276
- this.dispatchEvent(this.makeEvent_(eventName, {detail: clone}));
5275
+ const data = (new Map()).set('detail', clone);
5276
+ this.dispatchEvent(this.makeEvent_(eventName, data));
5277
5277
  }
5278
5278
 
5279
5279
  /**
@@ -6102,7 +6102,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6102
6102
  * @define {string} A version number taken from git at compile time.
6103
6103
  * @export
6104
6104
  */
6105
- shaka.Player.version = 'v3.1.3-uncompiled';
6105
+ shaka.Player.version = 'v3.1.4-uncompiled';
6106
6106
 
6107
6107
  // Initialize the deprecation system using the version string we just set
6108
6108
  // on the player.
@@ -34,8 +34,14 @@ shaka.polyfill.MediaCapabilities = class {
34
34
  // See: https://github.com/google/shaka-player/issues/3530
35
35
  // TODO: re-evaluate MediaCapabilities in the future versions of Apple
36
36
  // Browsers.
37
+ // Since MediaCapabilities implementation is buggy in PS5 browsers, we
38
+ // should always install polyfill for PS5 browsers.
39
+ // See: https://github.com/google/shaka-player/issues/3582
40
+ // TODO: re-evaluate MediaCapabilities in the future versions of PS5
41
+ // Browsers.
37
42
  if (!shaka.util.Platform.isChromecast() &&
38
43
  !shaka.util.Platform.isApple() &&
44
+ !shaka.util.Platform.isPS5() &&
39
45
  navigator.mediaCapabilities) {
40
46
  shaka.log.debug(
41
47
  'MediaCapabilities: Native mediaCapabilities support found.');
@@ -149,7 +155,7 @@ shaka.polyfill.MediaCapabilities = class {
149
155
  sessionTypes: mediaCapkeySystemConfig.sessionTypes,
150
156
  };
151
157
 
152
- // Only add the audio video capablities if they have valid data.
158
+ // Only add the audio video capabilities if they have valid data.
153
159
  // Otherwise the query will fail.
154
160
  if (audioCapabilities.length) {
155
161
  mediaKeySystemConfig.audioCapabilities = audioCapabilities;
@@ -208,6 +208,12 @@ shaka.polyfill.MediaSource = class {
208
208
  static patchVp09_() {
209
209
  const originalIsTypeSupported = MediaSource.isTypeSupported;
210
210
 
211
+ if (shaka.util.Platform.isWebOS()) {
212
+ // Don't do this on LG webOS as otherwise it is unable
213
+ // to play vp09 at all.
214
+ return;
215
+ }
216
+
211
217
  MediaSource.isTypeSupported = (mimeType) => {
212
218
  // Split the MIME type into its various parameters.
213
219
  const pieces = mimeType.split(/ *; */);
@@ -92,7 +92,7 @@ class extends shaka.util.FakeEventTarget {
92
92
 
93
93
  /** Dispatch a 'change' event. */
94
94
  dispatchChangeEvent() {
95
- const event = new shaka.util.FakeEvent('change', {});
95
+ const event = new shaka.util.FakeEvent('change');
96
96
  this.dispatchEvent(event);
97
97
  }
98
98
 
@@ -567,10 +567,10 @@ class extends shaka.util.FakeEventTarget {
567
567
 
568
568
  const isNew = this.keyStatuses.getStatus() == undefined;
569
569
 
570
- const event2 = new shaka.util.FakeEvent('message', {
571
- messageType: isNew ? 'license-request' : 'license-renewal',
572
- message: shaka.util.BufferUtils.toArrayBuffer(event.message),
573
- });
570
+ const data = new Map()
571
+ .set('messageType', isNew ? 'license-request' : 'license-renewal')
572
+ .set('message', shaka.util.BufferUtils.toArrayBuffer(event.message));
573
+ const event2 = new shaka.util.FakeEvent('message', data);
574
574
 
575
575
  this.dispatchEvent(event2);
576
576
  }
@@ -512,10 +512,10 @@ class extends shaka.util.FakeEventTarget {
512
512
 
513
513
  const isNew = this.keyStatuses.getStatus() == undefined;
514
514
 
515
- const event2 = new shaka.util.FakeEvent('message', {
516
- messageType: isNew ? 'license-request' : 'license-renewal',
517
- message: shaka.util.BufferUtils.toArrayBuffer(event.message),
518
- });
515
+ const data = new Map()
516
+ .set('messageType', isNew ? 'license-request' : 'license-renewal')
517
+ .set('message', shaka.util.BufferUtils.toArrayBuffer(event.message));
518
+ const event2 = new shaka.util.FakeEvent('message', data);
519
519
 
520
520
  this.dispatchEvent(event2);
521
521
  }
@@ -451,10 +451,10 @@ shaka.polyfill.PatchedMediaKeysWebkit.MediaKeys = class {
451
451
 
452
452
  const isNew = session.keyStatuses.getStatus() == undefined;
453
453
 
454
- const event2 = new shaka.util.FakeEvent('message', {
455
- messageType: isNew ? 'licenserequest' : 'licenserenewal',
456
- message: event.message,
457
- });
454
+ const data = new Map()
455
+ .set('messageType', isNew ? 'licenserequest' : 'licenserenewal')
456
+ .set('message', event.message);
457
+ const event2 = new shaka.util.FakeEvent('message', data);
458
458
 
459
459
  session.generated();
460
460
  session.dispatchEvent(event2);
@@ -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());
@@ -345,6 +345,19 @@ shaka.text.TextEngine = class {
345
345
  return cues;
346
346
  }
347
347
 
348
+ /**
349
+ * @param {!shaka.text.Cue} cue the cue to apply the timestamp to recursively
350
+ * @param {number} videoTimestampOffset the timestamp offset of the video
351
+ * @private
352
+ */
353
+ applyVideoTimestampOffsetRecursive_(cue, videoTimestampOffset) {
354
+ cue.startTime += videoTimestampOffset;
355
+ cue.endTime += videoTimestampOffset;
356
+ for (const nested of cue.nestedCues) {
357
+ this.applyVideoTimestampOffsetRecursive_(nested, videoTimestampOffset);
358
+ }
359
+ }
360
+
348
361
  /**
349
362
  * Store the closed captions in the text engine, and append the cues to the
350
363
  * text displayer. This is a side-channel used for embedded text only.
@@ -373,8 +386,7 @@ shaka.text.TextEngine = class {
373
386
 
374
387
  // Adjust CEA captions with respect to the timestamp offset of the video
375
388
  // stream in which they were embedded.
376
- cue.startTime += videoTimestampOffset;
377
- cue.endTime += videoTimestampOffset;
389
+ this.applyVideoTimestampOffsetRecursive_(cue, videoTimestampOffset);
378
390
 
379
391
  const keepThisCue =
380
392
  cue.startTime >= this.appendWindowStart_ &&
@@ -199,6 +199,11 @@ shaka.text.TtmlTextParser = class {
199
199
  /** @type {Element} */
200
200
  let parentElement = /** @type {Element} */(cueNode.parentNode);
201
201
 
202
+ if (cueNode.nodeType == Node.COMMENT_NODE) {
203
+ // The comments do not contain information that interests us here.
204
+ return null;
205
+ }
206
+
202
207
  if (cueNode.nodeType == Node.TEXT_NODE) {
203
208
  // This should generate an "anonymous span" according to the TTML spec.
204
209
  // So pretend the element was a <span>. parentElement was set above, so
@@ -167,9 +167,14 @@ shaka.text.UITextDisplayer = class {
167
167
  }
168
168
 
169
169
  // Remove the cues out of the time range.
170
+ const oldNumCues = this.cues_.length;
170
171
  this.cues_ = this.cues_.filter(
171
172
  (cue) => cue.startTime < start || cue.endTime >= end);
172
- this.updateCaptions_();
173
+ // If anything was actually removed in this process, force the captions to
174
+ // update. This makes sure that the currently-displayed cues will stop
175
+ // displaying if removed (say, due to the user changing languages).
176
+ const forceUpdate = oldNumCues > this.cues_.length;
177
+ this.updateCaptions_(forceUpdate);
173
178
 
174
179
  return true;
175
180
  }
@@ -192,13 +197,13 @@ shaka.text.UITextDisplayer = class {
192
197
  }
193
198
 
194
199
  /**
195
- * @param {!Array.<!shaka.text.Cue>} cues
200
+ * @param {!Array.<!shaka.extern.Cue>} cues
196
201
  * @param {!HTMLElement} container
197
202
  * @param {number} currentTime
198
- * @param {boolean} isNested
203
+ * @param {!Array.<!shaka.extern.Cue>} parents
199
204
  * @private
200
205
  */
201
- updateCuesRecursive_(cues, container, currentTime, isNested) {
206
+ updateCuesRecursive_(cues, container, currentTime, parents) {
202
207
  // Set to true if the cues have changed in some way, which will require
203
208
  // DOM changes. E.g. if a cue was added or removed.
204
209
  let updateDOM = false;
@@ -214,10 +219,12 @@ shaka.text.UITextDisplayer = class {
214
219
  * The cues whose corresponding elements should be in the DOM.
215
220
  * Some of these might be new, some might have been displayed beforehand.
216
221
  * These will only be added if updateDOM is true.
217
- * @type {!Array.<!shaka.text.Cue>}
222
+ * @type {!Array.<!shaka.extern.Cue>}
218
223
  */
219
224
  const toPlant = [];
220
225
  for (const cue of cues) {
226
+ parents.push(cue);
227
+
221
228
  let cueRegistry = this.currentCuesMap_.get(cue);
222
229
  const shouldBeDisplayed =
223
230
  cue.startTime <= currentTime && cue.endTime > currentTime;
@@ -241,7 +248,7 @@ shaka.text.UITextDisplayer = class {
241
248
  toPlant.push(cue);
242
249
  if (!cueRegistry) {
243
250
  // The cue has to be made!
244
- this.createCue_(cue, isNested);
251
+ this.createCue_(cue, parents);
245
252
  cueRegistry = this.currentCuesMap_.get(cue);
246
253
  wrapper = cueRegistry.wrapper;
247
254
  updateDOM = true;
@@ -255,8 +262,11 @@ shaka.text.UITextDisplayer = class {
255
262
  // guaranteed that the children will neither need to be added nor removed.
256
263
  if (cue.nestedCues.length > 0 && wrapper) {
257
264
  this.updateCuesRecursive_(
258
- cue.nestedCues, wrapper, currentTime, /* isNested= */ true);
265
+ cue.nestedCues, wrapper, currentTime, parents);
259
266
  }
267
+
268
+ const topCue = parents.pop();
269
+ goog.asserts.assert(topCue == cue, 'Parent cues should be kept in order');
260
270
  }
261
271
  if (updateDOM) {
262
272
  for (const cueElement of toUproot) {
@@ -304,7 +314,7 @@ shaka.text.UITextDisplayer = class {
304
314
 
305
315
  // Update the cues.
306
316
  this.updateCuesRecursive_(
307
- this.cues_, this.textContainer_, currentTime, false);
317
+ this.cues_, this.textContainer_, currentTime, /* parents= */ []);
308
318
 
309
319
  if (goog.DEBUG) {
310
320
  // Previously, we had an issue (#2076) where cues sometimes were not
@@ -327,10 +337,11 @@ shaka.text.UITextDisplayer = class {
327
337
  * Creates the object for a cue.
328
338
  *
329
339
  * @param {!shaka.extern.Cue} cue
330
- * @param {boolean} isNested
340
+ * @param {!Array.<!shaka.extern.Cue>} parents
331
341
  * @private
332
342
  */
333
- createCue_(cue, isNested) {
343
+ createCue_(cue, parents) {
344
+ const isNested = parents.length > 1;
334
345
  let type = isNested ? 'span' : 'div';
335
346
  if (cue.lineBreak || cue.spacer) {
336
347
  if (cue.spacer) {
@@ -344,7 +355,7 @@ shaka.text.UITextDisplayer = class {
344
355
  // Nested cues are inline elements. Top-level cues are block elements.
345
356
  const cueElement = shaka.util.Dom.createHTMLElement(type);
346
357
  if (type != 'br') {
347
- this.setCaptionStyles_(cueElement, cue, isNested);
358
+ this.setCaptionStyles_(cueElement, cue, parents);
348
359
  }
349
360
 
350
361
  let wrapper = cueElement;
@@ -363,13 +374,16 @@ shaka.text.UITextDisplayer = class {
363
374
  /**
364
375
  * @param {!HTMLElement} cueElement
365
376
  * @param {!shaka.extern.Cue} cue
366
- * @param {boolean} isNested
377
+ * @param {!Array.<!shaka.extern.Cue>} parents
367
378
  * @private
368
379
  */
369
- setCaptionStyles_(cueElement, cue, isNested) {
380
+ setCaptionStyles_(cueElement, cue, parents) {
370
381
  const Cue = shaka.text.Cue;
371
- let style = cueElement.style;
382
+ const inherit =
383
+ (cb) => shaka.text.UITextDisplayer.inheritProperty_(parents, cb);
384
+ const style = cueElement.style;
372
385
  const isLeaf = cue.nestedCues.length == 0;
386
+ const isNested = parents.length > 1;
373
387
 
374
388
  // TODO: wrapLine is not yet supported. Lines always wrap.
375
389
 
@@ -388,16 +402,6 @@ shaka.text.UITextDisplayer = class {
388
402
  const nonBreakingSpace = '\xa0';
389
403
  return nonBreakingSpace.repeat(match.length);
390
404
  });
391
- if (isNested) {
392
- cueElement.textContent = text;
393
- } else if (text.length) {
394
- // If a top-level cue has text, move to a <span> so the background is
395
- // styled correctly.
396
- const span = shaka.util.Dom.createHTMLElement('span');
397
- span.textContent = text;
398
- cueElement.appendChild(span);
399
- style = span.style;
400
- }
401
405
 
402
406
  style.backgroundColor = cue.backgroundColor;
403
407
  style.border = cue.border;
@@ -406,20 +410,40 @@ shaka.text.UITextDisplayer = class {
406
410
  style.opacity = cue.opacity;
407
411
  style.paddingLeft = shaka.text.UITextDisplayer.convertLengthValue_(
408
412
  cue.linePadding, cue, this.videoContainer_);
409
- style.paddingRight = shaka.text.UITextDisplayer.convertLengthValue_(
410
- cue.linePadding, cue, this.videoContainer_);
413
+ style.paddingRight =
414
+ shaka.text.UITextDisplayer.convertLengthValue_(
415
+ cue.linePadding, cue, this.videoContainer_);
411
416
 
412
417
  if (cue.backgroundImage) {
413
418
  style.backgroundImage = 'url(\'' + cue.backgroundImage + '\')';
414
419
  style.backgroundRepeat = 'no-repeat';
415
420
  style.backgroundSize = 'contain';
416
421
  style.backgroundPosition = 'center';
422
+ } else {
423
+ // If we have both text and nested cues, then style everything; otherwise
424
+ // place the text in its own <span> so the background doesn't fill the
425
+ // whole region.
426
+ let elem;
427
+ if (cue.nestedCues.length) {
428
+ elem = cueElement;
429
+ } else {
430
+ elem = shaka.util.Dom.createHTMLElement('span');
431
+ cueElement.appendChild(elem);
432
+ }
417
433
 
418
- if (cue.backgroundColor == '') {
419
- // In text-based cues, background color can default in CSS.
420
- // In bitmap-based cues, we default to a transparent background color,
421
- // so that the bitmap can be the only background.
422
- style.backgroundColor = 'transparent';
434
+ if (cue.border) {
435
+ elem.style.border = cue.border;
436
+ }
437
+ const bgColor = inherit((c) => c.backgroundColor);
438
+ if (bgColor) {
439
+ elem.style.backgroundColor = bgColor;
440
+ } else if (text) {
441
+ // If there is no background, default to a semi-transparent black.
442
+ // Only do this for the text itself.
443
+ elem.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
444
+ }
445
+ if (text) {
446
+ elem.textContent = text;
423
447
  }
424
448
  }
425
449
 
@@ -472,20 +496,28 @@ shaka.text.UITextDisplayer = class {
472
496
  // TODO: Implement lineAlignment of 'CENTER'.
473
497
  if (cue.line) {
474
498
  if (cue.lineInterpretation == Cue.lineInterpretation.PERCENTAGE) {
499
+ // When setting absolute positioning, you need to set x/y/width/height
500
+ // so the element is positioned correctly. Set these as default and
501
+ // other settings will override them.
475
502
  style.position = 'absolute';
503
+ style.left = '0';
504
+ style.top = '0';
476
505
  if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
506
+ style.width = '100%';
477
507
  if (cue.lineAlign == Cue.lineAlign.START) {
478
508
  style.top = cue.line + '%';
479
509
  } else if (cue.lineAlign == Cue.lineAlign.END) {
480
510
  style.bottom = cue.line + '%';
481
511
  }
482
512
  } else if (cue.writingMode == Cue.writingMode.VERTICAL_LEFT_TO_RIGHT) {
513
+ style.height = '100%';
483
514
  if (cue.lineAlign == Cue.lineAlign.START) {
484
515
  style.left = cue.line + '%';
485
516
  } else if (cue.lineAlign == Cue.lineAlign.END) {
486
517
  style.right = cue.line + '%';
487
518
  }
488
519
  } else {
520
+ style.height = '100%';
489
521
  if (cue.lineAlign == Cue.lineAlign.START) {
490
522
  style.right = cue.line + '%';
491
523
  } else if (cue.lineAlign == Cue.lineAlign.END) {
@@ -493,12 +525,7 @@ shaka.text.UITextDisplayer = class {
493
525
  }
494
526
  }
495
527
  }
496
- } else if (cue.region && cue.region.id &&
497
- ((!isNested && !isLeaf) || (cue.backgroundImage))) {
498
- // In text-base cues, regions are only applied to block container
499
- // (!isNested && !isLeaf).
500
- // In bitmap-based cues, region settings are used to specify the size and
501
- // position of the backgroundImage.
528
+ } else if (cue.region && cue.region.id) {
502
529
  const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
503
530
  const heightUnit = cue.region.heightUnits == percentageUnit ? '%' : 'px';
504
531
  const widthUnit = cue.region.widthUnits == percentageUnit ? '%' : 'px';
@@ -631,4 +658,25 @@ shaka.text.UITextDisplayer = class {
631
658
 
632
659
  return (containerHeight * value / cue.cellResolution.rows) + 'px';
633
660
  }
661
+
662
+ /**
663
+ * Inherits a property from the parent Cue elements. If the value is falsy,
664
+ * it is assumed to be inherited from the parent. This returns null if the
665
+ * value isn't found.
666
+ *
667
+ * @param {!Array.<!shaka.extern.Cue>} parents
668
+ * @param {function(!shaka.extern.Cue):?T} cb
669
+ * @return {?T}
670
+ * @template T
671
+ * @private
672
+ */
673
+ static inheritProperty_(parents, cb) {
674
+ for (let i = parents.length - 1; i >= 0; i--) {
675
+ const val = cb(parents[i]);
676
+ if (val || val === 0) {
677
+ return val;
678
+ }
679
+ }
680
+ return null;
681
+ }
634
682
  };
@@ -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.
@@ -80,7 +80,10 @@ shaka.util.PeriodCombiner = class {
80
80
 
81
81
  /** @return {!Array.<shaka.extern.Stream>} */
82
82
  getTextStreams() {
83
- return this.textStreams_;
83
+ // Return a copy of the array because makeTextStreamsForClosedCaptions
84
+ // may make changes to the contents of the array. Those changes should not
85
+ // propagate back to the PeriodCombiner.
86
+ return this.textStreams_.slice();
84
87
  }
85
88
 
86
89
  /** @return {!Array.<shaka.extern.Stream>} */