shaka-player 4.3.2 → 4.3.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 (52) hide show
  1. package/.github/workflows/selenium-lab-tests.yaml +22 -0
  2. package/CHANGELOG.md +37 -0
  3. package/dist/deps.js +4 -3
  4. package/dist/locales.js +33 -33
  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 +152 -144
  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 +778 -772
  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 +164 -157
  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 +851 -846
  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 +45 -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 +224 -97
  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/media/streaming_engine_unit.js +91 -119
  45. package/test/player_integration.js +27 -0
  46. package/test/player_unit.js +4 -2
  47. package/test/test/util/layout_tests.js +433 -0
  48. package/test/test/util/util.js +0 -120
  49. package/test/{ui → text}/text_displayer_layout_unit.js +59 -235
  50. package/test/text/ui_text_displayer_unit.js +72 -4
  51. package/test/text/vtt_text_parser_unit.js +114 -8
  52. 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) {
@@ -493,7 +493,11 @@ shaka.hls.HlsParser = class {
493
493
  // Now adjust timestamps back to begin at 0.
494
494
  const segmentN = segmentIndex.earliestReference();
495
495
  if (segmentN) {
496
- this.offsetStream_(streamInfo, -segmentN.startTime);
496
+ const streamOffset = -segmentN.startTime;
497
+ // Modify all SegmentReferences equally.
498
+ streamInfo.stream.segmentIndex.offset(streamOffset);
499
+ // Update other parts of streamInfo the same way.
500
+ this.offsetStreamInfo_(streamInfo, streamOffset);
497
501
  }
498
502
  }
499
503
  }
@@ -535,22 +539,29 @@ shaka.hls.HlsParser = class {
535
539
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
536
540
  const segmentIndex = streamInfo.stream.segmentIndex;
537
541
  if (segmentIndex != null) {
542
+ // A segment's startTime should be based on its syncTime vs the lowest
543
+ // syncTime across all streams. The earliest segment sync time from
544
+ // any stream will become presentation time 0. If two streams start
545
+ // e.g. 6 seconds apart in syncTime, then their first segments will
546
+ // also start 6 seconds apart in presentation time.
547
+
538
548
  const segment0 = segmentIndex.earliestReference();
539
549
  if (segment0.syncTime == null) {
540
550
  shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
541
551
  streamInfo.verbatimMediaPlaylistUri,
542
552
  'Expect AV sync issues!');
543
553
  } else {
544
- // The first segment's target startTime should be based entirely on
545
- // its syncTime. The rest of the stream will be based on that
546
- // starting point. The earliest segment sync time from any stream
547
- // will become presentation time 0. If two streams start e.g. 6
548
- // seconds apart in syncTime, then their first segments will also
549
- // start 6 seconds apart in presentation time.
554
+ // Stream metadata are offset by a fixed amount based on the
555
+ // first segment.
550
556
  const segment0TargetTime = segment0.syncTime - lowestSyncTime;
551
557
  const streamOffset = segment0TargetTime - segment0.startTime;
558
+ this.offsetStreamInfo_(streamInfo, streamOffset);
552
559
 
553
- this.offsetStream_(streamInfo, streamOffset);
560
+ // This is computed across all segments separately to manage
561
+ // accumulated drift in durations.
562
+ for (const segment of segmentIndex) {
563
+ segment.syncAgainst(lowestSyncTime);
564
+ }
554
565
  }
555
566
  }
556
567
  }
