shaka-player 4.2.6 → 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.
Files changed (51) hide show
  1. package/.github/workflows/selenium-lab-tests.yaml +22 -0
  2. package/CHANGELOG.md +36 -0
  3. package/dist/deps.js +4 -3
  4. package/dist/locales.js +30 -30
  5. package/dist/shaka-player.compiled.d.ts +10 -0
  6. package/dist/shaka-player.compiled.debug.d.ts +10 -0
  7. package/dist/shaka-player.compiled.debug.externs.js +12 -0
  8. package/dist/shaka-player.compiled.debug.js +177 -166
  9. package/dist/shaka-player.compiled.debug.map +4 -4
  10. package/dist/shaka-player.compiled.externs.js +12 -0
  11. package/dist/shaka-player.compiled.js +672 -666
  12. package/dist/shaka-player.compiled.map +4 -4
  13. package/dist/shaka-player.ui.d.ts +10 -0
  14. package/dist/shaka-player.ui.debug.d.ts +10 -0
  15. package/dist/shaka-player.ui.debug.externs.js +12 -0
  16. package/dist/shaka-player.ui.debug.js +189 -179
  17. package/dist/shaka-player.ui.debug.map +4 -4
  18. package/dist/shaka-player.ui.externs.js +12 -0
  19. package/dist/shaka-player.ui.js +820 -815
  20. package/dist/shaka-player.ui.map +4 -4
  21. package/lib/dash/segment_template.js +17 -5
  22. package/lib/hls/hls_parser.js +53 -16
  23. package/lib/media/media_source_engine.js +34 -4
  24. package/lib/media/segment_index.js +5 -10
  25. package/lib/media/segment_reference.js +39 -0
  26. package/lib/media/streaming_engine.js +15 -3
  27. package/lib/player.js +7 -1
  28. package/lib/polyfill/media_capabilities.js +4 -1
  29. package/lib/text/simple_text_displayer.js +2 -64
  30. package/lib/text/text_utils.js +88 -0
  31. package/lib/text/ui_text_displayer.js +24 -9
  32. package/lib/text/vtt_text_parser.js +256 -95
  33. package/lib/text/web_vtt_generator.js +2 -52
  34. package/lib/util/dom_utils.js +3 -1
  35. package/lib/util/platform.js +8 -0
  36. package/lib/util/stream_utils.js +6 -0
  37. package/lib/util/timer.js +17 -0
  38. package/package.json +1 -1
  39. package/support.html +1 -0
  40. package/test/dash/dash_parser_manifest_unit.js +0 -36
  41. package/test/hls/hls_live_unit.js +8 -30
  42. package/test/hls/hls_parser_unit.js +30 -0
  43. package/test/media/segment_index_unit.js +28 -0
  44. package/test/player_integration.js +27 -0
  45. package/test/player_unit.js +4 -2
  46. package/test/test/util/layout_tests.js +433 -0
  47. package/test/test/util/util.js +0 -120
  48. package/test/{ui → text}/text_displayer_layout_unit.js +59 -235
  49. package/test/text/ui_text_displayer_unit.js +72 -4
  50. package/test/text/vtt_text_parser_unit.js +123 -6
  51. package/test/text/web_vtt_layout_integration.js +528 -0
