shaka-player 4.3.13 → 4.3.14

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 (124) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/.github/ISSUE_TEMPLATE/question.md +7 -0
  3. package/.github/workflows/release-please.yaml +2 -27
  4. package/AUTHORS +5 -0
  5. package/CHANGELOG.md +87 -0
  6. package/CODE_OF_CONDUCT.md +1 -1
  7. package/CONTRIBUTORS +6 -0
  8. package/README.md +2 -0
  9. package/demo/common/asset.js +0 -1
  10. package/demo/common/assets.js +0 -1
  11. package/demo/custom.js +2 -0
  12. package/dist/demo.compiled.debug.js +1 -1
  13. package/dist/demo.compiled.debug.map +1 -1
  14. package/dist/demo.compiled.js +1 -1
  15. package/dist/demo.compiled.map +1 -1
  16. package/dist/deps.js +6 -6
  17. package/dist/locales.js +29 -29
  18. package/dist/receiver.compiled.debug.map +1 -1
  19. package/dist/receiver.compiled.map +1 -1
  20. package/dist/shaka-player.compiled.d.ts +25 -5
  21. package/dist/shaka-player.compiled.debug.d.ts +25 -5
  22. package/dist/shaka-player.compiled.debug.externs.js +73 -45
  23. package/dist/shaka-player.compiled.debug.js +414 -396
  24. package/dist/shaka-player.compiled.debug.map +4 -4
  25. package/dist/shaka-player.compiled.externs.js +73 -45
  26. package/dist/shaka-player.compiled.js +912 -903
  27. package/dist/shaka-player.compiled.map +4 -4
  28. package/dist/shaka-player.ui.d.ts +37 -6
  29. package/dist/shaka-player.ui.debug.d.ts +37 -6
  30. package/dist/shaka-player.ui.debug.externs.js +74 -45
  31. package/dist/shaka-player.ui.debug.js +438 -418
  32. package/dist/shaka-player.ui.debug.map +4 -4
  33. package/dist/shaka-player.ui.externs.js +74 -45
  34. package/dist/shaka-player.ui.js +1060 -1051
  35. package/dist/shaka-player.ui.map +4 -4
  36. package/docs/tutorials/ad_monetization.md +54 -0
  37. package/docs/tutorials/fairplay.md +2 -2
  38. package/docs/tutorials/index.json +1 -0
  39. package/docs/tutorials/plugins.md +4 -0
  40. package/docs/tutorials/text-displayer.md +15 -0
  41. package/docs/tutorials/ui-customization.md +29 -0
  42. package/docs/tutorials/ui.md +4 -1
  43. package/externs/lcevc.js +6 -0
  44. package/externs/shaka/manifest_parser.js +8 -0
  45. package/externs/shaka/player.js +1 -1
  46. package/externs/shaka/text.js +3 -1
  47. package/lib/abr/simple_abr_manager.js +1 -1
  48. package/lib/ads/client_side_ad.js +9 -0
  49. package/lib/ads/client_side_ad_manager.js +7 -2
  50. package/lib/ads/server_side_ad_manager.js +4 -0
  51. package/lib/cast/cast_proxy.js +18 -0
  52. package/lib/cea/cea608_memory.js +1 -1
  53. package/lib/cea/cea_decoder.js +11 -1
  54. package/lib/dash/dash_parser.js +75 -41
  55. package/lib/dash/mpd_utils.js +39 -27
  56. package/lib/hls/hls_parser.js +90 -27
  57. package/lib/hls/hls_utils.js +5 -5
  58. package/lib/media/content_workarounds.js +30 -0
  59. package/lib/media/drm_engine.js +16 -12
  60. package/lib/media/media_source_engine.js +4 -2
  61. package/lib/media/play_rate_controller.js +1 -0
  62. package/lib/media/presentation_timeline.js +11 -0
  63. package/lib/media/quality_observer.js +1 -1
  64. package/lib/media/segment_reference.js +5 -2
  65. package/lib/media/streaming_engine.js +50 -30
  66. package/lib/media/transmuxer.js +1 -1
  67. package/lib/media/video_wrapper.js +4 -3
  68. package/lib/offline/offline_manifest_parser.js +5 -0
  69. package/lib/offline/session_deleter.js +0 -8
  70. package/lib/offline/storage.js +4 -1
  71. package/lib/player.js +79 -18
  72. package/lib/polyfill/media_capabilities.js +23 -3
  73. package/lib/text/mp4_ttml_parser.js +2 -2
  74. package/lib/text/simple_text_displayer.js +7 -6
  75. package/lib/text/srt_text_parser.js +2 -2
  76. package/lib/text/text_engine.js +3 -2
  77. package/lib/text/ttml_text_parser.js +33 -5
  78. package/lib/text/ui_text_displayer.js +81 -15
  79. package/lib/text/vtt_text_parser.js +15 -2
  80. package/lib/util/buffer_utils.js +26 -4
  81. package/lib/util/cmcd_manager.js +10 -0
  82. package/lib/util/fairplay_utils.js +1 -1
  83. package/lib/util/manifest_parser_utils.js +14 -8
  84. package/lib/util/object_utils.js +1 -1
  85. package/lib/util/periods.js +91 -36
  86. package/lib/util/platform.js +39 -2
  87. package/lib/util/player_configuration.js +5 -3
  88. package/lib/util/stream_utils.js +36 -28
  89. package/lib/util/xml_utils.js +5 -0
  90. package/package.json +2 -2
  91. package/test/cea/cea_decoder_unit.js +40 -0
  92. package/test/dash/dash_parser_live_unit.js +5 -8
  93. package/test/hls/hls_parser_unit.js +50 -3
  94. package/test/media/buffering_observer_unit.js +13 -0
  95. package/test/media/content_workarounds_unit.js +1 -1
  96. package/test/media/drm_engine_unit.js +30 -3
  97. package/test/media/media_source_engine_unit.js +1 -1
  98. package/test/media/transmuxer_integration.js +2 -2
  99. package/test/offline/storage_integration.js +3 -0
  100. package/test/player_integration.js +40 -40
  101. package/test/player_unit.js +31 -0
  102. package/test/polyfill/media_capabilities_unit.js +12 -6
  103. package/test/test/util/fake_drm_engine.js +7 -0
  104. package/test/test/util/layout_tests.js +2 -1
  105. package/test/test/util/simple_fakes.js +2 -2
  106. package/test/test/util/test_scheme.js +3 -0
  107. package/test/test/util/waiter.js +5 -0
  108. package/test/text/mp4_ttml_parser_unit.js +4 -4
  109. package/test/text/simple_text_displayer_unit.js +1 -1
  110. package/test/text/srt_text_parser_unit.js +1 -1
  111. package/test/text/text_engine_unit.js +6 -0
  112. package/test/text/ttml_text_parser_unit.js +49 -18
  113. package/test/text/ui_text_displayer_unit.js +9 -0
  114. package/test/util/buffer_utils_unit.js +1 -1
  115. package/test/util/periods_unit.js +113 -0
  116. package/third_party/closure-uri/uri.js +1 -1
  117. package/ui/externs/ui.js +24 -0
  118. package/ui/language_utils.js +11 -5
  119. package/ui/play_button.js +20 -0
  120. package/ui/playback_rate_selection.js +4 -0
  121. package/ui/presentation_time.js +5 -5
  122. package/ui/small_play_button.js +2 -2
  123. package/ui/volume_bar.js +10 -6
  124. /package/.github/{config.yml → ISSUE_TEMPLATE/config.yml} +0 -0