@@ -561,13 +572,13 @@ shaka.hls.HlsParser = class {
561
572
  * @param {number} offset
562
573
  * @private
563
574
  */
564
- offsetStream_(streamInfo, offset) {
565
- streamInfo.stream.segmentIndex.offset(offset);
566
-
575
+ offsetStreamInfo_(streamInfo, offset) {
576
+ // Adjust our accounting of the maximum timestamp.
567
577
  streamInfo.maxTimestamp += offset;
568
578
  goog.asserts.assert(streamInfo.maxTimestamp >= 0,
569
579
  'Negative maxTimestamp after adjustment!');
570
580
 
581
+ // Update our map from sequence number to start time.
571
582
  const mediaSequenceToStartTime =
572
583
  this.getMediaSequenceToStartTimeFor_(streamInfo);
573
584
  for (const [key, value] of mediaSequenceToStartTime) {
@@ -934,6 +945,8 @@ shaka.hls.HlsParser = class {
934
945
  }
935
946
  });
936
947
 
948
+ const type = shaka.util.ManifestParserUtils.ContentType.TEXT;
949
+
937
950
  // Set the codecs for text streams.
938
951
  for (const tag of subtitleTags) {
939
952
  const groupId = tag.getRequiredAttrValue('GROUP-ID');
@@ -943,6 +956,9 @@ shaka.hls.HlsParser = class {
943
956
  if (textStreamInfos) {
944
957
  for (const textStreamInfo of textStreamInfos) {
945
958
  textStreamInfo.stream.codecs = codecs;
959
+ textStreamInfo.stream.mimeType =
960
+ this.guessMimeTypeBeforeLoading_(type, codecs) ||
961
+ this.guessMimeTypeFallback_(type);
946
962
  }
947
963
  }
948
964
  }
@@ -2621,16 +2637,19 @@ shaka.hls.HlsParser = class {
2621
2637
  position = mediaSequenceNumber + skippedSegments + i;
2622
2638
 
2623
2639
  const discontinuityTag = shaka.hls.Utils.getFirstTagWithName(
2624
- playlist.tags, 'EXT-X-DISCONTINUITY');
2640
+ item.tags, 'EXT-X-DISCONTINUITY');
2625
2641
  if (discontinuityTag) {
2626
2642
  discontinuitySequence++;
2627
2643
  }
2628
2644
 
2629
2645
  // Apply new AES-128 tags as you see them, keeping a running total.
2630
2646
  for (const drmTag of item.tags) {
2631
- if (drmTag.name == 'EXT-X-KEY' &&
2632
- drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
2633
- hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
2647
+ if (drmTag.name == 'EXT-X-KEY') {
2648
+ if (drmTag.getRequiredAttrValue('METHOD') == 'AES-128') {
2649
+ hlsAes128Key = this.parseAES128DrmTag_(drmTag, playlist);
2650
+ } else {
2651
+ hlsAes128Key = undefined;
2652
+ }
2634
2653
  }
2635
2654
  }
2636
2655
 
@@ -2751,6 +2770,16 @@ shaka.hls.HlsParser = class {
2751
2770
  }
2752
2771
  }
2753
2772
 
2773
+ // lowestSyncTime is a value from a previous playlist update. Use it to
2774
+ // set reference start times. If this is the first playlist parse, we will
2775
+ // skip this step, and wait until we have sync time across stream types.
2776
+ const lowestSyncTime = this.lowestSyncTime_;
2777
+ if (someSyncTime && lowestSyncTime != Infinity) {
2778
+ for (const reference of references) {
2779
+ reference.syncAgainst(lowestSyncTime);
2780
+ }
2781
+ }
2782
+
2754
2783
  return references;
2755
2784
  }
2756
2785
 
@@ -3318,7 +3347,7 @@ shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
3318
3347
  'm4f': 'application/mp4',
3319
3348
  'cmft': 'application/mp4',
3320
3349
  'vtt': 'text/vtt',
3321
- 'webvtt': 'txt/vtt',
3350
+ 'webvtt': 'text/vtt',
3322
3351
  'ttml': 'application/ttml+xml',
3323
3352
  };
3324
3353
 
@@ -682,6 +682,8 @@ shaka.media.MediaSourceEngine = class {
682
682
  // into extracting a timestamp for us to align text segments in sequence
683
683
  // mode.
684
684
 
685
+ const duration = this.mediaSource_.duration;
686
+
685
687
  // Timestamps can only be reliably extracted from video, not audio.
686
688
  // Packed audio formats do not have internal timestamps at all.
687
689
  // Prefer video for this when available.
@@ -725,7 +727,7 @@ shaka.media.MediaSourceEngine = class {
725
727
  // Clear the buffer.
726
728
  await this.enqueueOperation_(
727
729
  contentType,
728
- () => this.remove_(contentType, 0, this.mediaSource_.duration));
730
+ () => this.remove_(contentType, 0, duration));
729
731
 
730
732
  // Finally, flush the buffer in case of choppy video start on HLS fMP4.
731
733
  if (contentType == ContentType.VIDEO) {
@@ -737,6 +739,10 @@ shaka.media.MediaSourceEngine = class {
737
739
 
738
740
  // Now switch to sequence mode and fall through to our normal operations.
739
741
  sourceBuffer.mode = SEQUENCE;
742
+
743
+ // When we change the buffer mode the duration is lost, so we need to set
744
+ // it explicitly.
745
+ await this.setDuration(duration);
740
746
  }
741
747
 
742
748
  if (reference && this.sequenceMode_ && contentType != ContentType.TEXT) {
@@ -771,8 +777,13 @@ shaka.media.MediaSourceEngine = class {
771
777
  bufferedBefore, bufferedAfter);
772
778
  if (newBuffered) {
773
779
  const segmentDuration = reference.endTime - reference.startTime;
774
- if (Math.abs(newBuffered.start - reference.startTime) >
775
- segmentDuration / 2) {
780
+ // Check end times instead of start times. We may be overwriting a
781
+ // buffer and only the end changes, and that would be fine.
782
+ // Also, exclude tiny segments. Sometimes alignment segments as small
783
+ // as 33ms are seen in Google DAI content. For such tiny segments,
784
+ // half a segment duration would be no issue.
785
+ const offset = Math.abs(newBuffered.end - reference.endTime);
786
+ if (segmentDuration > 0.100 && offset > segmentDuration / 2) {
776
787
  shaka.log.error('Possible encoding problem detected!',
777
788
  'Unexpected buffered range for reference', reference,
778
789
  'from URIs', reference.getUris(),
@@ -933,8 +944,27 @@ shaka.media.MediaSourceEngine = class {
933
944
  * @return {!Promise}
934
945
  */
935
946
  async resync(contentType, timestampOffset) {
947
+ const ContentType = shaka.util.ManifestParserUtils.ContentType;
948
+
936
949
  goog.asserts.assert(this.sequenceMode_,
937
- 'resyncAudio only used with sequence mode!');
950
+ 'resync only used with sequence mode!');
951
+
952
+ if (contentType == ContentType.TEXT) {
953
+ // This operation is for audio and video only.
954
+ return;
955
+ }
956
+
957
+ // Queue an abort() to help MSE splice together overlapping segments.
958
+ // We set appendWindowEnd when we change periods in DASH content, and the
959
+ // period transition may result in overlap.
960
+ //
961
+ // An abort() also helps with MPEG2-TS. When we append a TS segment, we
962
+ // always enter a PARSING_MEDIA_SEGMENT state and we can't change the
963
+ // timestamp offset. By calling abort(), we reset the state so we can
964
+ // set it.
965
+ this.enqueueOperation_(
966
+ contentType,
967
+ () => this.abort_(contentType));
938
968
  await this.enqueueOperation_(
939
969
  contentType,
940
970
  () => 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
 
@@ -383,6 +384,44 @@ shaka.media.SegmentReference = class {
383
384
  getThumbnailSprite() {
384
385
  return this.thumbnailSprite;
385
386
  }
387
+
388
+ /**
389
+ * Offset the segment reference by a fixed amount.
390
+ *
391
+ * @param {number} offset The amount to add to the segment's start and end
392
+ * times.
393
+ * @export
394
+ */
395
+ offset(offset) {
396
+ this.startTime += offset;
397
+ this.endTime += offset;
398
+ this.trueEndTime += offset;
399
+
400
+ for (const partial of this.partialReferences) {
401
+ partial.startTime += offset;
402
+ partial.endTime += offset;
403
+ partial.trueEndTime += offset;
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Sync this segment against a particular sync time that will serve as "0" in
409
+ * the presentation timeline.
410
+ *
411
+ * @param {number} lowestSyncTime
412
+ * @export
413
+ */
414
+ syncAgainst(lowestSyncTime) {
415
+ if (this.syncTime == null) {
416
+ shaka.log.alwaysError('Sync attempted without sync time!');
417
+ return;
418
+ }
419
+ const desiredStart = this.syncTime - lowestSyncTime;
420
+ const offset = desiredStart - this.startTime;
421
+ if (Math.abs(offset) >= 0.001) {
422
+ this.offset(offset);
423
+ }
424
+ }
386
425
  };
387
426
 
388
427
 
@@ -836,6 +836,7 @@ shaka.media.StreamingEngine = class {
836
836
  // The playhead might be seeking on startup, if a start time is set, so
837
837
  // start "seeked" as true.
838
838
  seeked: true,
839
+ needsResync: false,
839
840
  recovering: false,
840
841
  hasError: false,
841
842
  operation: null,
@@ -1159,6 +1160,11 @@ shaka.media.StreamingEngine = class {
1159
1160
  shaka.log.v1(
1160
1161
  logPrefix, 'looking up segment from new stream endTime:', time);
1161
1162
 
1163
+ // Using a new iterator means we need to resync the stream in sequence
1164
+ // mode. The buffered range might not align perfectly with the last
1165
+ // segment end time, so we may end up repeating a segment. Resyncing
1166
+ // makes this safe to do.
1167
+ mediaState.needsResync = true;
1162
1168
  mediaState.segmentIterator =
1163
1169
  mediaState.stream.segmentIndex.getIteratorForTime(time);
1164
1170
  const ref = mediaState.segmentIterator &&
@@ -1676,7 +1682,9 @@ shaka.media.StreamingEngine = class {
1676
1682
  const lastDiscontinuitySequence =
1677
1683
  mediaState.lastSegmentReference ?
1678
1684
  mediaState.lastSegmentReference.discontinuitySequence : null;
1679
- if (reference.discontinuitySequence != lastDiscontinuitySequence) {
1685
+ if (reference.discontinuitySequence != lastDiscontinuitySequence ||
1686
+ mediaState.needsResync) {
1687
+ mediaState.needsResync = false;
1680
1688
  operations.push(this.playerInterface_.mediaSourceEngine.resync(
1681
1689
  mediaState.type, reference.startTime));
1682
1690
  }
@@ -2281,6 +2289,7 @@ shaka.media.StreamingEngine.PlayerInterface;
2281
2289
  * clearBufferSafeMargin: number,
2282
2290
  * clearingBuffer: boolean,
2283
2291
  * seeked: boolean,
2292
+ * needsResync: boolean,
2284
2293
  * adaptation: boolean,
2285
2294
  * recovering: boolean,
2286
2295
  * hasError: boolean,
@@ -2326,10 +2335,13 @@ shaka.media.StreamingEngine.PlayerInterface;
2326
2335
  * The amount of buffer to retain when clearing the buffer after the update.
2327
2336
  * @property {boolean} clearingBuffer
2328
2337
  * True indicates that the buffer is being cleared.
2329
- * @property {boolean} seeked
2338
+ * @property {boolean} seeked
2330
2339
  * True indicates that the presentation just seeked.
2331
- * @property {boolean} adaptation
2340
+ * @property {boolean} adaptation
2332
2341
  * True indicates that the presentation just automatically switched variants.
2342
+ * @property {boolean} needsResync
2343
+ * True indicates that the stream needs to be resynced in sequence mode,
2344
+ * regardless of discontinuity sequence.
2333
2345
  * @property {boolean} recovering
2334
2346
  * True indicates that the last segment was not appended because it could not
2335
2347
  * fit in the buffer.
package/lib/player.js CHANGED
@@ -1537,6 +1537,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1537
1537
  has.startTime = null;
1538
1538
  has.uri = null;
1539
1539
 
1540
+ // Release the region timeline, which is created when parsing the manifest.
1541
+ if (this.regionTimeline_) {
1542
+ this.regionTimeline_.release();
1543
+ this.regionTimeline_ = null;
1544
+ }
1545
+
1540
1546
  // In most cases we should have a media element. The one exception would
1541
1547
  // be if there was an error and we, by chance, did not have a media element.
1542
1548
  if (has.mediaElement) {
@@ -6978,7 +6984,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6978
6984
  * @define {string} A version number taken from git at compile time.
6979
6985
  * @export
6980
6986
  */
6981
- shaka.Player.version = 'v4.3.2-uncompiled';
6987
+ shaka.Player.version = 'v4.3.4-uncompiled';
6982
6988
 
6983
6989
  // Initialize the deprecation system using the version string we just set
6984
6990
  // on the player.
@@ -48,6 +48,8 @@ shaka.polyfill.MediaCapabilities = class {
48
48
  // should always install polyfill for WebOS browsers.
49
49
  // Since MediaCapabilities implementation is buggy in EOS browsers, we
50
50
  // should always install polyfill for EOS browsers.
51
+ // Since MediaCapabilities implementation is buggy in Hisense browsers, we
52
+ // should always install polyfill for Hisense browsers.
51
53
  let canUseNativeMCap = true;
52
54
  if (shaka.util.Platform.isApple() ||
53
55
  shaka.util.Platform.isPS5() ||
@@ -55,7 +57,8 @@ shaka.polyfill.MediaCapabilities = class {
55
57
  shaka.util.Platform.isWebOS() ||
56
58
  shaka.util.Platform.isTizen() ||
57
59
  shaka.util.Platform.isChromecast() ||
58
- shaka.util.Platform.isEOS()) {
60
+ shaka.util.Platform.isEOS() ||
61
+ shaka.util.Platform.isHisense()) {
59
62
  canUseNativeMCap = false;
60
63
  }
61
64
  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) {