shaka-player 3.3.15 → 3.3.17

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/selenium-lab-tests.yaml +22 -0
  2. package/CHANGELOG.md +27 -0
  3. package/dist/locales.js +31 -31
  4. package/dist/shaka-player.compiled.d.ts +5 -0
  5. package/dist/shaka-player.compiled.debug.d.ts +5 -0
  6. package/dist/shaka-player.compiled.debug.externs.js +4 -0
  7. package/dist/shaka-player.compiled.debug.js +99 -92
  8. package/dist/shaka-player.compiled.debug.map +3 -3
  9. package/dist/shaka-player.compiled.externs.js +4 -0
  10. package/dist/shaka-player.compiled.js +353 -350
  11. package/dist/shaka-player.compiled.map +3 -3
  12. package/dist/shaka-player.ui.d.ts +5 -0
  13. package/dist/shaka-player.ui.debug.d.ts +5 -0
  14. package/dist/shaka-player.ui.debug.externs.js +4 -0
  15. package/dist/shaka-player.ui.debug.js +113 -106
  16. package/dist/shaka-player.ui.debug.map +3 -3
  17. package/dist/shaka-player.ui.externs.js +4 -0
  18. package/dist/shaka-player.ui.js +393 -389
  19. package/dist/shaka-player.ui.map +3 -3
  20. package/externs/shaka/text.js +7 -0
  21. package/lib/dash/segment_template.js +17 -5
  22. package/lib/hls/hls_parser.js +1 -1
  23. package/lib/player.js +7 -1
  24. package/lib/polyfill/media_capabilities.js +1 -0
  25. package/lib/text/cue.js +6 -0
  26. package/lib/text/ui_text_displayer.js +25 -9
  27. package/lib/text/vtt_text_parser.js +256 -91
  28. package/lib/text/web_vtt_generator.js +21 -9
  29. package/lib/util/dom_utils.js +3 -1
  30. package/lib/util/platform.js +8 -0
  31. package/lib/util/stream_utils.js +6 -0
  32. package/lib/util/timer.js +17 -0
  33. package/package.json +1 -1
  34. package/support.html +1 -0
  35. package/test/dash/dash_parser_manifest_unit.js +0 -36
  36. package/test/player_integration.js +27 -0
  37. package/test/player_unit.js +4 -2
  38. package/test/test/util/layout_tests.js +433 -0
  39. package/test/test/util/util.js +0 -120
  40. package/test/{ui → text}/text_displayer_layout_unit.js +59 -235
  41. package/test/text/ui_text_displayer_unit.js +72 -4
  42. package/test/text/vtt_text_parser_unit.js +127 -6
  43. package/test/text/web_vtt_layout_integration.js +528 -0