package/lib/player.js CHANGED
@@ -479,6 +479,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
479
479
  */
480
480
  this.loadEventManager_ = new shaka.util.EventManager();
481
481
 
482
+ /**
483
+ * For listeners scoped to the lifetime of the loaded content.
484
+ * @private {shaka.util.EventManager}
485
+ */
486
+ this.trickPlayEventManager_ = new shaka.util.EventManager();
487
+
482
488
  /** @private {shaka.net.NetworkingEngine} */
483
489
  this.networkingEngine_ = null;
484
490
 
@@ -897,6 +903,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
897
903
  this.loadEventManager_.release();
898
904
  this.loadEventManager_ = null;
899
905
  }
906
+ if (this.trickPlayEventManager_) {
907
+ this.trickPlayEventManager_.release();
908
+ this.trickPlayEventManager_ = null;
909
+ }
900
910
 
901
911
  this.abrManagerFactory_ = null;
902
912
  this.config_ = null;
@@ -1555,6 +1565,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1555
1565
  // be if there was an error and we, by chance, did not have a media element.
1556
1566
  if (has.mediaElement) {
1557
1567
  this.loadEventManager_.removeAll();
1568
+ this.trickPlayEventManager_.removeAll();
1558
1569
  }
1559
1570
 
1560
1571
  // Stop the variant checker timer
@@ -2012,8 +2023,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2012
2023
 
2013
2024
  this.drmEngine_.configure(this.config_.drm);
2014
2025
 
2026
+ const tracksChanged = shaka.util.StreamUtils.applyRestrictions(
2027
+ this.manifest_.variants, this.config_.restrictions, this.maxHwRes_);
2028
+ if (tracksChanged && this.streamingEngine_) {
2029
+ this.onTracksChanged_();
2030
+ }
2031
+
2032
+ const playableVariants = shaka.util.StreamUtils.getPlayableVariants(
2033
+ this.manifest_.variants);
2034
+
2015
2035
  await this.drmEngine_.initForPlayback(
2016
- this.manifest_.variants,
2036
+ playableVariants,
2017
2037
  this.manifest_.offlineSessionIds);
2018
2038
 
2019
2039
  await this.drmEngine_.attach(has.mediaElement);
@@ -2192,6 +2212,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2192
2212
  await Promise.all(createSegmentIndexPromises);
2193
2213
  }
2194
2214
 
