shaka-player 3.2.0 → 3.2.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 (162) hide show
  1. package/.github/workflows/build_and_test.yaml +73 -0
  2. package/.github/workflows/release.yaml +53 -0
  3. package/AUTHORS +3 -0
  4. package/CHANGELOG.md +182 -1
  5. package/CONTRIBUTORS +3 -0
  6. package/README.md +4 -4
  7. package/build/checkversion.py +1 -1
  8. package/build/generateExterns.js +1 -1
  9. package/build/misspellings.txt +1 -0
  10. package/build/shakaBuildHelpers.py +6 -5
  11. package/build/subprocessWindowsPatch.py +103 -0
  12. package/build/test.py +6 -0
  13. package/demo/common/assets.js +1 -1
  14. package/demo/index.html +1 -1
  15. package/dist/controls.css +2 -2
  16. package/dist/controls.css.map +1 -1
  17. package/dist/demo.compiled.debug.js +1 -1
  18. package/dist/demo.compiled.js +1 -1
  19. package/dist/deps.js +8 -8
  20. package/dist/locales.js +37 -37
  21. package/dist/receiver.compiled.debug.js +1 -1
  22. package/dist/receiver.compiled.js +1 -1
  23. package/dist/shaka-player.compiled.d.ts +195 -5
  24. package/dist/shaka-player.compiled.debug.d.ts +195 -5
  25. package/dist/shaka-player.compiled.debug.externs.js +226 -14
  26. package/dist/shaka-player.compiled.debug.js +424 -399
  27. package/dist/shaka-player.compiled.debug.map +4 -4
  28. package/dist/shaka-player.compiled.externs.js +226 -14
  29. package/dist/shaka-player.compiled.js +826 -812
  30. package/dist/shaka-player.compiled.map +4 -4
  31. package/dist/shaka-player.ui.d.ts +197 -16
  32. package/dist/shaka-player.ui.debug.d.ts +197 -16
  33. package/dist/shaka-player.ui.debug.externs.js +230 -27
  34. package/dist/shaka-player.ui.debug.js +466 -442
  35. package/dist/shaka-player.ui.debug.map +4 -4
  36. package/dist/shaka-player.ui.externs.js +230 -27
  37. package/dist/shaka-player.ui.js +946 -932
  38. package/dist/shaka-player.ui.map +4 -4
  39. package/docs/tutorials/fairplay.md +1 -1
  40. package/docs/tutorials/faq.md +14 -0
  41. package/docs/tutorials/ui.md +5 -5
  42. package/externs/ima.js +4 -0
  43. package/externs/shaka/ads.js +25 -0
  44. package/externs/shaka/player.js +8 -0
  45. package/externs/shaka/text.js +10 -0
  46. package/karma.conf.js +20 -15
  47. package/lib/ads/ad_manager.js +46 -26
  48. package/lib/ads/client_side_ad_manager.js +82 -88
  49. package/lib/ads/server_side_ad_manager.js +45 -13
  50. package/lib/cast/cast_proxy.js +6 -2
  51. package/lib/cast/cast_receiver.js +5 -1
  52. package/lib/cast/cast_sender.js +2 -2
  53. package/lib/cea/sei_processor.js +24 -17
  54. package/lib/dash/dash_parser.js +131 -24
  55. package/lib/dash/segment_list.js +1 -1
  56. package/lib/dash/segment_template.js +1 -1
  57. package/lib/hls/hls_parser.js +74 -17
  58. package/lib/hls/manifest_text_parser.js +1 -0
  59. package/lib/media/closed_caption_parser.js +3 -1
  60. package/lib/media/content_workarounds.js +12 -0
  61. package/lib/media/drm_engine.js +20 -1
  62. package/lib/media/gap_jumping_controller.js +8 -4
  63. package/lib/media/media_source_engine.js +1 -2
  64. package/lib/media/playhead.js +1 -1
  65. package/lib/media/region_observer.js +39 -33
  66. package/lib/media/region_timeline.js +17 -19
  67. package/lib/media/segment_index.js +2 -1
  68. package/lib/media/segment_reference.js +19 -1
  69. package/lib/media/streaming_engine.js +8 -4
  70. package/lib/net/networking_engine.js +6 -1
  71. package/lib/offline/download_manager.js +1 -2
  72. package/lib/offline/storage.js +8 -9
  73. package/lib/player.js +129 -59
  74. package/lib/polyfill/aria.js +12 -2
  75. package/lib/polyfill/encryption_scheme.js +2 -0
  76. package/lib/polyfill/fullscreen.js +2 -0
  77. package/lib/polyfill/mathround.js +2 -0
  78. package/lib/polyfill/media_capabilities.js +73 -21
  79. package/lib/polyfill/mediasource.js +8 -0
  80. package/lib/polyfill/orientation.js +3 -1
  81. package/lib/polyfill/patchedmediakeys_apple.js +6 -4
  82. package/lib/polyfill/patchedmediakeys_ms.js +6 -4
  83. package/lib/polyfill/patchedmediakeys_nop.js +2 -0
  84. package/lib/polyfill/patchedmediakeys_webkit.js +6 -4
  85. package/lib/polyfill/pip_webkit.js +2 -0
  86. package/lib/polyfill/storage_estimate.js +2 -0
  87. package/lib/polyfill/video_play_promise.js +2 -0
  88. package/lib/polyfill/videoplaybackquality.js +2 -0
  89. package/lib/polyfill/vttcue.js +2 -0
  90. package/lib/text/cue.js +6 -0
  91. package/lib/text/mp4_ttml_parser.js +3 -0
  92. package/lib/text/text_engine.js +14 -2
  93. package/lib/text/ttml_text_parser.js +135 -87
  94. package/lib/text/ui_text_displayer.js +213 -80
  95. package/lib/text/vtt_text_parser.js +4 -2
  96. package/lib/text/web_vtt_generator.js +21 -12
  97. package/lib/util/fake_event.js +53 -7
  98. package/lib/util/fake_event_target.js +21 -1
  99. package/lib/util/mp4_box_parsers.js +1 -1
  100. package/lib/util/periods.js +4 -1
  101. package/lib/util/platform.js +13 -1
  102. package/lib/util/stream_utils.js +105 -13
  103. package/lib/util/xml_utils.js +50 -12
  104. package/package.json +3 -6
  105. package/roadmap.md +24 -22
  106. package/test/cast/cast_proxy_unit.js +12 -6
  107. package/test/cast/cast_receiver_integration.js +1 -1
  108. package/test/cast/cast_utils_unit.js +1 -1
  109. package/test/cea/mp4_cea_parser_unit.js +26 -0
  110. package/test/dash/dash_parser_live_unit.js +43 -0
  111. package/test/dash/dash_parser_manifest_unit.js +222 -3
  112. package/test/hls/hls_parser_unit.js +79 -1
  113. package/test/media/closed_caption_parser_unit.js +20 -0
  114. package/test/media/drm_engine_integration.js +86 -2
  115. package/test/media/drm_engine_unit.js +57 -0
  116. package/test/media/playhead_unit.js +27 -1
  117. package/test/media/region_observer_unit.js +40 -5
  118. package/test/media/region_timeline_unit.js +40 -22
  119. package/test/media/streaming_engine_integration.js +2 -1
  120. package/test/media/streaming_engine_unit.js +145 -0
  121. package/test/player_integration.js +79 -8
  122. package/test/player_src_equals_integration.js +41 -8
  123. package/test/player_unit.js +14 -4
  124. package/test/test/assets/3675/chunk-stream0-00001.m4s +0 -0
  125. package/test/test/assets/3675/chunk-stream0-00002.m4s +0 -0
  126. package/test/test/assets/3675/chunk-stream0-00003.m4s +0 -0
  127. package/test/test/assets/3675/chunk-stream0-00004.m4s +0 -0
  128. package/test/test/assets/3675/chunk-stream0-00005.m4s +0 -0
  129. package/test/test/assets/3675/chunk-stream0-00006.m4s +0 -0
  130. package/test/test/assets/3675/chunk-stream0-00007.m4s +0 -0
  131. package/test/test/assets/3675/dash_0.mpd +30 -0
  132. package/test/test/assets/3675/dash_1.mpd +30 -0
  133. package/test/test/assets/3675/dash_2.mpd +30 -0
  134. package/test/test/assets/3675/dash_3.mpd +30 -0
  135. package/test/test/assets/3675/dash_4.mpd +30 -0
  136. package/test/test/assets/3675/dash_5.mpd +26 -0
  137. package/test/test/assets/3675/init-stream0.m4s +0 -0
  138. package/test/test/assets/chapters.vtt +2 -2
  139. package/test/test/assets/chapters2.vtt +10 -0
  140. package/test/test/assets/empty_caption_video_init.mp4 +0 -0
  141. package/test/test/assets/empty_caption_video_segment.mp4 +0 -0
  142. package/test/test/util/fake_ad_manager.js +11 -1
  143. package/test/test/util/ttml_utils.js +81 -0
  144. package/test/test/util/util.js +10 -7
  145. package/test/text/mp4_ttml_parser_unit.js +11 -18
  146. package/test/text/ttml_text_parser_unit.js +542 -220
  147. package/test/text/ui_text_displayer_unit.js +93 -20
  148. package/test/text/vtt_text_parser_unit.js +17 -1
  149. package/test/text/web_vtt_generator_unit.js +58 -3
  150. package/test/ui/localization_unit.js +15 -14
  151. package/test/ui/text_displayer_layout_unit.js +21 -8
  152. package/test/util/mp4_box_parsers_unit.js +28 -0
  153. package/test/util/stream_utils_unit.js +60 -3
  154. package/ui/cast_button.js +2 -3
  155. package/ui/controls.js +11 -6
  156. package/ui/less/buttons.less +0 -4
  157. package/ui/less/containers.less +6 -7
  158. package/ui/locales/ru.json +6 -6
  159. package/ui/localization.js +40 -87
  160. package/ui/pip_button.js +2 -3
  161. package/ui/seek_bar.js +1 -2
  162. package/ui/text_selection.js +1 -0