@@ -297,6 +297,13 @@ shaka.extern.Cue = class {
297
297
  */
298
298
  this.fontFamily;
299
299
 
300
+ /**
301
+ * Text shadow color as a CSS text-shadow value.
302
+ * @type {string}
303
+ * @exportDoc
304
+ */
305
+ this.textShadow = '';
306
+
300
307
  /**
301
308
  * Text stroke color as a CSS color, e.g. "#FFFFFF" or "white".
302
309
  * @type {string}
@@ -102,11 +102,23 @@ shaka.dash.SegmentTemplate = class {
102
102
  const periodEnd = context.periodInfo.duration ?
103
103
  context.periodInfo.start + context.periodInfo.duration : Infinity;
104
104
 
105
- // Don't fit live content, since it might receive more segments.
106
- // Unless that live content is multi-period; it's safe to fit every period
107
- // but the last one, since only the last period might receive new
108
- // segments.
109
- const shouldFit = periodEnd != Infinity;
105
+ /* When to fit segments. All refactors should honor/update this table:
106
+ *
107
+ * | dynamic | infinite | last | should | notes |
108
+ * | | period | period | fit | |
109
+ * | ------- | -------- | ------ | ------ | ------------------------- |
110
+ * | F | F | X | T | typical VOD |
111
+ * | F | T | X | X | impossible: infinite VOD |
112
+ * | T | F | F | T | typical live, old period |
113
+ * | T | F | T | F | typical IPR |
114
+ * | T | T | F | X | impossible: old, infinite |
115
+ * | T | T | T | F | typical live, new period |
116
+ */
117
+
118
+ // We never fit the final period of dynamic content, which could be
119
+ // infinite live (with no limit to fit to) or IPR (which would expand the
120
+ // most recent segment to the end of the presentation).
121
+ const shouldFit = !(context.dynamic && context.periodInfo.isLastPeriod);
110
122
 
111
123
  if (segmentIndex) {
112
124
  if (shouldFit) {
@@ -3041,7 +3041,7 @@ shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
3041
3041
  'm4f': 'application/mp4',
3042
3042
  'cmft': 'application/mp4',
3043
3043
  'vtt': 'text/vtt',
3044
- 'webvtt': 'txt/vtt',
3044
+ 'webvtt': 'text/vtt',
3045
3045
  'ttml': 'application/ttml+xml',
3046
3046
  };
3047
3047
 
package/lib/player.js CHANGED
@@ -1419,6 +1419,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1419
1419
  has.startTime = null;
1420
1420
  has.uri = null;
1421
1421
 
1422
+ // Release the region timeline, which is created when parsing the manifest.
1423
+ if (this.regionTimeline_) {
1424
+ this.regionTimeline_.release();
1425
+ this.regionTimeline_ = null;
1426
+ }
1427
+
1422
1428
  // In most cases we should have a media element. The one exception would
1423
1429
  // be if there was an error and we, by chance, did not have a media element.
1424
1430
  if (has.mediaElement) {
@@ -6657,7 +6663,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6657
6663
  * @define {string} A version number taken from git at compile time.
6658
6664
  * @export
6659
6665
  */
6660
- shaka.Player.version = 'v3.3.15-uncompiled';
6666
+ shaka.Player.version = 'v3.3.17-uncompiled';
6661
6667
 
6662
6668
  // Initialize the deprecation system using the version string we just set
6663
6669
  // on the player.
@@ -45,6 +45,7 @@ shaka.polyfill.MediaCapabilities = class {
45
45
  !shaka.util.Platform.isWebOS() &&
46
46
  !shaka.util.Platform.isTizen() &&
47
47
  !shaka.util.Platform.isEOS() &&
48
+ !shaka.util.Platform.isHisense() &&
48
49
  navigator.mediaCapabilities) {
49
50
  shaka.log.info(
50
51
  'MediaCapabilities: Native mediaCapabilities support found.');
package/lib/text/cue.js CHANGED
@@ -139,6 +139,12 @@ shaka.text.Cue = class {
139
139
  */
140
140
  this.border = '';
141
141
 
142
+ /**
143
+ * @override
144
+ * @exportInterface
145
+ */
146
+ this.textShadow = '';
147
+
142
148
  /**
143
149
  * @override
144
150
  * @exportInterface
@@ -372,6 +372,25 @@ shaka.text.UITextDisplayer = class {
372
372
  }
373
373
  }
374
374
 
375
+ /**
376
+ * Compute a unique internal id:
377
+ * Regions can reuse the id but have different dimensions, we need to
378
+ * consider those differences
379
+ * @param {shaka.extern.CueRegion} region
380
+ * @private
381
+ */
382
+ generateRegionId_(region) {
383
+ const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
384
+ const heightUnit = region.heightUnits == percentageUnit ? '%' : 'px';
385
+ const viewportAnchorUnit =
386
+ region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
387
+ const uniqueRegionId = `${region.id}_${
388
+ region.width}x${region.height}${heightUnit}-${
389
+ region.viewportAnchorX}x${region.viewportAnchorY}${viewportAnchorUnit}`;
390
+
391
+ return uniqueRegionId;
392
+ }
393
+
375
394
  /**
376
395
  * Get or create a region element corresponding to the cue region. These are
377
396
  * cached by ID.
@@ -383,8 +402,9 @@ shaka.text.UITextDisplayer = class {
383
402
  getRegionElement_(cue) {
384
403
  const region = cue.region;
385
404
 
386
- if (this.regionElements_.has(region.id)) {
387
- return this.regionElements_.get(region.id);
405
+ const regionId = this.generateRegionId_(region);
406
+ if (this.regionElements_.has(regionId)) {
407
+ return this.regionElements_.get(regionId);
388
408
  }
389
409
 
390
410
  const regionElement = shaka.util.Dom.createHTMLElement('span');
@@ -395,7 +415,7 @@ shaka.text.UITextDisplayer = class {
395
415
  const viewportAnchorUnit =
396
416
  region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
397
417
 
398
- regionElement.id = 'shaka-text-region---' + region.id;
418
+ regionElement.id = 'shaka-text-region---' + regionId;
399
419
  regionElement.classList.add('shaka-text-region');
400
420
 
401
421
  regionElement.style.height = region.height + heightUnit;
@@ -416,7 +436,7 @@ shaka.text.UITextDisplayer = class {
416
436
  regionElement.style.justifyContent = 'flex-end';
417
437
  }
418
438
 
419
- this.regionElements_.set(region.id, regionElement);
439
+ this.regionElements_.set(regionId, regionElement);
420
440
  return regionElement;
421
441
  }
422
442
 
@@ -509,6 +529,7 @@ shaka.text.UITextDisplayer = class {
509
529
  style.paddingRight =
510
530
  shaka.text.UITextDisplayer.convertLengthValue_(
511
531
  cue.linePadding, cue, this.videoContainer_);
532
+ style.textShadow = cue.textShadow;
512
533
 
513
534
  if (cue.backgroundImage) {
514
535
  style.backgroundImage = 'url(\'' + cue.backgroundImage + '\')';
@@ -602,12 +623,7 @@ shaka.text.UITextDisplayer = class {
602
623
  // TODO: Implement lineAlignment of 'CENTER'.
603
624
  if (cue.line != null) {
604
625
  if (cue.lineInterpretation == Cue.lineInterpretation.PERCENTAGE) {
605
- // When setting absolute positioning, you need to set x/y/width/height
606
- // so the element is positioned correctly. Set these as default and
607
- // other settings will override them.
608
626
  style.position = 'absolute';
609
- style.left = '0';
610
- style.top = '0';
611
627
  if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
612
628
  style.width = '100%';
613
629
  if (cue.lineAlign == Cue.lineAlign.START) {
@@ -132,14 +132,14 @@ shaka.text.VttTextParser = class {
132
132
  for (const [key, value] of Object.entries(textColor)) {
133
133
  const cue = new shaka.text.Cue(0, 0, '');
134
134
  cue.color = value;
135
- styles.set(key, cue);
135
+ styles.set('.' + key, cue);
136
136
  }
137
137
 
138
138
  const bgColor = shaka.text.Cue.defaultTextBackgroundColor;
139
139
  for (const [key, value] of Object.entries(bgColor)) {
140
140
  const cue = new shaka.text.Cue(0, 0, '');
141
141
  cue.backgroundColor = value;
142
- styles.set(key, cue);
142
+ styles.set('.' + key, cue);
143
143
  }
144
144
  }
145
145
 
@@ -199,92 +199,121 @@ shaka.text.VttTextParser = class {
199
199
  return;
200
200
  }
201
201
 
202
- if (!text[1].includes('::cue')) {
203
- return;
204
- }
205
- let styleSelector = 'global';
206
- // Look for what is within parentisesis. For example:
207
- // <code>:: cue (b) {</code>, what we are looking for is <code>b</code>
208
- const selector = text[1].match(/\((.*)\)/);
209
- if (selector) {
210
- styleSelector = selector.pop();
202
+ /** @type {!Array.<!Array.<string>>} */
203
+ const styleBlocks = [];
204
+ let lastBlockIndex = -1;
205
+ for (let i = 1; i < text.length; i++) {
206
+ if (text[i].includes('::cue')) {
207
+ styleBlocks.push([]);
208
+ lastBlockIndex = styleBlocks.length - 1;
209
+ }
210
+ if (lastBlockIndex == -1) {
211
+ continue;
212
+ }
213
+ styleBlocks[lastBlockIndex].push(text[i]);
214
+ if (text[i].includes('}')) {
215
+ lastBlockIndex = -1;
216
+ }
211
217
  }
212
218
 
213
- // We start at 2 to avoid '::cue' and end earlier to avoid '}'
214
- let propertyLines = text.slice(2, -1);
215
- if (text[1].includes('}')) {
216
- const payload = /\{(.*?)\}/.exec(text[1]);
217
- if (payload) {
218
- propertyLines = payload[1].split(';');
219
+ for (const styleBlock of styleBlocks) {
220
+ let styleSelector = 'global';
221
+ // Look for what is within parentheses. For example:
222
+ // <code>:: cue (b) {</code>, what we are looking for is <code>b</code>
223
+ const selector = styleBlock[0].match(/\((.*)\)/);
224
+ if (selector) {
225
+ styleSelector = selector.pop();
219
226
  }
220
- }
221
227
 
222
- const cue = new shaka.text.Cue(0, 0, '');
223
- let validStyle = false;
224
- for (let i = 0; i < propertyLines.length; i++) {
225
- // We look for CSS properties. As a general rule they are separated by
226
- // <code>:</code>. Eg: <code>color: red;</code>
227
- const lineParts = /^\s*([^:]+):\s*(.*)/.exec(propertyLines[i]);
228
- if (lineParts) {
229
- const name = lineParts[1].trim();
230
- const value = lineParts[2].trim().replace(';', '');
231
- switch (name) {
232
- case 'background-color':
233
- validStyle = true;
234
- cue.backgroundColor = value;
235
- break;
236
- case 'color':
237
- validStyle = true;
238
- cue.color = value;
239
- break;
240
- case 'font-family':
241
- validStyle = true;
242
- cue.fontFamily = value;
243
- break;
244
- case 'font-size':
245
- validStyle = true;
246
- cue.fontSize = value;
247
- break;
248
- case 'font-weight':
249
- if (parseInt(value, 10) >= 700) {
228
+ // We start at 1 to avoid '::cue' and end earlier to avoid '}'
229
+ let propertyLines = styleBlock.slice(1, -1);
230
+ if (styleBlock[0].includes('}')) {
231
+ const payload = /\{(.*?)\}/.exec(styleBlock[0]);
232
+ if (payload) {
233
+ propertyLines = payload[1].split(';');
234
+ }
235
+ }
236
+
237
+ // Continue styles over multiple selectors if necessary.
238
+ // For example,
239
+ // ::cue(b) { background: white; } ::cue(b) { color: blue; }
240
+ // should set both the background and foreground of bold tags.
241
+ let cue = styles.get(styleSelector);
242
+ if (!cue) {
243
+ cue = new shaka.text.Cue(0, 0, '');
244
+ }
245
+
246
+ let validStyle = false;
247
+ for (let i = 0; i < propertyLines.length; i++) {
248
+ // We look for CSS properties. As a general rule they are separated by
249
+ // <code>:</code>. Eg: <code>color: red;</code>
250
+ const lineParts = /^\s*([^:]+):\s*(.*)/.exec(propertyLines[i]);
251
+ if (lineParts) {
252
+ const name = lineParts[1].trim();
253
+ const value = lineParts[2].trim().replace(';', '');
254
+ switch (name) {
255
+ case 'background-color':
256
+ case 'background':
250
257
  validStyle = true;
251
- cue.fontWeight = shaka.text.Cue.fontWeight.BOLD;
252
- }
253
- break;
254
- case 'font-style':
255
- switch (value) {
256
- case 'normal':
257
- validStyle = true;
258
- cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL;
259
- break;
260
- case 'italic':
261
- validStyle = true;
262
- cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
263
- break;
264
- case 'oblique':
258
+ cue.backgroundColor = value;
259
+ break;
260
+ case 'color':
261
+ validStyle = true;
262
+ cue.color = value;
263
+ break;
264
+ case 'font-family':
265
+ validStyle = true;
266
+ cue.fontFamily = value;
267
+ break;
268
+ case 'font-size':
269
+ validStyle = true;
270
+ cue.fontSize = value;
271
+ break;
272
+ case 'font-weight':
273
+ if (parseInt(value, 10) >= 700 || value == 'bold') {
265
274
  validStyle = true;
266
- cue.fontStyle = shaka.text.Cue.fontStyle.OBLIQUE;
267
- break;
268
- }
269
- break;
270
- case 'opacity':
271
- validStyle = true;
272
- cue.opacity = parseFloat(value);
273
- break;
274
- case 'white-space':
275
- validStyle = true;
276
- cue.wrapLine = value != 'noWrap';
277
- break;
278
- default:
279
- shaka.log.warning('VTT parser encountered an unsupported style: ',
280
- lineParts);
281
- break;
275
+ cue.fontWeight = shaka.text.Cue.fontWeight.BOLD;
276
+ }
277
+ break;
278
+ case 'font-style':
279
+ switch (value) {
280
+ case 'normal':
281
+ validStyle = true;
282
+ cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL;
283
+ break;
284
+ case 'italic':
285
+ validStyle = true;
286
+ cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
287
+ break;
288
+ case 'oblique':
289
+ validStyle = true;
290
+ cue.fontStyle = shaka.text.Cue.fontStyle.OBLIQUE;
291
+ break;
292
+ }
293
+ break;
294
+ case 'opacity':
295
+ validStyle = true;
296
+ cue.opacity = parseFloat(value);
297
+ break;
298
+ case 'text-shadow':
299
+ validStyle = true;
300
+ cue.textShadow = value;
301
+ break;
302
+ case 'white-space':
303
+ validStyle = true;
304
+ cue.wrapLine = value != 'noWrap';
305
+ break;
306
+ default:
307
+ shaka.log.warning('VTT parser encountered an unsupported style: ',
308
+ lineParts);
309
+ break;
310
+ }
282
311
  }
283
312
  }
284
- }
285
313
 
286
- if (validStyle) {
287
- styles.set(styleSelector, cue);
314
+ if (validStyle) {
315
+ styles.set(styleSelector, cue);
316
+ }
288
317
  }
289
318
  }
290
319
 
@@ -311,8 +340,8 @@ shaka.text.VttTextParser = class {
311
340
  return null;
312
341
  }
313
342
 
314
- // Skip style blocks.
315
- if (text[0] == 'STYLE') {
343
+ // Skip style and region blocks.
344
+ if (text[0] == 'STYLE' || text[0] == 'REGION') {
316
345
  return null;
317
346
  }
318
347
 
@@ -340,7 +369,8 @@ shaka.text.VttTextParser = class {
340
369
  end += timeOffset;
341
370
 
342
371
  // Get the payload.
343
- const payload = text.slice(1).join('\n').trim();
372
+ const payload = VttTextParser.htmlUnescape_(
373
+ text.slice(1).join('\n').trim());
344
374
 
345
375
  let cue = null;
346
376
  if (styles.has('global')) {
@@ -386,6 +416,7 @@ shaka.text.VttTextParser = class {
386
416
  VttTextParser.addDefaultTextColor_(styles);
387
417
  }
388
418
  payload = VttTextParser.replaceColorPayload_(payload);
419
+ payload = VttTextParser.replaceVoiceStylePayload_(payload);
389
420
  const xmlPayload = '<span>' + payload + '</span>';
390
421
  const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span');
391
422
  if (element) {
@@ -411,6 +442,79 @@ shaka.text.VttTextParser = class {
411
442
  }
412
443
  }
413
444
 
445
+ /**
446
+ * Converts voice style tag to be valid for xml parsing
447
+ * For example,
448
+ * input: <v Shaka>Test
449
+ * output: <v.voice-Shaka>Test</v.voice-Shaka>
450
+ *
451
+ * @param {string} payload
452
+ * @return {string} processed payload
453
+ * @private
454
+ */
455
+ static replaceVoiceStylePayload_(payload) {
456
+ const voiceTag = 'v';
457
+ const names = [];
458
+ let nameStart = -1;
459
+ let newPayload = '';
460
+ let hasVoiceEndTag = false;
461
+ for (let i = 0; i < payload.length; i++) {
462
+ // This condition is used to manage tags that have end tags.
463
+ if (payload[i] === '/') {
464
+ const end = payload.indexOf('>', i);
465
+ if (end === -1) {
466
+ return payload;
467
+ }
468
+ const tagEnd = payload.substring(i + 1, end);
469
+ if (!tagEnd || tagEnd != voiceTag) {
470
+ newPayload += payload[i];
471
+ continue;
472
+ }
473
+ hasVoiceEndTag = true;
474
+ let tagStart = null;
475
+ if (names.length) {
476
+ tagStart = names[names.length -1];
477
+ }
478
+ if (!tagStart) {
479
+ newPayload += payload[i];
480
+ } else if (tagStart === tagEnd) {
481
+ newPayload += '/' + tagEnd + '>';
482
+ i += tagEnd.length + 1;
483
+ } else {
484
+ if (!tagStart.startsWith(voiceTag)) {
485
+ newPayload += payload[i];
486
+ continue;
487
+ }
488
+ newPayload += '/' + tagStart + '>';
489
+ i += tagEnd.length + 1;
490
+ }
491
+ } else {
492
+ // Here we only want the tag name, not any other payload.
493
+ if (payload[i] === '<') {
494
+ nameStart = i + 1;
495
+ if (payload[nameStart] != voiceTag) {
496
+ nameStart = -1;
497
+ }
498
+ } else if (payload[i] === '>') {
499
+ if (nameStart > 0) {
500
+ names.push(payload.substr(nameStart, i - nameStart));
501
+ nameStart = -1;
502
+ }
503
+ }
504
+ newPayload += payload[i];
505
+ }
506
+ }
507
+ for (const name of names) {
508
+ const newName = name.replace(' ', '.voice-');
509
+ newPayload = newPayload.replace(`<${name}>`, `<${newName}>`);
510
+ newPayload = newPayload.replace(`</${name}>`, `</${newName}>`);
511
+ if (!hasVoiceEndTag) {
512
+ newPayload += `</${newName}>`;
513
+ }
514
+ }
515
+ return newPayload;
516
+ }
517
+
414
518
  /**
415
519
  * Converts color end tag to be valid for xml parsing
416
520
  * For example,
@@ -429,28 +533,36 @@ shaka.text.VttTextParser = class {
429
533
  let nameStart = -1;
430
534
  let newPayload = '';
431
535
  for (let i = 0; i < payload.length; i++) {
432
- if (payload[i] === '/') {
536
+ if (payload[i] === '/' && i > 0 && payload[i - 1] === '<') {
433
537
  const end = payload.indexOf('>', i);
434
538
  if (end <= i) {
435
539
  return payload;
436
540
  }
437
541
  const tagEnd = payload.substring(i + 1, end);
542
+ if (!tagEnd || tagEnd !== 'c') {
543
+ newPayload += payload[i];
544
+ continue;
545
+ }
438
546
  const tagStart = names.pop();
439
- if (!tagEnd || !tagStart) {
440
- return payload;
547
+ if (!tagStart) {
548
+ newPayload += payload[i];
441
549
  } else if (tagStart === tagEnd) {
442
550
  newPayload += '/' + tagEnd + '>';
443
551
  i += tagEnd.length + 1;
444
552
  } else {
445
- if (!tagStart.startsWith('c.') || tagEnd !== 'c') {
446
- return payload;
553
+ if (!tagStart.startsWith('c.')) {
554
+ newPayload += payload[i];
555
+ continue;
447
556
  }
448
- newPayload += '/' + tagStart + '>';
449
557
  i += tagEnd.length + 1;
558
+ newPayload += '/' + tagStart + '>';
450
559
  }
451
560
  } else {
452
561
  if (payload[i] === '<') {
453
562
  nameStart = i + 1;
563
+ if (payload[nameStart] != 'c') {
564
+ nameStart = -1;
565
+ }
454
566
  } else if (payload[i] === '>') {
455
567
  if (nameStart > 0) {
456
568
  names.push(payload.substr(nameStart, i - nameStart));
@@ -519,12 +631,29 @@ shaka.text.VttTextParser = class {
519
631
  const bold = shaka.text.Cue.fontWeight.BOLD;
520
632
  const italic = shaka.text.Cue.fontStyle.ITALIC;
521
633
  const underline = shaka.text.Cue.textDecoration.UNDERLINE;
522
- const tags = element.nodeName.split(/[ .]+/);
634
+ const tags = element.nodeName.split(/(?=[ .])+/g);
523
635
  for (const tag of tags) {
524
- if (styles.has(tag)) {
525
- VttTextParser.mergeStyle_(nestedCue, styles.get(tag));
636
+ let styleTag = tag;
637
+ // White blanks at start indicate that the style is a voice
638
+ if (styleTag.startsWith('.voice-')) {
639
+ const voice = styleTag.split('-').pop();
640
+ styleTag = `v[voice="${voice}"]`;
641
+ // The specification allows to have quotes and not, so we check to
642
+ // see which one is being used.
643
+ if (!styles.has(styleTag)) {
644
+ styleTag = `v[voice=${voice}]`;
645
+ }
646
+ }
647
+ if (styles.has(styleTag)) {
648
+ VttTextParser.mergeStyle_(nestedCue, styles.get(styleTag));
526
649
  }
527
650
  switch (tag) {
651
+ case 'br': {
652
+ const lineBreakCue = rootCue.clone();
653
+ lineBreakCue.lineBreak = true;
654
+ cues.push(lineBreakCue);
655
+ break;
656
+ }
528
657
  case 'b':
529
658
  nestedCue.fontWeight = bold;
530
659
  break;
@@ -769,6 +898,42 @@ shaka.text.VttTextParser = class {
769
898
 
770
899
  return (milliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
771
900
  }
901
+
902
+ /**
903
+ * This method converts the HTML entities &amp;, &lt;, &gt;, &quot;, &#39;,
904
+ * &nbsp;, &lrm; and &rlm; in string to their corresponding characters.
905
+ *
906
+ * @param {!string} input
907
+ * @return {string}
908
+ * @private
909
+ */
910
+ static htmlUnescape_(input) {
911
+ // Used to map HTML entities to characters.
912
+ const htmlUnescapes = {
913
+ '&amp;': '&',
914
+ '&lt;': '<',
915
+ '&gt;': '>',
916
+ '&quot;': '"',
917
+ '&#39;': '\'',
918
+ '&nbsp;': '\u{a0}',
919
+ '&lrm;': '\u{200e}',
920
+ '&rlm;': '\u{200f}',
921
+ };
922
+
923
+ // Used to match HTML entities and HTML characters.
924
+ const reEscapedHtml = /&(?:amp|lt|gt|quot|#(0+)?39|nbsp|lrm|rlm);/g;
925
+ const reHasEscapedHtml = RegExp(reEscapedHtml.source);
926
+ // This check is an optimization, since replace always makes a copy
927
+ if (input && reHasEscapedHtml.test(input)) {
928
+ return input.replace(reEscapedHtml, (entity) => {
929
+ // The only thing that might not match the dictionary above is the
930
+ // single quote, which can be matched by many strings in the regex, but
931
+ // only has a single entry in the dictionary.
932
+ return htmlUnescapes[entity] || '\'';
933
+ });
934
+ }
935
+ return input || '';
936
+ }
772
937
  };
773
938
 
774
939
  /**
@@ -87,16 +87,28 @@ shaka.text.WebVttGenerator = class {
87
87
  // We don't want to modify the array or objects passed in, since we don't
88
88
  // technically own them. So we build a new array and replace certain items
89
89
  // in it if they need to be flattened.
90
- const flattenedCues = cues.map((cue) => {
91
- if (cue.nestedCues.length) {
92
- const flatCue = cue.clone();
93
- flatCue.nestedCues = [];
94
- flatCue.payload = flattenPayload(cue);
95
- return flatCue;
96
- } else {
97
- return cue;
90
+ // We also don't want to flatten the text payloads starting at a container
91
+ // element; otherwise, for containers encapsulating multiple caption lines,
92
+ // the lines would merge into a single cue. This is undesirable when a
93
+ // subset of the captions are outside of the append time window. To fix
94
+ // this, we only call flattenPayload() starting at elements marked as
95
+ // isContainer = false.
96
+ const getCuesToFlatten = (cues, result) => {
97
+ for (const cue of cues) {
98
+ if (cue.isContainer) {
99
+ // Recurse to find the actual text payload cues.
100
+ getCuesToFlatten(cue.nestedCues, result);
101
+ } else {
102
+ // Flatten the payload.
103
+ const flatCue = cue.clone();
104
+ flatCue.nestedCues = [];
105
+ flatCue.payload = flattenPayload(cue);
106
+ result.push(flatCue);
107
+ }
98
108
  }
99
- });
109
+ return result;
110
+ };
111
+ const flattenedCues = getCuesToFlatten(cues, []);
100
112
 
101
113
  let webvttString = 'WEBVTT\n\n';
102
114
  for (const cue of flattenedCues) {
@@ -36,7 +36,9 @@ shaka.util.Dom = class {
36
36
  * @return {!HTMLButtonElement}
37
37
  */
38
38
  static createButton() {
39
- return /** @type {!HTMLButtonElement} */(document.createElement('button'));
39
+ const button = document.createElement('button');
40
+ button.setAttribute('type', 'button');
41
+ return /** @type {!HTMLButtonElement} */ (button);
40
42
  }
41
43
 
42
44