shaka-player 4.2.7 → 4.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -154,14 +154,14 @@ shaka.text.VttTextParser = class {
154
154
  for (const [key, value] of Object.entries(textColor)) {
155
155
  const cue = new shaka.text.Cue(0, 0, '');
156
156
  cue.color = value;
157
- styles.set(key, cue);
157
+ styles.set('.' + key, cue);
158
158
  }
159
159
 
160
160
  const bgColor = shaka.text.Cue.defaultTextBackgroundColor;
161
161
  for (const [key, value] of Object.entries(bgColor)) {
162
162
  const cue = new shaka.text.Cue(0, 0, '');
163
163
  cue.backgroundColor = value;
164
- styles.set(key, cue);
164
+ styles.set('.' + key, cue);
165
165
  }
166
166
  }
167
167
 
@@ -221,96 +221,121 @@ shaka.text.VttTextParser = class {
221
221
  return;
222
222
  }
223
223
 
224
- if (!text[1].includes('::cue')) {
225
- return;
226
- }
227
- let styleSelector = 'global';
228
- // Look for what is within parentisesis. For example:
229
- // <code>:: cue (b) {</code>, what we are looking for is <code>b</code>
230
- const selector = text[1].match(/\((.*)\)/);
231
- if (selector) {
232
- styleSelector = selector.pop();
224
+ /** @type {!Array.<!Array.<string>>} */
225
+ const styleBlocks = [];
226
+ let lastBlockIndex = -1;
227
+ for (let i = 1; i < text.length; i++) {
228
+ if (text[i].includes('::cue')) {
229
+ styleBlocks.push([]);
230
+ lastBlockIndex = styleBlocks.length - 1;
231
+ }
232
+ if (lastBlockIndex == -1) {
233
+ continue;
234
+ }
235
+ styleBlocks[lastBlockIndex].push(text[i]);
236
+ if (text[i].includes('}')) {
237
+ lastBlockIndex = -1;
238
+ }
233
239
  }
234
240
 
235
- // We start at 2 to avoid '::cue' and end earlier to avoid '}'
236
- let propertyLines = text.slice(2, -1);
237
- if (text[1].includes('}')) {
238
- const payload = /\{(.*?)\}/.exec(text[1]);
239
- if (payload) {
240
- propertyLines = payload[1].split(';');
241
+ for (const styleBlock of styleBlocks) {
242
+ let styleSelector = 'global';
243
+ // Look for what is within parentheses. For example:
244
+ // <code>:: cue (b) {</code>, what we are looking for is <code>b</code>
245
+ const selector = styleBlock[0].match(/\((.*)\)/);
246
+ if (selector) {
247
+ styleSelector = selector.pop();
241
248
  }
242
- }
243
249
 
244
- const cue = new shaka.text.Cue(0, 0, '');
245
- let validStyle = false;
246
- for (let i = 0; i < propertyLines.length; i++) {
247
- // We look for CSS properties. As a general rule they are separated by
248
- // <code>:</code>. Eg: <code>color: red;</code>
249
- const lineParts = /^\s*([^:]+):\s*(.*)/.exec(propertyLines[i]);
250
- if (lineParts) {
251
- const name = lineParts[1].trim();
252
- const value = lineParts[2].trim().replace(';', '');
253
- switch (name) {
254
- case 'background-color':
255
- validStyle = true;
256
- cue.backgroundColor = value;
257
- break;
258
- case 'color':
259
- validStyle = true;
260
- cue.color = value;
261
- break;
262
- case 'font-family':
263
- validStyle = true;
264
- cue.fontFamily = value;
265
- break;
266
- case 'font-size':
267
- validStyle = true;
268
- cue.fontSize = value;
269
- break;
270
- case 'font-weight':
271
- if (parseInt(value, 10) >= 700) {
250
+ // We start at 1 to avoid '::cue' and end earlier to avoid '}'
251
+ let propertyLines = styleBlock.slice(1, -1);
252
+ if (styleBlock[0].includes('}')) {
253
+ const payload = /\{(.*?)\}/.exec(styleBlock[0]);
254
+ if (payload) {
255
+ propertyLines = payload[1].split(';');
256
+ }
257
+ }
258
+
259
+ // Continue styles over multiple selectors if necessary.
260
+ // For example,
261
+ // ::cue(b) { background: white; } ::cue(b) { color: blue; }
262
+ // should set both the background and foreground of bold tags.
263
+ let cue = styles.get(styleSelector);
264
+ if (!cue) {
265
+ cue = new shaka.text.Cue(0, 0, '');
266
+ }
267
+
268
+ let validStyle = false;
269
+ for (let i = 0; i < propertyLines.length; i++) {
270
+ // We look for CSS properties. As a general rule they are separated by
271
+ // <code>:</code>. Eg: <code>color: red;</code>
272
+ const lineParts = /^\s*([^:]+):\s*(.*)/.exec(propertyLines[i]);
273
+ if (lineParts) {
274
+ const name = lineParts[1].trim();
275
+ const value = lineParts[2].trim().replace(';', '');
276
+ switch (name) {
277
+ case 'background-color':
278
+ case 'background':
272
279
  validStyle = true;
273
- cue.fontWeight = shaka.text.Cue.fontWeight.BOLD;
274
- }
275
- break;
276
- case 'font-style':
277
- switch (value) {
278
- case 'normal':
279
- validStyle = true;
280
- cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL;
281
- break;
282
- case 'italic':
283
- validStyle = true;
284
- cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
285
- break;
286
- case 'oblique':
280
+ cue.backgroundColor = value;
281
+ break;
282
+ case 'color':
283
+ validStyle = true;
284
+ cue.color = value;
285
+ break;
286
+ case 'font-family':
287
+ validStyle = true;
288
+ cue.fontFamily = value;
289
+ break;
290
+ case 'font-size':
291
+ validStyle = true;
292
+ cue.fontSize = value;
293
+ break;
294
+ case 'font-weight':
295
+ if (parseInt(value, 10) >= 700 || value == 'bold') {
287
296
  validStyle = true;
288
- cue.fontStyle = shaka.text.Cue.fontStyle.OBLIQUE;
289
- break;
290
- }
291
- break;
292
- case 'opacity':
293
- validStyle = true;
294
- cue.opacity = parseFloat(value);
295
- break;
296
- case 'text-shadow':
297
- validStyle = true;
298
- cue.textShadow = value;
299
- break;
300
- case 'white-space':
301
- validStyle = true;
302
- cue.wrapLine = value != 'noWrap';
303
- break;
304
- default:
305
- shaka.log.warning('VTT parser encountered an unsupported style: ',
306
- lineParts);
307
- break;
297
+ cue.fontWeight = shaka.text.Cue.fontWeight.BOLD;
298
+ }
299
+ break;
300
+ case 'font-style':
301
+ switch (value) {
302
+ case 'normal':
303
+ validStyle = true;
304
+ cue.fontStyle = shaka.text.Cue.fontStyle.NORMAL;
305
+ break;
306
+ case 'italic':
307
+ validStyle = true;
308
+ cue.fontStyle = shaka.text.Cue.fontStyle.ITALIC;
309
+ break;
310
+ case 'oblique':
311
+ validStyle = true;
312
+ cue.fontStyle = shaka.text.Cue.fontStyle.OBLIQUE;
313
+ break;
314
+ }
315
+ break;
316
+ case 'opacity':
317
+ validStyle = true;
318
+ cue.opacity = parseFloat(value);
319
+ break;
320
+ case 'text-shadow':
321
+ validStyle = true;
322
+ cue.textShadow = value;
323
+ break;
324
+ case 'white-space':
325
+ validStyle = true;
326
+ cue.wrapLine = value != 'noWrap';
327
+ break;
328
+ default:
329
+ shaka.log.warning('VTT parser encountered an unsupported style: ',
330
+ lineParts);
331
+ break;
332
+ }
308
333
  }
309
334
  }
310
- }
311
335
 
312
- if (validStyle) {
313
- styles.set(styleSelector, cue);
336
+ if (validStyle) {
337
+ styles.set(styleSelector, cue);
338
+ }
314
339
  }
315
340
  }
316
341
 
@@ -337,8 +362,8 @@ shaka.text.VttTextParser = class {
337
362
  return null;
338
363
  }
339
364
 
340
- // Skip style blocks.
341
- if (text[0] == 'STYLE') {
365
+ // Skip style and region blocks.
366
+ if (text[0] == 'STYLE' || text[0] == 'REGION') {
342
367
  return null;
343
368
  }
344
369
 
@@ -366,7 +391,8 @@ shaka.text.VttTextParser = class {
366
391
  end += timeOffset;
367
392
 
368
393
  // Get the payload.
369
- const payload = text.slice(1).join('\n').trim();
394
+ const payload = VttTextParser.htmlUnescape_(
395
+ text.slice(1).join('\n').trim());
370
396
 
371
397
  let cue = null;
372
398
  if (styles.has('global')) {
@@ -413,6 +439,7 @@ shaka.text.VttTextParser = class {
413
439
  }
414
440
  payload = VttTextParser.replaceColorPayload_(payload);
415
441
  payload = VttTextParser.replaceKaraokeStylePayload_(payload);
442
+ payload = VttTextParser.replaceVoiceStylePayload_(payload);
416
443
  const xmlPayload = '<span>' + payload + '</span>';
417
444
  const element = shaka.util.XmlUtils.parseXmlString(xmlPayload, 'span');
418
445
  if (element) {
@@ -438,6 +465,79 @@ shaka.text.VttTextParser = class {
438
465
  }
439
466
  }
440
467
 
468
+ /**
469
+ * Converts voice style tag to be valid for xml parsing
470
+ * For example,
471
+ * input: <v Shaka>Test
472
+ * output: <v.voice-Shaka>Test</v.voice-Shaka>
473
+ *
474
+ * @param {string} payload
475
+ * @return {string} processed payload
476
+ * @private
477
+ */
478
+ static replaceVoiceStylePayload_(payload) {
479
+ const voiceTag = 'v';
480
+ const names = [];
481
+ let nameStart = -1;
482
+ let newPayload = '';
483
+ let hasVoiceEndTag = false;
484
+ for (let i = 0; i < payload.length; i++) {
485
+ // This condition is used to manage tags that have end tags.
486
+ if (payload[i] === '/') {
487
+ const end = payload.indexOf('>', i);
488
+ if (end === -1) {
489
+ return payload;
490
+ }
491
+ const tagEnd = payload.substring(i + 1, end);
492
+ if (!tagEnd || tagEnd != voiceTag) {
493
+ newPayload += payload[i];
494
+ continue;
495
+ }
496
+ hasVoiceEndTag = true;
497
+ let tagStart = null;
498
+ if (names.length) {
499
+ tagStart = names[names.length -1];
500
+ }
501
+ if (!tagStart) {
502
+ newPayload += payload[i];
503
+ } else if (tagStart === tagEnd) {
504
+ newPayload += '/' + tagEnd + '>';
505
+ i += tagEnd.length + 1;
506
+ } else {
507
+ if (!tagStart.startsWith(voiceTag)) {
508
+ newPayload += payload[i];
509
+ continue;
510
+ }
511
+ newPayload += '/' + tagStart + '>';
512
+ i += tagEnd.length + 1;
513
+ }
514
+ } else {
515
+ // Here we only want the tag name, not any other payload.
516
+ if (payload[i] === '<') {
517
+ nameStart = i + 1;
518
+ if (payload[nameStart] != voiceTag) {
519
+ nameStart = -1;
520
+ }
521
+ } else if (payload[i] === '>') {
522
+ if (nameStart > 0) {
523
+ names.push(payload.substr(nameStart, i - nameStart));
524
+ nameStart = -1;
525
+ }
526
+ }
527
+ newPayload += payload[i];
528
+ }
529
+ }
530
+ for (const name of names) {
531
+ const newName = name.replace(' ', '.voice-');
532
+ newPayload = newPayload.replace(`<${name}>`, `<${newName}>`);
533
+ newPayload = newPayload.replace(`</${name}>`, `</${newName}>`);
534
+ if (!hasVoiceEndTag) {
535
+ newPayload += `</${newName}>`;
536
+ }
537
+ }
538
+ return newPayload;
539
+ }
540
+
441
541
  /**
442
542
  * Converts karaoke style tag to be valid for xml parsing
443
543
  * For example,
@@ -494,28 +594,36 @@ shaka.text.VttTextParser = class {
494
594
  let nameStart = -1;
495
595
  let newPayload = '';
496
596
  for (let i = 0; i < payload.length; i++) {
497
- if (payload[i] === '/') {
597
+ if (payload[i] === '/' && i > 0 && payload[i - 1] === '<') {
498
598
  const end = payload.indexOf('>', i);
499
599
  if (end <= i) {
500
600
  return payload;
501
601
  }
502
602
  const tagEnd = payload.substring(i + 1, end);
603
+ if (!tagEnd || tagEnd !== 'c') {
604
+ newPayload += payload[i];
605
+ continue;
606
+ }
503
607
  const tagStart = names.pop();
504
- if (!tagEnd || !tagStart) {
505
- return payload;
608
+ if (!tagStart) {
609
+ newPayload += payload[i];
506
610
  } else if (tagStart === tagEnd) {
507
611
  newPayload += '/' + tagEnd + '>';
508
612
  i += tagEnd.length + 1;
509
613
  } else {
510
- if (!tagStart.startsWith('c.') || tagEnd !== 'c') {
511
- return payload;
614
+ if (!tagStart.startsWith('c.')) {
615
+ newPayload += payload[i];
616
+ continue;
512
617
  }
513
- newPayload += '/' + tagStart + '>';
514
618
  i += tagEnd.length + 1;
619
+ newPayload += '/' + tagStart + '>';
515
620
  }
516
621
  } else {
517
622
  if (payload[i] === '<') {
518
623
  nameStart = i + 1;
624
+ if (payload[nameStart] != 'c') {
625
+ nameStart = -1;
626
+ }
519
627
  } else if (payload[i] === '>') {
520
628
  if (nameStart > 0) {
521
629
  names.push(payload.substr(nameStart, i - nameStart));
@@ -584,12 +692,29 @@ shaka.text.VttTextParser = class {
584
692
  const bold = shaka.text.Cue.fontWeight.BOLD;
585
693
  const italic = shaka.text.Cue.fontStyle.ITALIC;
586
694
  const underline = shaka.text.Cue.textDecoration.UNDERLINE;
587
- const tags = element.nodeName.split(/[ .]+/);
695
+ const tags = element.nodeName.split(/(?=[ .])+/g);
588
696
  for (const tag of tags) {
589
- if (styles.has(tag)) {
590
- VttTextParser.mergeStyle_(nestedCue, styles.get(tag));
697
+ let styleTag = tag;
698
+ // White blanks at start indicate that the style is a voice
699
+ if (styleTag.startsWith('.voice-')) {
700
+ const voice = styleTag.split('-').pop();
701
+ styleTag = `v[voice="${voice}"]`;
702
+ // The specification allows to have quotes and not, so we check to
703
+ // see which one is being used.
704
+ if (!styles.has(styleTag)) {
705
+ styleTag = `v[voice=${voice}]`;
706
+ }
707
+ }
708
+ if (styles.has(styleTag)) {
709
+ VttTextParser.mergeStyle_(nestedCue, styles.get(styleTag));
591
710
  }
592
711
  switch (tag) {
712
+ case 'br': {
713
+ const lineBreakCue = rootCue.clone();
714
+ lineBreakCue.lineBreak = true;
715
+ cues.push(lineBreakCue);
716
+ break;
717
+ }
593
718
  case 'b':
594
719
  nestedCue.fontWeight = bold;
595
720
  break;
@@ -843,6 +968,42 @@ shaka.text.VttTextParser = class {
843
968
 
844
969
  return (milliseconds / 1000) + seconds + (minutes * 60) + (hours * 3600);
845
970
  }
971
+
972
+ /**
973
+ * This method converts the HTML entities &amp;, &lt;, &gt;, &quot;, &#39;,
974
+ * &nbsp;, &lrm; and &rlm; in string to their corresponding characters.
975
+ *
976
+ * @param {!string} input
977
+ * @return {string}
978
+ * @private
979
+ */
980
+ static htmlUnescape_(input) {
981
+ // Used to map HTML entities to characters.
982
+ const htmlUnescapes = {
983
+ '&amp;': '&',
984
+ '&lt;': '<',
985
+ '&gt;': '>',
986
+ '&quot;': '"',
987
+ '&#39;': '\'',
988
+ '&nbsp;': '\u{a0}',
989
+ '&lrm;': '\u{200e}',
990
+ '&rlm;': '\u{200f}',
991
+ };
992
+
993
+ // Used to match HTML entities and HTML characters.
994
+ const reEscapedHtml = /&(?:amp|lt|gt|quot|#(0+)?39|nbsp|lrm|rlm);/g;
995
+ const reHasEscapedHtml = RegExp(reEscapedHtml.source);
996
+ // This check is an optimization, since replace always makes a copy
997
+ if (input && reHasEscapedHtml.test(input)) {
998
+ return input.replace(reEscapedHtml, (entity) => {
999
+ // The only thing that might not match the dictionary above is the
1000
+ // single quote, which can be matched by many strings in the regex, but
1001
+ // only has a single entry in the dictionary.
1002
+ return htmlUnescapes[entity] || '\'';
1003
+ });
1004
+ }
1005
+ return input || '';
1006
+ }
846
1007
  };
847
1008
 
848
1009
  /**
@@ -7,6 +7,7 @@
7
7
  goog.provide('shaka.text.WebVttGenerator');
8
8
 
9
9
  goog.require('shaka.text.Cue');
10
+ goog.require('shaka.text.Utils');
10
11
 
11
12
 
12
13
  /**
@@ -20,45 +21,6 @@ shaka.text.WebVttGenerator = class {
20
21
  * @return {string}
21
22
  */
22
23
  static convert(cues, adCuePoints) {
23
- // Flatten nested cue payloads recursively. If a cue has nested cues,
24
- // their contents should be combined and replace the payload of the parent.
25
- const flattenPayload = (cue) => {
26
- // Handle styles (currently bold/italics/underline).
27
- // TODO: add support for color rendering.
28
- const openStyleTags = [];
29
- const bold = cue.fontWeight >= shaka.text.Cue.fontWeight.BOLD;
30
- const italics = cue.fontStyle == shaka.text.Cue.fontStyle.ITALIC;
31
- const underline = cue.textDecoration.includes(
32
- shaka.text.Cue.textDecoration.UNDERLINE);
33
- if (bold) {
34
- openStyleTags.push('b');
35
- }
36
- if (italics) {
37
- openStyleTags.push('i');
38
- }
39
- if (underline) {
40
- openStyleTags.push('u');
41
- }
42
-
43
- // Prefix opens tags, suffix closes tags in reverse order of opening.
44
- const prefixStyleTags = openStyleTags.reduce((acc, tag) => {
45
- return `${acc}<${tag}>`;
46
- }, '');
47
- const suffixStyleTags = openStyleTags.reduceRight((acc, tag) => {
48
- return `${acc}</${tag}>`;
49
- }, '');
50
-
51
- if (cue.lineBreak) {
52
- // This is a vertical lineBreak, so insert a newline.
53
- return '\n';
54
- } else if (cue.nestedCues.length) {
55
- return cue.nestedCues.map(flattenPayload).join('');
56
- } else {
57
- // This is a real cue.
58
- return prefixStyleTags + cue.payload + suffixStyleTags;
59
- }
60
- };
61
-
62
24
  const webvttTimeString = (time) => {
63
25
  let newTime = time;
64
26
  for (const adCuePoint of adCuePoints) {
@@ -78,31 +40,7 @@ shaka.text.WebVttGenerator = class {
78
40
  milliseconds;
79
41
  };
80
42
 
81
- // We don't want to modify the array or objects passed in, since we don't
82
- // technically own them. So we build a new array and replace certain items
83
- // in it if they need to be flattened.
84
- // We also don't want to flatten the text payloads starting at a container
85
- // element; otherwise, for containers encapsulating multiple caption lines,
86
- // the lines would merge into a single cue. This is undesirable when a
87
- // subset of the captions are outside of the append time window. To fix
88
- // this, we only call flattenPayload() starting at elements marked as
89
- // isContainer = false.
90
- const getCuesToFlatten = (cues, result) => {
91
- for (const cue of cues) {
92
- if (cue.isContainer) {
93
- // Recurse to find the actual text payload cues.
94
- getCuesToFlatten(cue.nestedCues, result);
95
- } else {
96
- // Flatten the payload.
97
- const flatCue = cue.clone();
98
- flatCue.nestedCues = [];
99
- flatCue.payload = flattenPayload(cue);
100
- result.push(flatCue);
101
- }
102
- }
103
- return result;
104
- };
105
- const flattenedCues = getCuesToFlatten(cues, []);
43
+ const flattenedCues = shaka.text.Utils.getCuesToFlatten(cues, []);
106
44
 
107
45
  let webvttString = 'WEBVTT\n\n';
108
46
  for (const cue of flattenedCues) {
@@ -238,6 +238,14 @@ shaka.util.Platform = class {
238
238
  return shaka.util.Platform.userAgentContains_('PlayStation 4');
239
239
  }
240
240
 
241
+ /**
242
+ * Check if the current platform is Hisense.
243
+ */
244
+ static isHisense() {
245
+ return shaka.util.Platform.userAgentContains_('Hisense') ||
246
+ shaka.util.Platform.userAgentContains_('VIDAA');
247
+ }
248
+
241
249
  /**
242
250
  * Check if the current platform is Virgin Media device.
243
251
  */
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": "4.2.7",
4
+ "version": "4.2.8",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
package/support.html CHANGED
@@ -38,6 +38,7 @@
38
38
  }
39
39
 
40
40
  </style>
41
+ <script defer src="node_modules/mux.js/dist/mux.min.js"></script>
41
42
  <script src="dist/shaka-player.compiled.js"></script>
42
43
  <script>
43
44
  function whenLoaded(fn) {
@@ -195,42 +195,6 @@ describe('DashParser Manifest', () => {
195
195
  }));
196
196
  });
197
197
 
198
- it('rejects periods after one without duration', async () => {
199
- const periodContents = [
200
- ' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',
201
- ' <Representation bandwidth="100">',
202
- ' <SegmentTemplate startNumber="1" media="l-$Number$.mp4">',
203
- ' <SegmentTimeline>',
204
- ' <S t="0" d="10" />',
205
- ' </SegmentTimeline>',
206
- ' </SegmentTemplate>',
207
- ' </Representation>',
208
- ' </AdaptationSet>',
209
- ].join('\n');
210
- const template = [
211
- '<MPD mediaPresentationDuration="PT75S">',
212
- ' <Period id="1">',
213
- '%(periodContents)s',
214
- ' </Period>',
215
- ' <Period id="2">',
216
- '%(periodContents)s',
217
- ' </Period>',
218
- '</MPD>',
219
- ].join('\n');
220
- const source = sprintf(template, {periodContents: periodContents});
221
-
222
- fakeNetEngine.setResponseText('dummy://foo', source);
223
- /** @type {shaka.extern.Manifest} */
224
- const manifest = await parser.start('dummy://foo', playerInterface);
225
- const video = manifest.variants[0].video;
226
- await video.createSegmentIndex();
227
-
228
- // The first period has a segment from 0-10.
229
- // With the second period skipping, we should fail to find a segment at 10.
230
- expect(video.segmentIndex.find(0)).not.toBe(null);
231
- expect(video.segmentIndex.find(10)).toBe(null);
232
- });
233
-
234
198
  it('calculates Period times when missing', async () => {
235
199
  const periodContents = [
236
200
  ' <AdaptationSet mimeType="video/mp4" lang="en" group="1">',