@@ -73,7 +73,14 @@ shaka.text.UITextDisplayer = class {
73
73
  this.updateCaptions_();
74
74
  }).tickEvery(updatePeriod);
75
75
 
76
- /** private {Map.<!shaka.extern.Cue, !HTMLElement>} */
76
+ /**
77
+ * Maps cues to cue elements. Specifically points out the wrapper element of
78
+ * the cue (e.g. the HTML element to put nested cues inside).
79
+ * @private {Map.<!shaka.extern.Cue, !{
80
+ * cueElement: !HTMLElement,
81
+ * wrapper: !HTMLElement
82
+ * }>}
83
+ */
77
84
  this.currentCuesMap_ = new Map();
78
85
 
79
86
  /** @private {shaka.util.EventManager} */
@@ -160,9 +167,14 @@ shaka.text.UITextDisplayer = class {
160
167
  }
161
168
 
162
169
  // Remove the cues out of the time range.
170
+ const oldNumCues = this.cues_.length;
163
171
  this.cues_ = this.cues_.filter(
164
172
  (cue) => cue.startTime < start || cue.endTime >= end);
165
- 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);
166
178
 
167
179
  return true;
168
180
  }
@@ -185,68 +197,151 @@ shaka.text.UITextDisplayer = class {
185
197
  }