2215
+ if (this.parser_ && this.parser_.onInitialVariantChosen) {
2216
+ this.parser_.onInitialVariantChosen(toLazyLoad);
2217
+ }
2218
+
2195
2219
  shaka.Player.applyPlayRange_(this.manifest_.presentationTimeline,
2196
2220
  this.config_.playRangeStart,
2197
2221
  this.config_.playRangeEnd);
@@ -3705,6 +3729,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3705
3729
  shaka.log.alwaysWarn('A trick play rate of 0 is unsupported!');
3706
3730
  return;
3707
3731
  }
3732
+ this.trickPlayEventManager_.removeAll();
3708
3733
 
3709
3734
  if (this.video_.paused) {
3710
3735
  // Our fast forward is implemented with playbackRate and needs the video
@@ -3718,6 +3743,22 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3718
3743
  this.abrManager_.playbackRateChanged(rate);
3719
3744
  this.streamingEngine_.setTrickPlay(Math.abs(rate) > 1);
3720
3745
  }
3746
+ if (this.isLive()) {
3747
+ this.trickPlayEventManager_.listen(this.video_, 'timeupdate', () => {
3748
+ const currentTime = this.video_.currentTime;
3749
+ const seekRange = this.seekRange();
3750
+ const safeSeekOffset = this.config_.streaming.safeSeekOffset;
3751
+ if (rate > 0) {
3752
+ if (currentTime >= seekRange.end) {
3753
+ this.cancelTrickPlay();
3754
+ }
3755
+ } else {
3756
+ if (currentTime <= (seekRange.start + safeSeekOffset)) {
3757
+ this.cancelTrickPlay();
3758
+ }
3759
+ }
3760
+ });
3761
+ }
3721
3762
  }
3722
3763
 
3723
3764
  /**
@@ -3737,6 +3778,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3737
3778
  this.abrManager_.playbackRateChanged(defaultPlaybackRate);
3738
3779
  this.streamingEngine_.setTrickPlay(false);
3739
3780
  }
3781
+ this.trickPlayEventManager_.removeAll();
3740
3782
  }
3741
3783
 
3742
3784
  /**
@@ -4635,7 +4677,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4635
4677
 
4636
4678
  if (this.isLive()) {
4637
4679
  const now = this.getPresentationStartTimeAsDate().valueOf() +
4638
- this.seekRange().end * 1000;
4680
+ element.currentTime * 1000;
4639
4681
  const latency = (Date.now() - now) / 1000;
4640
4682
  this.stats_.setLiveLatency(latency);
4641
4683
  }
@@ -4689,6 +4731,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4689
4731
  shaka.util.Error.Code.CONTENT_NOT_LOADED);
4690
4732
  }
4691
4733
 
4734
+ if (kind != 'subtitles' && kind != 'captions') {
4735
+ shaka.log.alwaysWarn(
4736
+ 'Using a kind value different of `subtitles` or `captions` can ' +
4737
+ 'cause unwanted issues.');
4738
+ }
4739
+
4692
4740
  if (!mimeType) {
4693
4741
  mimeType = await this.getTextMimetype_(uri);
4694
4742
  }
@@ -4707,9 +4755,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4707
4755
  }
4708
4756
  await this.addSrcTrackElement_(uri, language, kind, mimeType, label || '',
4709
4757
  adCuePoints);
4758
+
4759
+ const LanguageUtils = shaka.util.LanguageUtils;
4760
+ const languageNormalized = LanguageUtils.normalize(language);
4761
+
4710
4762
  const textTracks = this.getTextTracks();
4711
4763
  const srcTrack = textTracks.find((t) => {
4712
- return t.language == language &&
4764
+ return LanguageUtils.normalize(t.language) == languageNormalized &&
4713
4765
  t.label == (label || '') &&
4714
4766
  t.kind == kind;
4715
4767
  });
@@ -4871,16 +4923,22 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4871
4923
  vttOffset: 0,
4872
4924
  };
4873
4925
  const data = shaka.util.BufferUtils.toUint8(buffer);
4874
- const cues = TextParser.parseMedia(data, time);
4926
+ const cues = TextParser.parseMedia(data, time, uri);
4875
4927
 
4876
4928
  const references = [];
4877
4929
  for (const cue of cues) {
4878
- const imageUri = shaka.util.ManifestParserUtils.resolveUris(
4879
- [uri], [cue.payload])[0];
4930
+ let uris = null;
4931
+ const getUris = () => {
4932
+ if (uris == null) {
4933
+ uris = shaka.util.ManifestParserUtils.resolveUris(
4934
+ [uri], [cue.payload]);
4935
+ }
4936
+ return uris || [];
4937
+ };
4880
4938
  const reference = new shaka.media.SegmentReference(
4881
4939
  cue.startTime,
4882
4940
  cue.endTime,
4883
- () => [imageUri],
4941
+ getUris,
4884
4942
  /* startByte= */ 0,
4885
4943
  /* endByte= */ null,
4886
4944
  /* initSegmentReference= */ null,