@@ -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) {
@@ -136,6 +136,14 @@ shaka.hls.HlsParser = class {
136
136
  */
137
137
  this.minSequenceNumber_ = null;
138
138
 
139
+ /**
140
+ * The lowest time value for any of the streams, as defined by the
141
+ * EXT-X-PROGRAM-DATE-TIME value. Measured in seconds since January 1, 1970.
142
+ *
143
+ * @private {number}
144
+ */
145
+ this.lowestSyncTime_ = Infinity;
146
+
139
147
  /**
140
148
  * This timer is used to trigger the start of a manifest update. A manifest
141
149
  * update is async. Once the update is finished, the timer will be restarted
@@ -446,7 +454,11 @@ shaka.hls.HlsParser = class {
446
454
  // Now adjust timestamps back to begin at 0.
447
455
  const segmentN = segmentIndex.earliestReference();
448
456
  if (segmentN) {
449
- this.offsetStream_(streamInfo, -segmentN.startTime);
457
+ const streamOffset = -segmentN.startTime;
458
+ // Modify all SegmentReferences equally.
459
+ streamInfo.stream.segmentIndex.offset(streamOffset);
460
+ // Update other parts of streamInfo the same way.
461
+ this.offsetStreamInfo_(streamInfo, streamOffset);
450
462
  }
451
463
  }
452
464
  }
@@ -465,6 +477,10 @@ shaka.hls.HlsParser = class {
465
477
  return;
466
478
  }
467
479
 
480
+ if (this.lowestSyncTime_ != Infinity) {
481
+ return;
482
+ }
483
+
468
484
  let lowestSyncTime = Infinity;
469
485
 
470
486
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
@@ -481,28 +497,36 @@ shaka.hls.HlsParser = class {
481
497
  // Nothing to sync.
482
498
  return;
483
499
  }
500
+ this.lowestSyncTime_ = lowestSyncTime;
484
501
 
485
502
  shaka.log.debug('Syncing HLS streams against base time:', lowestSyncTime);
486
503
 
487
504
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
488
505
  const segmentIndex = streamInfo.stream.segmentIndex;
489
506
  if (segmentIndex != null) {
507
+ // A segment's startTime should be based on its syncTime vs the lowest
508
+ // syncTime across all streams. The earliest segment sync time from
509
+ // any stream will become presentation time 0. If two streams start
510
+ // e.g. 6 seconds apart in syncTime, then their first segments will
511
+ // also start 6 seconds apart in presentation time.
512
+
490
513
  const segment0 = segmentIndex.earliestReference();
491
514
  if (segment0.syncTime == null) {
492
515
  shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
493
516
  streamInfo.verbatimMediaPlaylistUri,
494
517
  'Expect AV sync issues!');
495
518
  } else {
496
- // The first segment's target startTime should be based entirely on
497
- // its syncTime. The rest of the stream will be based on that
498
- // starting point. The earliest segment sync time from any stream
499
- // will become presentation time 0. If two streams start e.g. 6
500
- // seconds apart in syncTime, then their first segments will also
501
- // start 6 seconds apart in presentation time.
519
+ // Stream metadata are offset by a fixed amount based on the
520
+ // first segment.
502
521
  const segment0TargetTime = segment0.syncTime - lowestSyncTime;
503
522
  const streamOffset = segment0TargetTime - segment0.startTime;
523
+ this.offsetStreamInfo_(streamInfo, streamOffset);
504
524
 
505
- this.offsetStream_(streamInfo, streamOffset);
525
+ // This is computed across all segments separately to manage
526
+ // accumulated drift in durations.
527
+ for (const segment of segmentIndex) {
528
+ segment.syncAgainst(lowestSyncTime);
529
+ }
506
530
  }
507
531
  }
508
532
  }
@@ -515,13 +539,13 @@ shaka.hls.HlsParser = class {
515
539
  * @param {number} offset
516
540
  * @private
517
541
  */
518
- offsetStream_(streamInfo, offset) {
519
- streamInfo.stream.segmentIndex.offset(offset);
520
-
542
+ offsetStreamInfo_(streamInfo, offset) {
543
+ // Adjust our accounting of the maximum timestamp.
521
544
  streamInfo.maxTimestamp += offset;
522
545
  goog.asserts.assert(streamInfo.maxTimestamp >= 0,
523
546
  'Negative maxTimestamp after adjustment!');
524
547
 
548
+ // Update our map from sequence number to start time.
525
549
  for (const [key, value] of streamInfo.mediaSequenceToStartTime) {
526
550
  streamInfo.mediaSequenceToStartTime.set(key, value + offset);
527
551
  }
@@ -2272,16 +2296,19 @@ shaka.hls.HlsParser = class {
2272
2296
  position = mediaSequenceNumber + skippedSegments + i;
2273
2297
 
2274
2298
  const discontinuityTag = shaka.hls.Utils.getFirstTagWithName(
2275
- playlist.tags, 'EXT-X-DISCONTINUITY');
2299
+ item.tags, 'EXT-X-DISCONTINUITY');
2276
2300
  if (discontinuityTag) {
2277
2301
  discontinuitySequence++;
2278
2302
  }
2279
2303
 
2280
2304
  // Apply new AES-128 tags as you see them, keeping a running total.
2281
2305
  for (const drmTag of item.tags) {
2282
- if (drmTag.name == 'EXT-X-KEY' &&
2283
- drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
2284
- hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
2306
+ if (drmTag.name == 'EXT-X-KEY') {
2307
+ if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
2308
+ hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
2309
+ } else {
2310
+ hlsAes128Key = undefined;
2311
+ }
2285
2312
  }
2286
2313
  }
2287
2314
 
@@ -2402,6 +2429,16 @@ shaka.hls.HlsParser = class {
2402
2429
  }
2403
2430
  }
2404
2431
 
2432
+ // lowestSyncTime is a value from a previous playlist update. Use it to
2433
+ // set reference start times. If this is the first playlist parse, we will
2434
+ // skip this step, and wait until we have sync time across stream types.
2435
+ const lowestSyncTime = this.lowestSyncTime_;
2436
+ if (someSyncTime && lowestSyncTime != Infinity) {
2437
+ for (const reference of references) {
2438
+ reference.syncAgainst(lowestSyncTime);
2439
+ }
2440
+ }
2441
+
2405
2442
  return references;
2406
2443
  }
2407
2444
 
@@ -2896,7 +2933,7 @@ shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
2896
2933
  'm4f': 'application/mp4',
2897
2934
  'cmft': 'application/mp4',
2898
2935
  'vtt': 'text/vtt',
2899
- 'webvtt': 'txt/vtt',
2936
+ 'webvtt': 'text/vtt',
2900
2937
  'ttml': 'application/ttml+xml',
2901
2938
  };
2902
2939
 
@@ -616,6 +616,8 @@ shaka.media.MediaSourceEngine = class {
616
616
  // into extracting a timestamp for us to align text segments in sequence
617
617
  // mode.
618
618
 
619
+ const duration = this.mediaSource_.duration;
620
+
619
621
  // Timestamps can only be reliably extracted from video, not audio.
620
622
  // Packed audio formats do not have internal timestamps at all.
621
623
  // Prefer video for this when available.
@@ -659,7 +661,7 @@ shaka.media.MediaSourceEngine = class {
659
661
  // Clear the buffer.
660
662
  await this.enqueueOperation_(
661
663
  contentType,
662
- () => this.remove_(contentType, 0, this.mediaSource_.duration));
664
+ () => this.remove_(contentType, 0, duration));
663
665
 
664
666
  // Finally, flush the buffer in case of choppy video start on HLS fMP4.
665
667
  if (contentType == ContentType.VIDEO) {
@@ -671,6 +673,10 @@ shaka.media.MediaSourceEngine = class {
671
673
 
672
674
  // Now switch to sequence mode and fall through to our normal operations.
673
675
  sourceBuffer.mode = SEQUENCE;
676
+
677
+ // When we change the buffer mode the duration is lost, so we need to set
678
+ // it explicitly.
679
+ await this.setDuration(duration);
674
680
  }
675
681
 
676
682
  if (reference && this.sequenceMode_ && contentType != ContentType.TEXT) {
@@ -705,8 +711,13 @@ shaka.media.MediaSourceEngine = class {
705
711
  bufferedBefore, bufferedAfter);
706
712
  if (newBuffered) {
707
713
  const segmentDuration = reference.endTime - reference.startTime;
708
- if (Math.abs(newBuffered.start - reference.startTime) >
709
- segmentDuration / 2) {
714
+ // Check end times instead of start times. We may be overwriting a
715
+ // buffer and only the end changes, and that would be fine.
716
+ // Also, exclude tiny segments. Sometimes alignment segments as small
717
+ // as 33ms are seen in Google DAI content. For such tiny segments,
718
+ // half a segment duration would be no issue.
719
+ const offset = Math.abs(newBuffered.end - reference.endTime);
720
+ if (segmentDuration > 0.100 && offset > segmentDuration / 2) {
710
721
  shaka.log.error('Possible encoding problem detected!',
711
722
  'Unexpected buffered range for reference', reference,
712
723
  'from URIs', reference.getUris(),
@@ -865,8 +876,27 @@ shaka.media.MediaSourceEngine = class {
865
876
  * @return {!Promise}
866
877
  */
867
878
  async resync(contentType, timestampOffset) {
879
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
880
+
868
881
  goog.asserts.assert(this.sequenceMode_,
869
- 'resyncAudio only used with sequence mode!');
882
+ 'resync only used with sequence mode!');
883
+
884
+ if (contentType == ContentType.TEXT) {
885
+ // This operation is for audio and video only.
886
+ return;
887
+ }
888
+
889
+ // Queue an abort() to help MSE splice together overlapping segments.
890
+ // We set appendWindowEnd when we change periods in DASH content, and the
891
+ // period transition may result in overlap.
892
+ //
893
+ // An abort() also helps with MPEG2-TS. When we append a TS segment, we
894
+ // always enter a PARSING_MEDIA_SEGMENT state and we can't change the
895
+ // timestamp offset. By calling abort(), we reset the state so we can
896
+ // set it.
897
+ this.enqueueOperation_(
898
+ contentType,
899
+ () => this.abort_(contentType));
870
900
  await this.enqueueOperation_(
871
901
  contentType,
872
902
  () => this.setTimestampOffset_(contentType, timestampOffset));
@@ -183,15 +183,7 @@ shaka.media.SegmentIndex = class {
183
183
  offset(offset) {
184
184
  if (!this.immutable_) {
185
185
  for (const ref of this.references) {
186
- ref.startTime += offset;
187
- ref.endTime += offset;
188
- ref.trueEndTime += offset;
189
-
190
- for (const partial of ref.partialReferences) {
191
- partial.startTime += offset;
192
- partial.endTime += offset;
193
- partial.trueEndTime += offset;
194
- }
186
+ ref.offset(offset);
195
187
  }
196
188
  }
197
189
  }
@@ -225,8 +217,11 @@ shaka.media.SegmentIndex = class {
225
217
  // Partial segments are used for live edge, and should be removed when they
226
218
  // get older. Remove the old SegmentReferences after the first new
227
219
  // reference's start time.
220
+ // Use times rounded to the millisecond for filtering purposes, so that
221
+ // tiny rounding errors will not result in duplicate segments in the index.
222
+ const firstStartTime = Math.round(references[0].startTime * 1000) / 1000;
228
223
  this.references = this.references.filter((r) => {
229
- return r.startTime < references[0].startTime;
224
+ return (Math.round(r.startTime * 1000) / 1000) < firstStartTime;
230
225
  });
231
226
 
232
227
  this.references.push(...references);
@@ -8,6 +8,7 @@ goog.provide('shaka.media.InitSegmentReference');
8
8
  goog.provide('shaka.media.SegmentReference');
9
9
 
10
10
  goog.require('goog.asserts');
11
+ goog.require('shaka.log');
11
12
  goog.require('shaka.util.ArrayUtils');
12
13
 
13
14
 
@@ -356,6 +357,44 @@ shaka.media.SegmentReference = class {
356
357
  markAsUnavailable() {
357
358
  this.status = shaka.media.SegmentReference.Status.UNAVAILABLE;
358
359
  }
360
+
361
+ /**
362
+ * Offset the segment reference by a fixed amount.
363
+ *
364
+ * @param {number} offset The amount to add to the segment's start and end
365
+ * times.
366
+ * @export
367
+ */
368
+ offset(offset) {
369
+ this.startTime += offset;
370
+ this.endTime += offset;
371
+ this.trueEndTime += offset;
372
+
373
+ for (const partial of this.partialReferences) {
374
+ partial.startTime += offset;
375
+ partial.endTime += offset;
376
+ partial.trueEndTime += offset;
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Sync this segment against a particular sync time that will serve as "0" in
382
+ * the presentation timeline.
383
+ *
384
+ * @param {number} lowestSyncTime
385
+ * @export
386
+ */
387
+ syncAgainst(lowestSyncTime) {
388
+ if (this.syncTime == null) {
389
+ shaka.log.alwaysError('Sync attempted without sync time!');
390
+ return;
391
+ }
392
+ const desiredStart = this.syncTime - lowestSyncTime;
393
+ const offset = desiredStart - this.startTime;
394
+ if (Math.abs(offset) >= 0.001) {
395
+ this.offset(offset);
396
+ }
397
+ }
359
398
  };
360
399
 
361
400
 
@@ -832,6 +832,7 @@ shaka.media.StreamingEngine = class {
832
832
  // The playhead might be seeking on startup, if a start time is set, so
833
833
  // start "seeked" as true.
834
834
  seeked: true,
835
+ needsResync: false,
835
836
  recovering: false,
836
837
  hasError: false,
837
838
  operation: null,
@@ -1155,6 +1156,11 @@ shaka.media.StreamingEngine = class {
1155
1156
  shaka.log.v1(
1156
1157
  logPrefix, 'looking up segment from new stream endTime:', time);
1157
1158
 
1159
+ // Using a new iterator means we need to resync the stream in sequence
1160
+ // mode. The buffered range might not align perfectly with the last
1161
+ // segment end time, so we may end up repeating a segment. Resyncing
1162
+ // makes this safe to do.
1163
+ mediaState.needsResync = true;
1158
1164
  mediaState.segmentIterator =
1159
1165
  mediaState.stream.segmentIndex.getIteratorForTime(time);
1160
1166
  const ref = mediaState.segmentIterator &&
@@ -1660,7 +1666,9 @@ shaka.media.StreamingEngine = class {
1660
1666
  const lastDiscontinuitySequence =
1661
1667
  mediaState.lastSegmentReference ?
1662
1668
  mediaState.lastSegmentReference.discontinuitySequence : null;
1663
- if (reference.discontinuitySequence != lastDiscontinuitySequence) {
1669
+ if (reference.discontinuitySequence != lastDiscontinuitySequence ||
1670
+ mediaState.needsResync) {
1671
+ mediaState.needsResync = false;
1664
1672
  operations.push(this.playerInterface_.mediaSourceEngine.resync(
1665
1673
  mediaState.type, reference.startTime));
1666
1674
  }
@@ -2151,6 +2159,7 @@ shaka.media.StreamingEngine.PlayerInterface;
2151
2159
  * clearBufferSafeMargin: number,
2152
2160
  * clearingBuffer: boolean,
2153
2161
  * seeked: boolean,
2162
+ * needsResync: boolean,
2154
2163
  * adaptation: boolean,
2155
2164
  * recovering: boolean,
2156
2165
  * hasError: boolean,
@@ -2196,10 +2205,13 @@ shaka.media.StreamingEngine.PlayerInterface;
2196
2205
  * The amount of buffer to retain when clearing the buffer after the update.
2197
2206
  * @property {boolean} clearingBuffer
2198
2207
  * True indicates that the buffer is being cleared.
2199
- * @property {boolean} seeked
2208
+ * @property {boolean} seeked
2200
2209
  * True indicates that the presentation just seeked.
2201
- * @property {boolean} adaptation
2210
+ * @property {boolean} adaptation
2202
2211
  * True indicates that the presentation just automatically switched variants.
2212
+ * @property {boolean} needsResync
2213
+ * True indicates that the stream needs to be resynced in sequence mode,
2214
+ * regardless of discontinuity sequence.
2203
2215
  * @property {boolean} recovering
2204
2216
  * True indicates that the last segment was not appended because it could not
2205
2217
  * fit in the buffer.
package/lib/player.js CHANGED
@@ -1427,6 +1427,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1427
1427
  has.startTime = null;
1428
1428
  has.uri = null;
1429
1429
 
1430
+ // Release the region timeline, which is created when parsing the manifest.
1431
+ if (this.regionTimeline_) {
1432
+ this.regionTimeline_.release();
1433
+ this.regionTimeline_ = null;
1434
+ }
1435
+
1430
1436
  // In most cases we should have a media element. The one exception would
1431
1437
  // be if there was an error and we, by chance, did not have a media element.
1432
1438
  if (has.mediaElement) {
@@ -6599,7 +6605,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6599
6605
  * @define {string} A version number taken from git at compile time.
6600
6606
  * @export
6601
6607
  */
6602
- shaka.Player.version = 'v4.2.6-uncompiled';
6608
+ shaka.Player.version = 'v4.2.8-uncompiled';
6603
6609
 
6604
6610
  // Initialize the deprecation system using the version string we just set
6605
6611
  // on the player.
@@ -47,6 +47,8 @@ shaka.polyfill.MediaCapabilities = class {
47
47
  // should always install polyfill for WebOS browsers.
48
48
  // Since MediaCapabilities implementation is buggy in EOS browsers, we
49
49
  // should always install polyfill for EOS browsers.
50
+ // Since MediaCapabilities implementation is buggy in Hisense browsers, we
51
+ // should always install polyfill for Hisense browsers.
50
52
  let canUseNativeMCap = true;
51
53
  if (shaka.util.Platform.isApple() ||
52
54
  shaka.util.Platform.isPS5() ||
@@ -54,7 +56,8 @@ shaka.polyfill.MediaCapabilities = class {
54
56
  shaka.util.Platform.isWebOS() ||
55
57
  shaka.util.Platform.isTizen() ||
56
58
  shaka.util.Platform.isChromecast() ||
57
- shaka.util.Platform.isEOS()) {
59
+ shaka.util.Platform.isEOS() ||
60
+ shaka.util.Platform.isHisense()) {
58
61
  canUseNativeMCap = false;
59
62
  }
60
63
  if (canUseNativeMCap && navigator.mediaCapabilities) {
@@ -14,6 +14,7 @@ goog.provide('shaka.text.SimpleTextDisplayer');
14
14
  goog.require('goog.asserts');
15
15
  goog.require('shaka.log');
16
16
  goog.require('shaka.text.Cue');
17
+ goog.require('shaka.text.Utils');
17
18
 
18
19
 
19
20
  /**
@@ -79,70 +80,7 @@ shaka.text.SimpleTextDisplayer = class {
79
80
  * @export
80
81
  */
81
82
  append(cues) {
82
- // Flatten nested cue payloads recursively. If a cue has nested cues,
83
- // their contents should be combined and replace the payload of the parent.
84
- const flattenPayload = (cue) => {
85
- // Handle styles (currently bold/italics/underline).
86
- // TODO add support for color rendering.
87
- const openStyleTags = [];
88
- const bold = cue.fontWeight >= shaka.text.Cue.fontWeight.BOLD;
89
- const italics = cue.fontStyle == shaka.text.Cue.fontStyle.ITALIC;
90
- const underline = cue.textDecoration.includes(
91
- shaka.text.Cue.textDecoration.UNDERLINE);
92
- if (bold) {
93
- openStyleTags.push('b');
94
- }
95
- if (italics) {
96
- openStyleTags.push('i');
97
- }
98
- if (underline) {
99
- openStyleTags.push('u');
100
- }
101
-
102
- // Prefix opens tags, suffix closes tags in reverse order of opening.
103
- const prefixStyleTags = openStyleTags.reduce((acc, tag) => {
104
- return `${acc}<${tag}>`;
105
- }, '');
106
- const suffixStyleTags = openStyleTags.reduceRight((acc, tag) => {
107
- return `${acc}</${tag}>`;
108
- }, '');
109
-
110
- if (cue.lineBreak) {
111
- // This is a vertical lineBreak, so insert a newline.
112
- return '\n';
113
- } else if (cue.nestedCues.length) {
114
- return cue.nestedCues.map(flattenPayload).join('');
115
- } else {
116
- // This is a real cue.
117
- return prefixStyleTags + cue.payload + suffixStyleTags;
118
- }
119
- };
120
-
121
- // We don't want to modify the array or objects passed in, since we don't
122
- // technically own them. So we build a new array and replace certain items
123
- // in it if they need to be flattened.
124
- // We also don't want to flatten the text payloads starting at a container
125
- // element; otherwise, for containers encapsulating multiple caption lines,
126
- // the lines would merge into a single cue. This is undesirable when a
127
- // subset of the captions are outside of the append time window. To fix
128
- // this, we only call flattenPayload() starting at elements marked as
129
- // isContainer = false.
130
- const getCuesToFlatten = (cues, result) => {
131
- for (const cue of cues) {
132
- if (cue.isContainer) {
133
- // Recurse to find the actual text payload cues.
134
- getCuesToFlatten(cue.nestedCues, result);
135
- } else {
136
- // Flatten the payload.
137
- const flatCue = cue.clone();
138
- flatCue.nestedCues = [];
139
- flatCue.payload = flattenPayload(cue);
140
- result.push(flatCue);
141
- }
142
- }
143
- return result;
144
- };
145
- const flattenedCues = getCuesToFlatten(cues, []);
83
+ const flattenedCues = shaka.text.Utils.getCuesToFlatten(cues, []);
146
84
 
147
85
  // Convert cues.
148
86
  const textTrackCues = [];
@@ -0,0 +1,88 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2016 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ goog.provide('shaka.text.Utils');
8
+
9
+ goog.require('shaka.text.Cue');
10
+
11
+
12
+ shaka.text.Utils = class {
13
+ /**
14
+ * Flatten nested cue payloads recursively. If a cue has nested cues,
15
+ * their contents should be combined and replace the payload of the parent.
16
+ *
17
+ * @param {!shaka.text.Cue} cue
18
+ * @return {string}
19
+ * @private
20
+ */
21
+ static flattenPayload_(cue) {
22
+ // Handle styles (currently bold/italics/underline).
23
+ // TODO: add support for color rendering.
24
+ const openStyleTags = [];
25
+ const bold = cue.fontWeight >= shaka.text.Cue.fontWeight.BOLD;
26
+ const italics = cue.fontStyle == shaka.text.Cue.fontStyle.ITALIC;
27
+ const underline = cue.textDecoration.includes(
28
+ shaka.text.Cue.textDecoration.UNDERLINE);
29
+ if (bold) {
30
+ openStyleTags.push('b');
31
+ }
32
+ if (italics) {
33
+ openStyleTags.push('i');
34
+ }
35
+ if (underline) {
36
+ openStyleTags.push('u');
37
+ }
38
+
39
+ // Prefix opens tags, suffix closes tags in reverse order of opening.
40
+ const prefixStyleTags = openStyleTags.reduce((acc, tag) => {
41
+ return `${acc}<${tag}>`;
42
+ }, '');
43
+ const suffixStyleTags = openStyleTags.reduceRight((acc, tag) => {
44
+ return `${acc}</${tag}>`;
45
+ }, '');
46
+
47
+ if (cue.lineBreak) {
48
+ // This is a vertical lineBreak, so insert a newline.
49
+ return '\n';
50
+ } else if (cue.nestedCues.length) {
51
+ return cue.nestedCues.map(shaka.text.Utils.flattenPayload_).join('');
52
+ } else {
53
+ // This is a real cue.
54
+ return prefixStyleTags + cue.payload + suffixStyleTags;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * We don't want to modify the array or objects passed in, since we don't
60
+ * technically own them. So we build a new array and replace certain items
61
+ * in it if they need to be flattened.
62
+ * We also don't want to flatten the text payloads starting at a container
63
+ * element; otherwise, for containers encapsulating multiple caption lines,
64
+ * the lines would merge into a single cue. This is undesirable when a
65
+ * subset of the captions are outside of the append time window. To fix
66
+ * this, we only call flattenPayload() starting at elements marked as
67
+ * isContainer = false.
68
+ *
69
+ * @param {!Array.<!shaka.text.Cue>} cues
70
+ * @param {!Array.<!shaka.text.Cue>} result
71
+ * @return {!Array.<!shaka.text.Cue>}
72
+ */
73
+ static getCuesToFlatten(cues, result) {
74
+ for (const cue of cues) {
75
+ if (cue.isContainer) {
76
+ // Recurse to find the actual text payload cues.
77
+ shaka.text.Utils.getCuesToFlatten(cue.nestedCues, result);
78
+ } else {
79
+ // Flatten the payload.
80
+ const flatCue = cue.clone();
81
+ flatCue.nestedCues = [];
82
+ flatCue.payload = shaka.text.Utils.flattenPayload_(cue);
83
+ result.push(flatCue);
84
+ }
85
+ }
86
+ return result;
87
+ }
88
+ };
@@ -371,6 +371,25 @@ shaka.text.UITextDisplayer = class {
371
371
  }
372
372
  }
373
373
 
374
+ /**
375
+ * Compute a unique internal id:
376
+ * Regions can reuse the id but have different dimensions, we need to
377
+ * consider those differences
378
+ * @param {shaka.extern.CueRegion} region
379
+ * @private
380
+ */
381
+ generateRegionId_(region) {
382
+ const percentageUnit = shaka.text.CueRegion.units.PERCENTAGE;
383
+ const heightUnit = region.heightUnits == percentageUnit ? '%' : 'px';
384
+ const viewportAnchorUnit =
385
+ region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
386
+ const uniqueRegionId = `${region.id}_${
387
+ region.width}x${region.height}${heightUnit}-${
388
+ region.viewportAnchorX}x${region.viewportAnchorY}${viewportAnchorUnit}`;
389
+
390
+ return uniqueRegionId;
391
+ }
392
+
374
393
  /**
375
394
  * Get or create a region element corresponding to the cue region. These are
376
395
  * cached by ID.
@@ -382,8 +401,9 @@ shaka.text.UITextDisplayer = class {
382
401
  getRegionElement_(cue) {
383
402
  const region = cue.region;
384
403
 
385
- if (this.regionElements_.has(region.id)) {
386
- return this.regionElements_.get(region.id);
404
+ const regionId = this.generateRegionId_(region);
405
+ if (this.regionElements_.has(regionId)) {
406
+ return this.regionElements_.get(regionId);
387
407
  }
388
408
 
389
409
  const regionElement = shaka.util.Dom.createHTMLElement('span');
@@ -394,7 +414,7 @@ shaka.text.UITextDisplayer = class {
394
414
  const viewportAnchorUnit =
395
415
  region.viewportAnchorUnits == percentageUnit ? '%' : 'px';
396
416
 
397
- regionElement.id = 'shaka-text-region---' + region.id;
417
+ regionElement.id = 'shaka-text-region---' + regionId;
398
418
  regionElement.classList.add('shaka-text-region');
399
419
 
400
420
  regionElement.style.height = region.height + heightUnit;
@@ -415,7 +435,7 @@ shaka.text.UITextDisplayer = class {
415
435
  regionElement.style.justifyContent = 'flex-end';
416
436
  }
417
437
 
418
- this.regionElements_.set(region.id, regionElement);
438
+ this.regionElements_.set(regionId, regionElement);
419
439
  return regionElement;
420
440
  }
421
441
 
@@ -597,12 +617,7 @@ shaka.text.UITextDisplayer = class {
597
617
  // TODO: Implement lineAlignment of 'CENTER'.
598
618
  if (cue.line != null) {
599
619
  if (cue.lineInterpretation == Cue.lineInterpretation.PERCENTAGE) {
600
- // When setting absolute positioning, you need to set x/y/width/height
601
- // so the element is positioned correctly. Set these as default and
602
- // other settings will override them.
603
620
  style.position = 'absolute';
604
- style.left = '0';
605
- style.top = '0';
606
621
  if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
607
622
  style.width = '100%';
608
623
  if (cue.lineAlign == Cue.lineAlign.START) {