186
198
 
187
199
  /**
188
- * Display the current captions.
189
- * @param {boolean=} forceUpdate
200
+ * @param {!Array.<!shaka.extern.Cue>} cues
201
+ * @param {!HTMLElement} container
202
+ * @param {number} currentTime
203
+ * @param {!Array.<!shaka.extern.Cue>} parents
190
204
  * @private
191
205
  */
192
- updateCaptions_(forceUpdate = false) {
193
- const currentTime = this.video_.currentTime;
206
+ updateCuesRecursive_(cues, container, currentTime, parents) {
207
+ // Set to true if the cues have changed in some way, which will require
208
+ // DOM changes. E.g. if a cue was added or removed.
209
+ let updateDOM = false;
210
+ /**
211
+ * The elements to remove from the DOM.
212
+ * Some of these elements may be added back again, if their corresponding
213
+ * cue is in toPlant.
214
+ * These elements are only removed if updateDOM is true.
215
+ * @type {!Array.<!HTMLElement>}
216
+ */
217
+ const toUproot = [];
218
+ /**
219
+ * The cues whose corresponding elements should be in the DOM.
220
+ * Some of these might be new, some might have been displayed beforehand.
221
+ * These will only be added if updateDOM is true.
222
+ * @type {!Array.<!shaka.extern.Cue>}
223
+ */
224
+ const toPlant = [];
225
+ for (const cue of cues) {
226
+ parents.push(cue);
227
+
228
+ let cueRegistry = this.currentCuesMap_.get(cue);
229
+ const shouldBeDisplayed =
230
+ cue.startTime <= currentTime && cue.endTime > currentTime;
231
+ let wrapper = cueRegistry ? cueRegistry.wrapper : null;
232
+
233
+ if (cueRegistry) {
234
+ // If the cues are replanted, all existing cues should be uprooted,
235
+ // even ones which are going to be planted again.
236
+ toUproot.push(cueRegistry.cueElement);
237
+
238
+ // If the cue should not be displayed, remove it entirely.
239
+ if (!shouldBeDisplayed) {
240
+ // Since something has to be removed, we will need to update the DOM.
241
+ updateDOM = true;
242
+ this.currentCuesMap_.delete(cue);
243
+ cueRegistry = null;
244
+ }
245
+ }
194
246
 
195
- // Return true if the cue should be displayed at the current time point.
196
- const shouldCueBeDisplayed = (cue) => {
197
- return this.cues_.includes(cue) && this.isTextVisible_ &&
198
- cue.startTime <= currentTime && cue.endTime > currentTime;
199
- };
247
+ if (shouldBeDisplayed) {
248
+ toPlant.push(cue);
249
+ if (!cueRegistry) {
250
+ // The cue has to be made!
251
+ this.createCue_(cue, parents);
252
+ cueRegistry = this.currentCuesMap_.get(cue);
253
+ wrapper = cueRegistry.wrapper;
254
+ updateDOM = true;
255
+ }
256
+ }
200
257
 
201
- // For each cue in the current cues map, if the cue's end time has passed,
202
- // remove the entry from the map, and remove the captions from the page.
203
- for (const cue of this.currentCuesMap_.keys()) {
204
- if (!shouldCueBeDisplayed(cue) || forceUpdate) {
205
- const captions = this.currentCuesMap_.get(cue);
206
- this.textContainer_.removeChild(captions);
207
- this.currentCuesMap_.delete(cue);
258
+ // Recursively check the nested cues, to see if they need to be added or
259
+ // removed.
260
+ // If wrapper is null, that means that the cue is not only not being
261
+ // displayed currently, it also was not removed this tick. So it's
262
+ // guaranteed that the children will neither need to be added nor removed.
263
+ if (cue.nestedCues.length > 0 && wrapper) {
264
+ this.updateCuesRecursive_(
265
+ cue.nestedCues, wrapper, currentTime, parents);
208
266
  }
209
- }
210
267
 
211
- // Sometimes we don't remove a cue element correctly. So check all the
212
- // child nodes and remove any that don't have an associated cue.
213
- const expectedChildren = new Set(this.currentCuesMap_.values());
214
- for (const child of Array.from(this.textContainer_.childNodes)) {
215
- if (!expectedChildren.has(child)) {
216
- this.textContainer_.removeChild(child);
268
+ const topCue = parents.pop();
269
+ goog.asserts.assert(topCue == cue, 'Parent cues should be kept in order');
270
+ }
271
+ if (updateDOM) {
272
+ for (const cueElement of toUproot) {
273
+ container.removeChild(cueElement);
274
+ }
275
+ toPlant.sort((a, b) => {
276
+ if (a.startTime != b.startTime) {
277
+ return a.startTime - b.startTime;
278
+ } else {
279
+ return a.endTime - b.endTime;
280
+ }
281
+ });
282
+ for (const cue of toPlant) {
283
+ const cueRegistry = this.currentCuesMap_.get(cue);
284
+ goog.asserts.assert(cueRegistry, 'cueRegistry should exist.');
285
+ container.appendChild(cueRegistry.cueElement);
217
286
  }
218
287
  }
288
+ }
219
289
 
220
- // Get the current cues that should be added to display. If the cue is not
221
- // being displayed already, add it to the map, and add the captions onto the
222
- // page.
223
- const currentCues = this.cues_.filter((cue) => {
224
- return shouldCueBeDisplayed(cue) && !this.currentCuesMap_.has(cue);
225
- }).sort((a, b) => {
226
- if (a.startTime != b.startTime) {
227
- return a.startTime - b.startTime;
228
- } else {
229
- return a.endTime - b.endTime;
290
+ /**
291
+ * Display the current captions.
292
+ * @param {boolean=} forceUpdate
293
+ * @private
294
+ */
295
+ updateCaptions_(forceUpdate = false) {
296
+ if (!this.textContainer_) {
297
+ return;
298
+ }
299
+
300
+ const currentTime = this.video_.currentTime;
301
+ 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();
306
+ }
307
+ }
308
+ if (this.isTextVisible_) {
309
+ // Log currently attached cue elements for verification, later.
310
+ const previousCuesMap = new Map();
311
+ for (const cue of this.currentCuesMap_.keys()) {
312
+ previousCuesMap.set(cue, this.currentCuesMap_.get(cue));
230
313
  }
231
- });
232
314
 
233
- for (const cue of currentCues) {
234
- const cueElement = this.displayCue_(
235
- this.textContainer_, cue, /* isNested= */ false);
236
- this.currentCuesMap_.set(cue, cueElement);
315
+ // Update the cues.
316
+ this.updateCuesRecursive_(
317
+ this.cues_, this.textContainer_, currentTime, /* parents= */ []);
318
+
319
+ if (goog.DEBUG) {
320
+ // Previously, we had an issue (#2076) where cues sometimes were not
321
+ // properly removed from the DOM. It is not clear if this issue still
322
+ // happens, so the previous fix for it has been changed to an assert.
323
+ for (const cue of previousCuesMap.keys()) {
324
+ if (!this.currentCuesMap_.has(cue)) {
325
+ // TODO: If the problem does not appear again, then we should remove
326
+ // this assert (and the previousCuesMap code) in Shaka v4.
327
+ const cueElement = previousCuesMap.get(cue).cueElement;
328
+ goog.asserts.assert(
329
+ !cueElement.parentNode, 'Cue was not properly removed!');
330
+ }
331
+ }
332
+ }
237
333
  }
238
334
  }
239
335
 
240
336
  /**
241
- * Displays a cue
337
+ * Creates the object for a cue.
242
338
  *
243
- * @param {Element} container
244
339
  * @param {!shaka.extern.Cue} cue
245
- * @param {boolean} isNested
246
- * @return {!Element} the created captions element
340
+ * @param {!Array.<!shaka.extern.Cue>} parents
247
341
  * @private
248
342
  */
249
- displayCue_(container, cue, isNested) {
343
+ createCue_(cue, parents) {
344
+ const isNested = parents.length > 1;
250
345
  let type = isNested ? 'span' : 'div';
251
346
  if (cue.lineBreak || cue.spacer) {
252
347
  if (cue.spacer) {
@@ -257,40 +352,42 @@ shaka.text.UITextDisplayer = class {
257
352
  type = 'br';
258
353
  }
259
354
 
355
+ const needWrapper = !isNested && cue.nestedCues.length > 0;
356
+
260
357
  // Nested cues are inline elements. Top-level cues are block elements.
261
358
  const cueElement = shaka.util.Dom.createHTMLElement(type);
262
359
  if (type != 'br') {
263
- this.setCaptionStyles_(cueElement, cue, isNested);
360
+ this.setCaptionStyles_(cueElement, cue, parents, needWrapper);
264
361
  }
265
362
 
266
363
  let wrapper = cueElement;
267
- if (!isNested && cue.nestedCues.length) {
364
+ if (needWrapper) {
268
365
  // Create a wrapper element which will serve to contain all children into
269
366
  // a single item. This ensures that nested span elements appear
270
367
  // horizontally and br elements occupy no vertical space.
271
368
  wrapper = shaka.util.Dom.createHTMLElement('span');
272
369
  wrapper.classList.add('shaka-text-wrapper');
370
+ wrapper.style.backgroundColor = cue.backgroundColor;
273
371
  cueElement.appendChild(wrapper);
274
372
  }
275
373
 
276
- for (const nestedCue of cue.nestedCues) {
277
- this.displayCue_(wrapper, nestedCue, /* isNested= */ true);
278
- }
279
-
280
- container.appendChild(cueElement);
281
- return cueElement;
374
+ this.currentCuesMap_.set(cue, {cueElement, wrapper});
282
375
  }
283
376
 
284
377
  /**
285
378
  * @param {!HTMLElement} cueElement
286
379
  * @param {!shaka.extern.Cue} cue
287
- * @param {boolean} isNested
380
+ * @param {!Array.<!shaka.extern.Cue>} parents
381
+ * @param {boolean} hasWrapper
288
382
  * @private
289
383
  */
290
- setCaptionStyles_(cueElement, cue, isNested) {
384
+ setCaptionStyles_(cueElement, cue, parents, hasWrapper) {
291
385
  const Cue = shaka.text.Cue;
292
- let style = cueElement.style;
386
+ const inherit =
387
+ (cb) => shaka.text.UITextDisplayer.inheritProperty_(parents, cb);
388
+ const style = cueElement.style;
293
389
  const isLeaf = cue.nestedCues.length == 0;
390
+ const isNested = parents.length > 1;
294
391
 
295
392
  // TODO: wrapLine is not yet supported. Lines always wrap.
296
393
 
@@ -309,16 +406,6 @@ shaka.text.UITextDisplayer = class {
309
406
  const nonBreakingSpace = '\xa0';
310
407
  return nonBreakingSpace.repeat(match.length);
311
408
  });
312
- if (isNested) {
313
- cueElement.textContent = text;
314
- } else if (text.length) {
315
- // If a top-level cue has text, move to a <span> so the background is
316
- // styled correctly.
317
- const span = shaka.util.Dom.createHTMLElement('span');
318
- span.textContent = text;
319
- cueElement.appendChild(span);
320
- style = span.style;
321
- }
322
409
 
323
410
  style.backgroundColor = cue.backgroundColor;
324
411
  style.border = cue.border;
@@ -327,27 +414,49 @@ shaka.text.UITextDisplayer = class {
327
414
  style.opacity = cue.opacity;
328
415
  style.paddingLeft = shaka.text.UITextDisplayer.convertLengthValue_(
329
416
  cue.linePadding, cue, this.videoContainer_);
330
- style.paddingRight = shaka.text.UITextDisplayer.convertLengthValue_(
331
- cue.linePadding, cue, this.videoContainer_);
417
+ style.paddingRight =
418
+ shaka.text.UITextDisplayer.convertLengthValue_(
419
+ cue.linePadding, cue, this.videoContainer_);
332
420
 
333
421
  if (cue.backgroundImage) {
334
422
  style.backgroundImage = 'url(\'' + cue.backgroundImage + '\')';
335
423
  style.backgroundRepeat = 'no-repeat';
336
424
  style.backgroundSize = 'contain';
337
425
  style.backgroundPosition = 'center';
426
+ } else {
427
+ // If we have both text and nested cues, then style everything; otherwise
428
+ // place the text in its own <span> so the background doesn't fill the
429
+ // whole region.
430
+ let elem;
431
+ if (cue.nestedCues.length) {
432
+ elem = cueElement;
433
+ } else {
434
+ elem = shaka.util.Dom.createHTMLElement('span');
435
+ cueElement.appendChild(elem);
436
+ }
338
437
 
339
- if (cue.backgroundColor == '') {
340
- // In text-based cues, background color can default in CSS.
341
- // In bitmap-based cues, we default to a transparent background color,
342
- // so that the bitmap can be the only background.
343
- style.backgroundColor = 'transparent';
438
+ if (cue.border) {
439
+ elem.style.border = cue.border;
440
+ }
441
+ if (!hasWrapper) {
442
+ const bgColor = inherit((c) => c.backgroundColor);
443
+ if (bgColor) {
444
+ elem.style.backgroundColor = bgColor;
445
+ } else if (text) {
446
+ // If there is no background, default to a semi-transparent black.
447
+ // Only do this for the text itself.
448
+ elem.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
449
+ }
450
+ }
451
+ if (text) {
452
+ elem.textContent = text;
344
453
  }
345
454
  }
346
455
 
347
456
  // The displayAlign attribute specifies the vertical alignment of the
348
457
  // captions inside the text container. Before means at the top of the
349
458
  // text container, and after means at the bottom.
350
- if (isNested) {
459
+ if (isNested && !parents[parents.length - 1].isContainer) {
351
460
  style.display = 'inline';
352
461
  } else {
353
462
  style.display = 'flex';
@@ -393,20 +502,28 @@ shaka.text.UITextDisplayer = class {
393
502
  // TODO: Implement lineAlignment of 'CENTER'.
394
503
  if (cue.line) {
395
504
  if (cue.lineInterpretation == Cue.lineInterpretation.PERCENTAGE) {
505
+ // When setting absolute positioning, you need to set x/y/width/height
506
+ // so the element is positioned correctly. Set these as default and
507
+ // other settings will override them.
396
508
  style.position = 'absolute';
509
+ style.left = '0';
510
+ style.top = '0';
397
511
  if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
512
+ style.width = '100%';
398
513
  if (cue.lineAlign == Cue.lineAlign.START) {
399
514
  style.top = cue.line + '%';
400
515
  } else if (cue.lineAlign == Cue.lineAlign.END) {
401
516
  style.bottom = cue.line + '%';
402
517
  }
403
518
  } else if (cue.writingMode == Cue.writingMode.VERTICAL_LEFT_TO_RIGHT) {
519
+ style.height = '100%';
404
520
  if (cue.lineAlign == Cue.lineAlign.START) {
405
521
  style.left = cue.line + '%';
406
522
  } else if (cue.lineAlign == Cue.lineAlign.END) {
407
523
  style.right = cue.line + '%';
408
524
  }
409
525
  } else {
526
+ style.height = '100%';
410
527
  if (cue.lineAlign == Cue.lineAlign.START) {
411
528
  style.right = cue.line + '%';
412
529
  } else if (cue.lineAlign == Cue.lineAlign.END) {
@@ -414,12 +531,7 @@ shaka.text.UITextDisplayer = class {
414
531
  }
415
532
  }
416
533
  }
417
- } else if (cue.region && cue.region.id &&
418
- ((!isNested && !isLeaf) || (cue.backgroundImage))) {
419
- // In text-base cues, regions are only applied to block container
420
- // (!isNested && !isLeaf).
421
- // In bitmap-based cues, region settings are used to specify the size and
422
- // position of the backgroundImage.
534
+ } else if (cue.region && cue.region.id) {
423
535
  const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
424
536
  const heightUnit = cue.region.heightUnits == percentageUnit ? '%' : 'px';
425
537
  const widthUnit = cue.region.widthUnits == percentageUnit ? '%' : 'px';
@@ -552,4 +664,25 @@ shaka.text.UITextDisplayer = class {
552
664
 
553
665
  return (containerHeight * value / cue.cellResolution.rows) + 'px';
554
666
  }
667
+
668
+ /**
669
+ * Inherits a property from the parent Cue elements. If the value is falsy,
670
+ * it is assumed to be inherited from the parent. This returns null if the
671
+ * value isn't found.
672
+ *
673
+ * @param {!Array.<!shaka.extern.Cue>} parents
674
+ * @param {function(!shaka.extern.Cue):?T} cb
675
+ * @return {?T}
676
+ * @template T
677
+ * @private
678
+ */
679
+ static inheritProperty_(parents, cb) {
680
+ for (let i = parents.length - 1; i >= 0; i--) {
681
+ const val = cb(parents[i]);
682
+ if (val || val === 0) {
683
+ return val;
684
+ }
685
+ }
686
+ return null;
687
+ }
555
688
  };
@@ -122,7 +122,7 @@ shaka.text.VttTextParser = class {
122
122
  }
123
123
 
124
124
  /**
125
- * Add deafult color
125
+ * Add default color
126
126
  *
127
127
  * @param {!Map.<string, shaka.text.Cue>} styles
128
128
  * @private
@@ -436,7 +436,9 @@ shaka.text.VttTextParser = class {
436
436
  }
437
437
  const tagEnd = payload.substring(i + 1, end);
438
438
  const tagStart = names.pop();
439
- if (tagStart === tagEnd) {
439
+ if (!tagEnd || !tagStart) {
440
+ return payload;
441
+ } else if (tagStart === tagEnd) {
440
442
  newPayload += '/' + tagEnd + '>';
441
443
  i += tagEnd.length + 1;
442
444
  } else {
@@ -17,9 +17,10 @@ goog.require('shaka.text.Cue');
17
17
  shaka.text.WebVttGenerator = class {
18
18
  /**
19
19
  * @param {!Array.<!shaka.text.Cue>} cues
20
+ * @param {!Array.<!shaka.extern.AdCuePoint>} adCuePoints
20
21
  * @return {string}
21
22
  */
22
- static convert(cues) {
23
+ static convert(cues, adCuePoints) {
23
24
  // Flatten nested cue payloads recursively. If a cue has nested cues,
24
25
  // their contents should be combined and replace the payload of the parent.
25
26
  const flattenPayload = (cue) => {
@@ -64,6 +65,25 @@ shaka.text.WebVttGenerator = class {
64
65
  }
65
66
  };
66
67
 
68
+ const webvttTimeString = (time) => {
69
+ let newTime = time;
70
+ for (const adCuePoint of adCuePoints) {
71
+ if (adCuePoint.end && adCuePoint.start < time) {
72
+ const offset = adCuePoint.end - adCuePoint.start;
73
+ newTime += offset;
74
+ }
75
+ }
76
+ const hours = Math.floor(newTime / 3600);
77
+ const minutes = Math.floor(newTime / 60 % 60);
78
+ const seconds = Math.floor(newTime % 60);
79
+ const milliseconds = Math.floor(newTime * 1000 % 1000);
80
+ return (hours < 10 ? '0' : '') + hours + ':' +
81
+ (minutes < 10 ? '0' : '') + minutes + ':' +
82
+ (seconds < 10 ? '0' : '') + seconds + '.' +
83
+ (milliseconds < 100 ? (milliseconds < 10 ? '00' : '0') : '') +
84
+ milliseconds;
85
+ };
86
+
67
87
  // We don't want to modify the array or objects passed in, since we don't
68
88
  // technically own them. So we build a new array and replace certain items
69
89
  // in it if they need to be flattened.
@@ -80,17 +100,6 @@ shaka.text.WebVttGenerator = class {
80
100
 
81
101
  let webvttString = 'WEBVTT\n\n';
82
102
  for (const cue of flattenedCues) {
83
- const webvttTimeString = (time) => {
84
- const hours = Math.floor(time / 3600);
85
- const minutes = Math.floor(time / 60 % 60);
86
- const seconds = Math.floor(time % 60);
87
- const milliseconds = Math.floor(time * 1000 % 1000);
88
- return (hours < 10 ? '0' : '') + hours + ':' +
89
- (minutes < 10 ? '0' : '') + minutes + ':' +
90
- (seconds < 10 ? '0' : '') + seconds + '.' +
91
- (milliseconds < 100 ? (milliseconds < 10 ? '00' : '0') : '') +
92
- milliseconds;
93
- };
94
103
  const webvttSettings = (cue) => {
95
104
  const settings = [];
96
105
  const Cue = shaka.text.Cue;
@@ -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.
@@ -9,6 +9,7 @@ goog.provide('shaka.util.FakeEventTarget');
9
9
  goog.require('goog.asserts');
10
10
  goog.require('shaka.log');
11
11
  goog.require('shaka.util.FakeEvent');
12
+ goog.require('shaka.util.IReleasable');
12
13
  goog.require('shaka.util.MultiMap');
13
14
 
14
15
 
@@ -18,13 +19,14 @@ goog.require('shaka.util.MultiMap');
18
19
  * to non-DOM classes. Only FakeEvents should be dispatched.
19
20
  *
20
21
  * @implements {EventTarget}
22
+ * @implements {shaka.util.IReleasable}
21
23
  * @exportInterface
22
24
  */
23
25
  shaka.util.FakeEventTarget = class {
24
26
  /** */
25
27
  constructor() {
26
28
  /**
27
- * @private {!shaka.util.MultiMap.<shaka.util.FakeEventTarget.ListenerType>}
29
+ * @private {shaka.util.MultiMap.<shaka.util.FakeEventTarget.ListenerType>}
28
30
  */
29
31
  this.listeners_ = new shaka.util.MultiMap();
30
32
 
@@ -46,6 +48,9 @@ shaka.util.FakeEventTarget = class {
46
48
  * @exportInterface
47
49
  */
48
50
  addEventListener(type, listener, options) {
51
+ if (!this.listeners_) {
52
+ return;
53
+ }
49
54
  this.listeners_.push(type, listener);
50
55
  }
51
56
 
@@ -73,6 +78,9 @@ shaka.util.FakeEventTarget = class {
73
78
  * @exportInterface
74
79
  */
75
80
  removeEventListener(type, listener, options) {
81
+ if (!this.listeners_) {
82
+ return;
83
+ }
76
84
  this.listeners_.remove(type, listener);
77
85
  }
78
86
 
@@ -90,6 +98,10 @@ shaka.util.FakeEventTarget = class {
90
98
  goog.asserts.assert(event instanceof shaka.util.FakeEvent,
91
99
  'FakeEventTarget can only dispatch FakeEvents!');
92
100
 
101
+ if (!this.listeners_) {
102
+ return true;
103
+ }
104
+
93
105
  let listeners = this.listeners_.get(event.type) || [];
94
106
  const universalListeners =
95
107
  this.listeners_.get(shaka.util.FakeEventTarget.ALL_EVENTS_);
@@ -129,6 +141,14 @@ shaka.util.FakeEventTarget = class {
129
141
 
130
142
  return event.defaultPrevented;
131
143
  }
144
+
145
+ /**
146
+ * @override
147
+ * @exportInterface
148
+ */
149
+ release() {
150
+ this.listeners_ = null;
151
+ }
132
152
  };
133
153
 
134
154
  /**
@@ -176,7 +176,7 @@ shaka.util.Mp4BoxParsers = class {
176
176
  if (version == 1) {
177
177
  reader.skip(8); // Skip "creation_time"
178
178
  reader.skip(8); // Skip "modification_time"
179
- trackId = reader.readUint64();
179
+ trackId = reader.readUint32();
180
180
  } else {
181
181
  reader.skip(4); // Skip "creation_time"
182
182
  reader.skip(4); // Skip "modification_time"
@@ -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>} */