@@ -4888,8 +4946,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4888
4946
  /* appendWindowStart= */ 0,
4889
4947
  /* appendWindowEnd= */ Infinity,
4890
4948
  );
4891
- if (imageUri.includes('#xywh')) {
4892
- const spriteInfo = imageUri.split('#xywh=')[1].split(',');
4949
+ if (cue.payload.includes('#xywh')) {
4950
+ const spriteInfo = cue.payload.split('#xywh=')[1].split(',');
4893
4951
  if (spriteInfo.length === 4) {
4894
4952
  reference.setThumbnailSprite({
4895
4953
  height: parseInt(spriteInfo[3], 10),
@@ -5129,7 +5187,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5129
5187
  vttOffset: 0,
5130
5188
  };
5131
5189
  const data = shaka.util.BufferUtils.toUint8(buffer);
5132
- const cues = obj.parseMedia(data, time);
5190
+ const cues = obj.parseMedia(data, time, /* uri= */ null);
5133
5191
  return shaka.text.WebVttGenerator.convert(cues, adCuePoints);
5134
5192
  }
5135
5193
  throw new shaka.util.Error(
@@ -5248,7 +5306,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5248
5306
  return new shaka.text.UITextDisplayer(
5249
5307
  this.video_, this.videoContainer_);
5250
5308
  } else {
5251
- return new shaka.text.SimpleTextDisplayer(this.video_);
5309
+ return new shaka.text.SimpleTextDisplayer(
5310
+ this.video_, shaka.Player.TextTrackLabel);
5252
5311
  }
5253
5312
  };
5254
5313
  return config;
@@ -6263,6 +6322,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6263
6322
  const restrictedStatuses = shaka.Player.restrictedStatuses_;
6264
6323
  let tracksChanged = false;
6265
6324
 
6325
+ goog.asserts.assert(this.drmEngine_, 'drmEngine should be non-null here.');
6326
+
6266
6327
  // Only filter tracks for keys if we have some key statuses to look at.
6267
6328
  if (keyIds.length) {
6268
6329
  for (const variant of this.manifest_.variants) {
@@ -6278,8 +6339,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6278
6339
 
6279
6340
  for (const keyId of stream.keyIds) {
6280
6341
  const keyStatus = keyStatusMap[isGlobalStatus ? '00' : keyId];
6281
- variant.allowedByKeySystem = variant.allowedByKeySystem &&
6282
- !!keyStatus && !restrictedStatuses.includes(keyStatus);
6342
+ if (keyStatus || this.drmEngine_.hasManifestInitData()) {
6343
+ variant.allowedByKeySystem = variant.allowedByKeySystem &&
6344
+ !!keyStatus && !restrictedStatuses.includes(keyStatus);
6345
+ }
6283
6346
  }
6284
6347
  }
6285
6348
 
@@ -6291,6 +6354,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6291
6354
  } // if (keyIds.size)
6292
6355
 
6293
6356
  if (tracksChanged) {
6357
+ this.onTracksChanged_();
6294
6358
  const variantsUpdated = this.updateAbrManagerVariants_();
6295
6359
  if (!variantsUpdated) {
6296
6360
  return;
@@ -6302,10 +6366,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6302
6366
  shaka.log.debug('Choosing new streams after key status changed');
6303
6367
  this.chooseVariantAndSwitch_();
6304
6368
  }
6305
-
6306
- if (tracksChanged) {
6307
- this.onTracksChanged_();
6308
- }
6309
6369
  }
6310
6370
 
6311
6371
  /**
@@ -6971,7 +7031,8 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6971
7031
  * @define {string} A version number taken from git at compile time.
6972
7032
  * @export
6973
7033
  */
6974
- shaka.Player.version = 'v4.3.13-uncompiled';
7034
+ // eslint-disable-next-line no-useless-concat
7035
+ shaka.Player.version = 'v4.3.14' + '-uncompiled'; // x-release-please-version
6975
7036
 
6976
7037
  // Initialize the deprecation system using the version string we just set
6977
7038
  // on the player.
@@ -141,8 +141,15 @@ shaka.polyfill.MediaCapabilities = class {
141
141
  }
142
142
 
143
143
  if (audioConfig) {
144
- const contentType = audioConfig.contentType;
145
- const isSupported = Capabilities.isTypeSupported(contentType);
144
+ let isSupported;
145
+ if (shaka.util.Platform.isChromecast() &&
146
+ audioConfig.spatialRendering) {
147
+ const extendedType =
148
+ audioConfig.contentType + '; spatialRendering=true';
149
+ isSupported = Capabilities.isTypeSupported(extendedType);
150
+ } else {
151
+ isSupported = Capabilities.isTypeSupported(audioConfig.contentType);
152
+ }
146
153
  if (!isSupported) {
147
154
  return res;
148
155
  }
@@ -184,10 +191,23 @@ shaka.polyfill.MediaCapabilities = class {
184
191
  const videoCapabilities = [];
185
192
 
186
193
  if (mediaCapkeySystemConfig.audio) {
187
- const capability = {
194
+ let capability = {
188
195
  robustness: mediaCapkeySystemConfig.audio.robustness || '',
189
196
  contentType: mediaDecodingConfig.audio.contentType,
190
197
  };
198
+
199
+ // Some Tizen devices seem to misreport AC-3 support, but correctly
200
+ // report EC-3 support. So query EC-3 as a fallback for AC-3.
201
+ // See https://github.com/shaka-project/shaka-player/issues/2989 for
202
+ // details.
203
+ if (shaka.util.Platform.isTizen() &&
204
+ mediaDecodingConfig.audio.contentType.includes('codecs="ac-3"')) {
205
+ capability = {
206
+ robustness: capability.robustness,
207
+ contentType: 'audio/mp4; codecs="ec-3"',
208
+ };
209
+ }
210
+
191
211
  audioCapabilities.push(capability);
192
212
  }
193
213
 
@@ -67,7 +67,7 @@ shaka.text.Mp4TtmlParser = class {
67
67
  * @override
68
68
  * @export
69
69
  */
70
- parseMedia(data, time) {
70
+ parseMedia(data, time, uri) {
71
71
  const Mp4Parser = shaka.util.Mp4Parser;
72
72
 
73
73
  let sawMDAT = false;
@@ -78,7 +78,7 @@ shaka.text.Mp4TtmlParser = class {
78
78
  sawMDAT = true;
79
79
  // Join this to any previous payload, in case the mp4 has multiple
80
80
  // mdats.
81
- payload = payload.concat(this.parser_.parseMedia(data, time));
81
+ payload = payload.concat(this.parser_.parseMedia(data, time, uri));
82
82
  }));
83
83
  parser.parse(data, /* partialOkay= */ false);
84
84
 
@@ -6,7 +6,6 @@
6
6
 
7
7
  /**
8
8
  * @fileoverview
9
- * @suppress {missingRequire} TODO(b/152540451): this shouldn't be needed
10
9
  */
11
10
 
12
11
  goog.provide('shaka.text.SimpleTextDisplayer');
@@ -24,8 +23,11 @@ goog.require('shaka.text.Utils');
24
23
  * @export
25
24
  */
26
25
  shaka.text.SimpleTextDisplayer = class {
27
- /** @param {HTMLMediaElement} video */
28
- constructor(video) {
26
+ /**
27
+ * @param {HTMLMediaElement} video
28
+ * @param {string} label
29
+ */
30
+ constructor(video, label) {
29
31
  /** @private {TextTrack} */
30
32
  this.textTrack_ = null;
31
33
 
@@ -39,7 +41,7 @@ shaka.text.SimpleTextDisplayer = class {
39
41
  // element.
40
42
  track.mode = 'disabled';
41
43
 
42
- if (track.label == shaka.Player.TextTrackLabel) {
44
+ if (track.label == label) {
43
45
  this.textTrack_ = track;
44
46
  }
45
47
  }
@@ -49,8 +51,7 @@ shaka.text.SimpleTextDisplayer = class {
49
51
  // kind to 'subtitles' or 'captions' when creating the TextTrack object.
50
52
  // The individual text tracks from the manifest will still have their own
51
53
  // kinds which can be displayed in the app's UI.
52
- this.textTrack_ = video.addTextTrack(
53
- 'subtitles', shaka.Player.TextTrackLabel);
54
+ this.textTrack_ = video.addTextTrack('subtitles', label);
54
55
  }
55
56
  this.textTrack_.mode = 'hidden';
56
57
  }
@@ -47,7 +47,7 @@ shaka.text.SrtTextParser = class {
47
47
  * @override
48
48
  * @export
49
49
  */
50
- parseMedia(data, time) {
50
+ parseMedia(data, time, uri) {
51
51
  const SrtTextParser = shaka.text.SrtTextParser;
52
52
  const BufferUtils = shaka.util.BufferUtils;
53
53
  const StringUtils = shaka.util.StringUtils;
@@ -59,7 +59,7 @@ shaka.text.SrtTextParser = class {
59
59
 
60
60
  const newData = BufferUtils.toUint8(StringUtils.toUTF8(vvtText));
61
61
 
62
- return this.parser_.parseMedia(newData, time);
62
+ return this.parser_.parseMedia(newData, time, uri);
63
63
  }
64
64
 
65
65
  /**
@@ -160,9 +160,10 @@ shaka.text.TextEngine = class {
160
160
  * @param {BufferSource} buffer
161
161
  * @param {?number} startTime relative to the start of the presentation
162
162
  * @param {?number} endTime relative to the start of the presentation
163
+ * @param {?string=} uri
163
164
  * @return {!Promise}
164
165
  */
165
- async appendBuffer(buffer, startTime, endTime) {
166
+ async appendBuffer(buffer, startTime, endTime, uri) {
166
167
  goog.asserts.assert(
167
168
  this.parser_, 'The parser should already be initialized');
168
169
 
@@ -192,7 +193,7 @@ shaka.text.TextEngine = class {
192
193
 
193
194
  // Parse the buffer and add the new cues.
194
195
  const allCues = this.parser_.parseMedia(
195
- shaka.util.BufferUtils.toUint8(buffer), time);
196
+ shaka.util.BufferUtils.toUint8(buffer), time, uri);
196
197
  const cuesToAppend = allCues.filter((cue) => {
197
198
  return cue.startTime >= this.appendWindowStart_ &&
198
199
  cue.startTime < this.appendWindowEnd_;
@@ -7,6 +7,7 @@
7
7
  goog.provide('shaka.text.TtmlTextParser');
8
8
 
9
9
  goog.require('goog.asserts');
10
+ goog.require('goog.Uri');
10
11
  goog.require('shaka.log');
11
12
  goog.require('shaka.text.Cue');
12
13
  goog.require('shaka.text.CueRegion');
@@ -42,7 +43,7 @@ shaka.text.TtmlTextParser = class {
42
43
  * @override
43
44
  * @export
44
45
  */
45
- parseMedia(data, time) {
46
+ parseMedia(data, time, uri) {
46
47
  const TtmlTextParser = shaka.text.TtmlTextParser;
47
48
  const XmlUtils = shaka.util.XmlUtils;
48
49
  const ttpNs = TtmlTextParser.parameterNs_;
@@ -139,7 +140,7 @@ shaka.text.TtmlTextParser = class {
139
140
  body, time, rateInfo, metadataElements, styles,
140
141
  regionElements, cueRegions, whitespaceTrim,
141
142
  cellResolutionInfo, /* parentCueElement= */ null,
142
- /* isContent= */ false);
143
+ /* isContent= */ false, uri);
143
144
  if (cue) {
144
145
  // According to the TTML spec, backgrounds default to transparent.
145
146
  // So default the background of the top-level element to transparent.
@@ -167,12 +168,14 @@ shaka.text.TtmlTextParser = class {
167
168
  * @param {?{columns: number, rows: number}} cellResolution
168
169
  * @param {?Element} parentCueElement
169
170
  * @param {boolean} isContent
171
+ * @param {?(string|undefined)} uri
170
172
  * @return {shaka.text.Cue}
171
173
  * @private
172
174
  */
173
175
  static parseCue_(
174
176
  cueNode, timeContext, rateInfo, metadataElements, styles, regionElements,
175
- cueRegions, whitespaceTrim, cellResolution, parentCueElement, isContent) {
177
+ cueRegions, whitespaceTrim, cellResolution, parentCueElement, isContent,
178
+ uri) {
176
179
  /** @type {Element} */
177
180
  let cueElement;
178
181
  /** @type {Element} */
@@ -214,7 +217,21 @@ shaka.text.TtmlTextParser = class {
214
217
  }
215
218
  }
216
219
 
217
- if (cueNode.nodeName == 'p' || imageElement) {
220
+ let imageUri = null;
221
+ const backgroundImage = shaka.util.XmlUtils.getAttributeNSList(
222
+ cueElement,
223
+ shaka.text.TtmlTextParser.smpteNsList_,
224
+ 'backgroundImage');
225
+ if (uri && backgroundImage && !backgroundImage.startsWith('#')) {
226
+ const baseUri = new goog.Uri(uri);
227
+ const relativeUri = new goog.Uri(backgroundImage);
228
+ const newUri = baseUri.resolve(relativeUri).toString();
229
+ if (newUri) {
230
+ imageUri = newUri;
231
+ }
232
+ }
233
+
234
+ if (cueNode.nodeName == 'p' || imageElement || imageUri) {
218
235
  isContent = true;
219
236
  }
220
237
 
@@ -247,6 +264,7 @@ shaka.text.TtmlTextParser = class {
247
264
  cellResolution,
248
265
  cueElement,
249
266
  isContent,
267
+ uri,
250
268
  );
251
269
 
252
270
  // This node may or may not generate a nested cue.
@@ -380,6 +398,7 @@ shaka.text.TtmlTextParser = class {
380
398
  cueElement,
381
399
  regionElementForStyle,
382
400
  imageElement,
401
+ imageUri,
383
402
  styles,
384
403
  /** isNested= */ parentIsContent, // "nested in a <div>" doesn't count.
385
404
  /** isLeaf= */ (nestedCues.length == 0));
@@ -464,6 +483,11 @@ shaka.text.TtmlTextParser = class {
464
483
  region.viewportAnchorX = region.viewportAnchorX * 100 /
465
484
  globalWidth;
466
485
  }
486
+ } else if (!extent) {
487
+ region.width = 100 - region.viewportAnchorX;
488
+ region.widthUnits = shaka.text.CueRegion.units.PERCENTAGE;
489
+ region.height = 100 - region.viewportAnchorY;
490
+ region.heightUnits = shaka.text.CueRegion.units.PERCENTAGE;
467
491
  }
468
492
 
469
493
  region.viewportAnchorUnits = percentage || globalWidth != null ?
@@ -482,13 +506,15 @@ shaka.text.TtmlTextParser = class {
482
506
  * @param {!Element} cueElement
483
507
  * @param {Element} region
484
508
  * @param {Element} imageElement
509
+ * @param {?string} imageUri
485
510
  * @param {!Array.<!Element>} styles
486
511
  * @param {boolean} isNested
487
512
  * @param {boolean} isLeaf
488
513
  * @private
489
514
  */
490
515
  static addStyle_(
491
- cue, cueElement, region, imageElement, styles, isNested, isLeaf) {
516
+ cue, cueElement, region, imageElement, imageUri, styles,
517
+ isNested, isLeaf) {
492
518
  const TtmlTextParser = shaka.text.TtmlTextParser;
493
519
  const Cue = shaka.text.Cue;
494
520
 
@@ -656,6 +682,8 @@ shaka.text.TtmlTextParser = class {
656
682
  backgroundImageData) {
657
683
  cue.backgroundImage = 'data:image/png;base64,' + backgroundImageData;
658
684
  }
685
+ } else if (imageUri) {
686
+ cue.backgroundImage = imageUri;
659
687
  }
660
688
 
661
689
  const textOutline = TtmlTextParser.getStyleAttribute_(
@@ -44,6 +44,9 @@ shaka.text.UITextDisplayer = class {
44
44
  /** @private {HTMLElement} */
45
45
  this.videoContainer_ = videoContainer;
46
46
 
47
+ /** @private {?number} */
48
+ this.aspectRatio_ = null;
49
+
47
50
  /** @type {HTMLElement} */
48
51
  this.textContainer_ = shaka.util.Dom.createHTMLElement('div');
49
52
  this.textContainer_.classList.add('shaka-text-container');
@@ -90,6 +93,23 @@ shaka.text.UITextDisplayer = class {
90
93
  this.updateCaptions_(/* forceUpdate= */ true);
91
94
  });
92
95
 
96
+ // From: https://html.spec.whatwg.org/multipage/media.html#dom-video-videowidth
97
+ // Whenever the natural width or natural height of the video changes
98
+ // (including, for example, because the selected video track was changed),
99
+ // if the element's readyState attribute is not HAVE_NOTHING, the user
100
+ // agent must queue a media element task given the media element to fire an
101
+ // event named resize at the media element.
102
+ this.eventManager_.listen(this.video_, 'resize', () => {
103
+ const element = /** @type {!HTMLVideoElement} */ (this.video_);
104
+ const width = element.videoWidth;
105
+ const height = element.videoHeight;
106
+ if (width && height) {
107
+ this.aspectRatio_ = width / height;
108
+ } else {
109
+ this.aspectRatio_ = null;
110
+ }
111
+ });
112
+
93
113
  /** @private {ResizeObserver} */
94
114
  this.resizeObserver_ = null;
95
115
  if ('ResizeObserver' in window) {
@@ -134,6 +154,11 @@ shaka.text.UITextDisplayer = class {
134
154
  * @export
135
155
  */
136
156
  destroy() {
157
+ // Return resolved promise if destroy() has been called.
158
+ if (!this.textContainer_) {
159
+ return Promise.resolve();
160
+ }
161
+
137
162
  // Remove the text container element from the UI.
138
163
  this.videoContainer_.removeChild(this.textContainer_);
139
164
  this.textContainer_ = null;
@@ -156,6 +181,8 @@ shaka.text.UITextDisplayer = class {
156
181
  this.resizeObserver_.disconnect();
157
182
  this.resizeObserver_ = null;
158
183
  }
184
+
185
+ return Promise.resolve();
159
186
  }
160
187
 
161
188
 
@@ -400,6 +427,10 @@ shaka.text.UITextDisplayer = class {
400
427
  */
401
428
  getRegionElement_(cue) {
402
429
  const region = cue.region;
430
+ // from https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#caption-window-size
431
+ // if aspect ratio is 4/3, use that value, otherwise, use the 16:9 value
432
+ const lineWidthMultiple = this.aspectRatio_ === 4/3 ? 2.5 : 1.9;
433
+ const lineHeightMultiple = 5.33;
403
434
 
404
435
  const regionId = this.generateRegionId_(region);
405
436
  if (this.regionElements_.has(regionId)) {
@@ -417,11 +448,33 @@ shaka.text.UITextDisplayer = class {
417
448
  regionElement.id = 'shaka-text-region---' + regionId;
418
449
  regionElement.classList.add('shaka-text-region');
419
450
 
420
- regionElement.style.height = region.height + heightUnit;
421
- regionElement.style.width = region.width + widthUnit;
422
451
  regionElement.style.position = 'absolute';
423
- regionElement.style.top = region.viewportAnchorY + viewportAnchorUnit;
424
- regionElement.style.left = region.viewportAnchorX + viewportAnchorUnit;
452
+
453
+ const linesUnit = shaka.text.CueRegion.units.LINES;
454
+ if (region.heightUnits === linesUnit && region.widthUnits === linesUnit) {
455
+ regionElement.style.height = region.height * lineHeightMultiple + '%';
456
+ regionElement.style.width = region.width * lineWidthMultiple + '%';
457
+ } else {
458
+ regionElement.style.height = region.height + heightUnit;
459
+ regionElement.style.width = region.width + widthUnit;
460
+ }
461
+
462
+ if (region.viewportAnchorUnits === linesUnit) {
463
+ // from https://dvcs.w3.org/hg/text-tracks/raw-file/default/608toVTT/608toVTT.html#positioning-in-cea-708
464
+ let top = region.viewportAnchorY / 75 * 100;
465
+ const windowWidth = this.aspectRatio_ === 4/3 ? 160 : 210;
466
+ let left = region.viewportAnchorX / windowWidth * 100;
467
+ // adjust top and left values based on the region anchor and window size
468
+ top -= region.regionAnchorY * region.height * lineHeightMultiple / 100;
469
+ left -= region.regionAnchorX * region.width * lineWidthMultiple / 100;
470
+ regionElement.style.top = top + '%';
471
+ regionElement.style.left = left + '%';
472
+ } else {
473
+ regionElement.style.top = region.viewportAnchorY -
474
+ region.regionAnchorY * region.height / 100 + viewportAnchorUnit;
475
+ regionElement.style.left = region.viewportAnchorX -
476
+ region.regionAnchorX * region.width / 100 + viewportAnchorUnit;
477
+ }
425
478
 
426
479
  regionElement.style.display = 'flex';
427
480
  regionElement.style.flexDirection = 'column';
@@ -600,6 +653,10 @@ shaka.text.UITextDisplayer = class {
600
653
  // Only do this for the text itself.
601
654
  elem.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';
602
655
  }
656
+ const fontFamily = inherit((c) => c.fontFamily);
657
+ if (fontFamily) {
658
+ elem.style.fontFamily = fontFamily;
659
+ }
603
660
  }
604
661
  if (text) {
605
662
  elem.textContent = text;
@@ -653,37 +710,46 @@ shaka.text.UITextDisplayer = class {
653
710
  // (for vertical growing left) is aligned at the line.
654
711
  // TODO: Implement line alignment with line number.
655
712
  // TODO: Implement lineAlignment of 'CENTER'.
656
- if (cue.line != null) {
713
+ let line = cue.line;
714
+ if (line != null) {
657
715
  let lineInterpretation = cue.lineInterpretation;
658
716
  // HACK: the current implementation of UITextDisplayer only handled
659
- // PERCENTAGE, as cue.line=0 is zero, we can interpret it as percentage
660
- // safely.
661
- if (cue.line == 0 &&
662
- lineInterpretation == Cue.lineInterpretation.LINE_NUMBER) {
717
+ // PERCENTAGE, so we need convert LINE_NUMBER to PERCENTAGE
718
+ if (lineInterpretation == Cue.lineInterpretation.LINE_NUMBER) {
663
719
  lineInterpretation = Cue.lineInterpretation.PERCENTAGE;
720
+ let maxLines = 16;
721
+ // The maximum number of lines is different if it is a vertical video.
722
+ if (this.aspectRatio_ && this.aspectRatio_ < 1) {
723
+ maxLines = 32;
724
+ }
725
+ if (line < 0) {
726
+ line = 100 + line / maxLines * 100;
727
+ } else {
728
+ line = line / maxLines * 100;
729
+ }
664
730
  }
665
731
  if (lineInterpretation == Cue.lineInterpretation.PERCENTAGE) {
666
732
  style.position = 'absolute';
667
733
  if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
668
734
  style.width = '100%';
669
735
  if (cue.lineAlign == Cue.lineAlign.START) {
670
- style.top = cue.line + '%';
736
+ style.top = line + '%';
671
737
  } else if (cue.lineAlign == Cue.lineAlign.END) {
672
- style.bottom = (100 - cue.line) + '%';
738
+ style.bottom = (100 - line) + '%';
673
739
  }
674
740
  } else if (cue.writingMode == Cue.writingMode.VERTICAL_LEFT_TO_RIGHT) {
675
741
  style.height = '100%';
676
742
  if (cue.lineAlign == Cue.lineAlign.START) {
677
- style.left = cue.line + '%';
743
+ style.left = line + '%';
678
744
  } else if (cue.lineAlign == Cue.lineAlign.END) {
679
- style.right = (100 - cue.line) + '%';
745
+ style.right = (100 - line) + '%';
680
746
  }
681
747
  } else {
682
748
  style.height = '100%';
683
749
  if (cue.lineAlign == Cue.lineAlign.START) {
684
- style.right = cue.line + '%';
750
+ style.right = line + '%';
685
751
  } else if (cue.lineAlign == Cue.lineAlign.END) {
686
- style.left = (100 - cue.line) + '%';
752
+ style.left = (100 - line) + '%';
687
753
  }
688
754
  }
689
755
  }