shaka-player 3.2.14 → 3.2.16

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 (59) hide show
  1. package/AUTHORS +1 -0
  2. package/CHANGELOG.md +31 -0
  3. package/CONTRIBUTORS +1 -0
  4. package/dist/deps.js +2 -2
  5. package/dist/locales.js +31 -31
  6. package/dist/shaka-player.compiled.d.ts +1 -1
  7. package/dist/shaka-player.compiled.debug.d.ts +1 -1
  8. package/dist/shaka-player.compiled.debug.externs.js +2 -1
  9. package/dist/shaka-player.compiled.debug.js +202 -193
  10. package/dist/shaka-player.compiled.debug.map +3 -3
  11. package/dist/shaka-player.compiled.externs.js +2 -1
  12. package/dist/shaka-player.compiled.js +729 -724
  13. package/dist/shaka-player.compiled.map +3 -3
  14. package/dist/shaka-player.ui.d.ts +1 -1
  15. package/dist/shaka-player.ui.debug.d.ts +1 -1
  16. package/dist/shaka-player.ui.debug.externs.js +2 -1
  17. package/dist/shaka-player.ui.debug.js +241 -232
  18. package/dist/shaka-player.ui.debug.map +3 -3
  19. package/dist/shaka-player.ui.externs.js +2 -1
  20. package/dist/shaka-player.ui.js +847 -843
  21. package/dist/shaka-player.ui.map +3 -3
  22. package/docs/tutorials/welcome.md +2 -2
  23. package/externs/shaka/player.js +2 -1
  24. package/lib/cast/cast_proxy.js +2 -1
  25. package/lib/cast/cast_receiver.js +19 -5
  26. package/lib/cast/cast_sender.js +2 -1
  27. package/lib/cast/cast_utils.js +16 -6
  28. package/lib/cea/cea708_window.js +2 -2
  29. package/lib/dash/segment_template.js +1 -1
  30. package/lib/hls/hls_parser.js +9 -1
  31. package/lib/media/content_workarounds.js +12 -0
  32. package/lib/media/playhead.js +23 -1
  33. package/lib/media/streaming_engine.js +3 -12
  34. package/lib/player.js +24 -11
  35. package/lib/polyfill/media_capabilities.js +59 -9
  36. package/lib/text/ttml_text_parser.js +32 -7
  37. package/lib/text/ui_text_displayer.js +22 -5
  38. package/lib/util/manifest_parser_utils.js +4 -0
  39. package/lib/util/platform.js +21 -2
  40. package/lib/util/stream_utils.js +30 -10
  41. package/package.json +1 -1
  42. package/test/cast/cast_receiver_integration.js +97 -16
  43. package/test/cast/cast_receiver_unit.js +17 -12
  44. package/test/cast/cast_utils_unit.js +1 -0
  45. package/test/dash/dash_parser_manifest_unit.js +1 -0
  46. package/test/hls/hls_parser_unit.js +1 -0
  47. package/test/media/playhead_unit.js +41 -0
  48. package/test/media/streaming_engine_integration.js +13 -0
  49. package/test/media/streaming_engine_unit.js +23 -8
  50. package/test/player_unit.js +87 -0
  51. package/test/polyfill/media_capabilities_unit.js +261 -0
  52. package/test/test/util/simple_fakes.js +3 -0
  53. package/test/text/mp4_ttml_parser_unit.js +4 -4
  54. package/test/text/ttml_text_parser_unit.js +117 -84
  55. package/test/text/ui_text_displayer_unit.js +76 -0
  56. package/test/ui/text_displayer_layout_unit.js +14 -0
  57. package/test/util/stream_utils_unit.js +18 -0
  58. package/test/util/xml_utils_unit.js +6 -1
  59. package/ui/controls.js +5 -2
@@ -29,8 +29,8 @@ You can build Shaka on Linux, Windows, or Mac.
29
29
  To get the sources and compile the library, you will need:
30
30
  * {@link https://git-scm.com/downloads Git v1.9+}
31
31
  * {@link https://www.python.org/downloads/ Python v2.7 or v3.5+}
32
- * {@link https://java.com/en/download/ Java Runtime Environment v8+}
33
- * {@link https://nodejs.org/en/download/ NodeJS v10+}
32
+ * {@link https://java.com/en/download/ Java Runtime Environment v14+}
33
+ * {@link https://nodejs.org/en/download/ NodeJS v14+}
34
34
  * A local web server, such as {@link https://httpd.apache.org/ Apache}
35
35
  * _NOTE: A local web server is required because browsers place restrictions
36
36
  on applications from file:/// URLs._
@@ -123,7 +123,8 @@ shaka.extern.StateChange;
123
123
  * @property {number} manifestTimeSeconds
124
124
  * The amount of time it took to download and parse the manifest.
125
125
  * @property {number} drmTimeSeconds
126
- * The amount of time it took to download the first drm key.
126
+ * The amount of time it took to download the first drm key, and load that key
127
+ * into the drm system.
127
128
  * @property {number} playTime
128
129
  * The total time spent in a playing state in seconds.
129
130
  * @property {number} pauseTime
@@ -694,7 +694,8 @@ shaka.cast.CastProxy = class extends shaka.util.FakeEventTarget {
694
694
  // If we are casting, but the first update has not come in yet, use local
695
695
  // getters, but not local methods.
696
696
  if (this.sender_.isCasting() && !this.sender_.hasRemoteProperties()) {
697
- if (shaka.cast.CastUtils.PlayerGetterMethods[name]) {
697
+ if (shaka.cast.CastUtils.PlayerGetterMethods[name] ||
698
+ shaka.cast.CastUtils.LargePlayerGetterMethods[name]) {
698
699
  const value = /** @type {Object} */(this.localPlayer_)[name];
699
700
  goog.asserts.assert(typeof value == 'function',
700
701
  'only methods on Player');
@@ -518,6 +518,25 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
518
518
  update['video']['muted'] = systemVolume.muted;
519
519
  }
520
520
 
521
+ this.sendMessage_({
522
+ 'type': 'update',
523
+ 'update': update,
524
+ }, this.shakaBus_);
525
+
526
+ // Getters with large outputs each get sent in their own update message.
527
+ for (const name in shaka.cast.CastUtils.LargePlayerGetterMethods) {
528
+ const frequency = shaka.cast.CastUtils.LargePlayerGetterMethods[name];
529
+ if (this.updateNumber_ % frequency == 0) {
530
+ const update = {'player': {}};
531
+ update['player'][name] = /** @type {Object} */ (this.player_)[name]();
532
+
533
+ this.sendMessage_({
534
+ 'type': 'update',
535
+ 'update': update,
536
+ }, this.shakaBus_);
537
+ }
538
+ }
539
+
521
540
  // Only start progressing the update number once data is loaded,
522
541
  // just in case any of the "rarely changing" properties with less frequent
523
542
  // update messages changes significantly during the loading process.
@@ -525,11 +544,6 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget {
525
544
  this.updateNumber_ += 1;
526
545
  }
527
546
 
528
- this.sendMessage_({
529
- 'type': 'update',
530
- 'update': update,
531
- }, this.shakaBus_);
532
-
533
547
  this.maybeSendMediaInfoMessage_();
534
548
  }
535
549
 
@@ -360,7 +360,8 @@ shaka.cast.CastSender = class {
360
360
  return (...args) =>
361
361
  this.remoteAsyncCall_(targetName, property, ...args);
362
362
  }
363
- if (CastUtils.PlayerGetterMethods[property]) {
363
+ if (CastUtils.PlayerGetterMethods[property] ||
364
+ CastUtils.LargePlayerGetterMethods[property]) {
364
365
  return () => this.propertyGetter_(targetName, property);
365
366
  }
366
367
  }
@@ -307,9 +307,6 @@ shaka.cast.CastUtils.PlayerGetterMethods = {
307
307
  'getAudioLanguagesAndRoles': 4,
308
308
  'getBufferFullness': 1,
309
309
  'getBufferedInfo': 2,
310
- // NOTE: The 'getSharedConfiguration' property is not proxied as it would
311
- // not be possible to share a reference.
312
- 'getConfiguration': 4,
313
310
  'getExpiration': 2,
314
311
  'getKeyStatuses': 2,
315
312
  // NOTE: The 'getManifest' property is not proxied, as it is very large.
@@ -318,9 +315,6 @@ shaka.cast.CastUtils.PlayerGetterMethods = {
318
315
  'getPlaybackRate': 2,
319
316
  'getTextLanguages': 4,
320
317
  'getTextLanguagesAndRoles': 4,
321
- 'getTextTracks': 2,
322
- 'getStats': 5,
323
- 'getVariantTracks': 2,
324
318
  'getImageTracks': 2,
325
319
  'getThumbnails': 2,
326
320
  'isAudioOnly': 10,
@@ -334,6 +328,22 @@ shaka.cast.CastUtils.PlayerGetterMethods = {
334
328
  };
335
329
 
336
330
 
331
+ /**
332
+ * Player getter methods with data large enough to be sent in their own update
333
+ * messages, to reduce the size of each message. The format of this is
334
+ * identical to PlayerGetterMethods.
335
+ * @const {!Object.<string, number>}
336
+ */
337
+ shaka.cast.CastUtils.LargePlayerGetterMethods = {
338
+ // NOTE: The 'getSharedConfiguration' property is not proxied as it would
339
+ // not be possible to share a reference.
340
+ 'getConfiguration': 4,
341
+ 'getStats': 5,
342
+ 'getTextTracks': 2,
343
+ 'getVariantTracks': 2,
344
+ };
345
+
346
+
337
347
  /**
338
348
  * Player getter methods that are proxied while casting, but only when casting
339
349
  * a livestream.
@@ -420,7 +420,7 @@ shaka.cea.Cea708Window.TextJustification = {
420
420
  shaka.cea.Cea708Window.MAX_COLS = 42;
421
421
 
422
422
  /**
423
- * Maximum of 15 rows that can be indexed from 0 to 14.
423
+ * Maximum of 16 rows that can be indexed from 0 to 15.
424
424
  * @private @const {number}
425
425
  */
426
- shaka.cea.Cea708Window.MAX_ROWS = 15;
426
+ shaka.cea.Cea708Window.MAX_ROWS = 16;
@@ -67,7 +67,7 @@ shaka.dash.SegmentTemplate = class {
67
67
  },
68
68
  };
69
69
  } else if (info.segmentDuration) {
70
- if (!isUpdate) {
70
+ if (!isUpdate && context.adaptationSet.contentType !== 'image') {
71
71
  context.presentationTimeline.notifyMaxSegmentDuration(
72
72
  info.segmentDuration);
73
73
  context.presentationTimeline.notifyMinSegmentStartTime(
@@ -863,7 +863,7 @@ shaka.hls.HlsParser = class {
863
863
  shaka.log.debug('Guessing audio-only.');
864
864
  type = ContentType.AUDIO;
865
865
  ignoreStream = true;
866
- } else if (res.video.length) {
866
+ } else if (res.video.length && !res.audio.length) {
867
867
  // There are associated video streams. Assume this is audio.
868
868
  shaka.log.debug('Guessing audio-only.');
869
869
  type = ContentType.AUDIO;
@@ -1484,6 +1484,7 @@ shaka.hls.HlsParser = class {
1484
1484
  */
1485
1485
  const discontinuityToMediaSequence = new Map();
1486
1486
 
1487
+ const prevLowLatencyMode = this.lowLatencyMode_;
1487
1488
  let segments;
1488
1489
  try {
1489
1490
  segments = await this.createSegments_(verbatimMediaPlaylistUri,
@@ -1499,6 +1500,12 @@ shaka.hls.HlsParser = class {
1499
1500
  throw error;
1500
1501
  }
1501
1502
 
1503
+ // This happens when autoLowLatencyMode is true, so we need set the
1504
+ // correct lowLatencyPresentationDelay_
1505
+ if (prevLowLatencyMode != this.lowLatencyMode_) {
1506
+ this.determinePresentationType_(playlist);
1507
+ }
1508
+
1502
1509
  const minTimestamp = segments[0].startTime;
1503
1510
  const lastEndTime = segments[segments.length - 1].endTime;
1504
1511
  /** @type {!shaka.media.SegmentIndex} */
@@ -2969,6 +2976,7 @@ shaka.hls.HlsParser.TEXT_EXTENSIONS_TO_MIME_TYPES_ = {
2969
2976
  'm4f': 'application/mp4',
2970
2977
  'cmft': 'application/mp4',
2971
2978
  'vtt': 'text/vtt',
2979
+ 'webvtt': 'txt/vtt',
2972
2980
  'ttml': 'application/ttml+xml',
2973
2981
  };
2974
2982
 
@@ -69,6 +69,18 @@ shaka.media.ContentWorkarounds = class {
69
69
  })
70
70
  .fullBox('encv', onEncryptionMetadataBox)
71
71
  .fullBox('enca', onEncryptionMetadataBox)
72
+ .fullBox('hev1', (box) => {
73
+ boxesToModify.push({
74
+ box,
75
+ newType: ContentWorkarounds.BOX_TYPE_ENCV_,
76
+ });
77
+ })
78
+ .fullBox('hvc1', (box) => {
79
+ boxesToModify.push({
80
+ box,
81
+ newType: ContentWorkarounds.BOX_TYPE_ENCV_,
82
+ });
83
+ })
72
84
  .fullBox('avc1', (box) => {
73
85
  boxesToModify.push({
74
86
  box,
@@ -33,6 +33,14 @@ goog.requireType('shaka.media.PresentationTimeline');
33
33
  * @interface
34
34
  */
35
35
  shaka.media.Playhead = class {
36
+ /**
37
+ * Called when the Player is ready to begin playback. Anything that depends
38
+ * on setStartTime() should be done here, not in the constructor.
39
+ *
40
+ * @see https://github.com/shaka-project/shaka-player/issues/4244
41
+ */
42
+ ready() {}
43
+
36
44
  /**
37
45
  * Set the start time. If the content has already started playback, this will
38
46
  * be ignored.
@@ -76,6 +84,14 @@ shaka.media.SrcEqualsPlayhead = class {
76
84
 
77
85
  /** @private {shaka.util.EventManager} */
78
86
  this.eventManager_ = new shaka.util.EventManager();
87
+ }
88
+
89
+ /** @override */
90
+ ready() {
91
+ goog.asserts.assert(
92
+ this.mediaElement_ != null,
93
+ 'Playhead should not be released before calling ready()',
94
+ );
79
95
 
80
96
  // We listen for the loaded-data-event so that we know when we can
81
97
  // interact with |currentTime|.
@@ -88,6 +104,7 @@ shaka.media.SrcEqualsPlayhead = class {
88
104
  this.eventManager_.listenOnce(this.mediaElement_, 'seeking', () => {
89
105
  this.started_ = true;
90
106
  });
107
+
91
108
  const currentTime = this.mediaElement_.currentTime;
92
109
  // Using the currentTime allows using a negative number in Live HLS
93
110
  const newTime = Math.max(0, currentTime + this.startTime_);
@@ -211,7 +228,12 @@ shaka.media.MediaSourcePlayhead = class {
211
228
  /** @type {shaka.util.Timer} */
212
229
  this.checkWindowTimer_ = new shaka.util.Timer(() => {
213
230
  this.onPollWindow_();
214
- }).tickEvery(/* seconds= */ 0.25);
231
+ });
232
+ }
233
+
234
+ /** @override */
235
+ ready() {
236
+ this.checkWindowTimer_.tickEvery(/* seconds= */ 0.25);
215
237
  }
216
238
 
217
239
  /** @override */
@@ -1355,16 +1355,6 @@ shaka.media.StreamingEngine = class {
1355
1355
  this.mediaStates_.delete(ContentType.TEXT);
1356
1356
  } else if (error.code == shaka.util.Error.Code.QUOTA_EXCEEDED_ERROR) {
1357
1357
  this.handleQuotaExceeded_(mediaState, error);
1358
- } else if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS &&
1359
- error.data && error.data[1] == 404) {
1360
- // The segment could not be found, does not exist, or is not available.
1361
- // In any case just try again.
1362
- // The current segment is not available. Schedule another update to
1363
- // fetch the segment again.
1364
- shaka.log.v2(logPrefix, 'segment not available.');
1365
- mediaState.performingUpdate = false;
1366
- mediaState.updateTimer = null;
1367
- this.scheduleUpdate_(mediaState, 1);
1368
1358
  } else {
1369
1359
  shaka.log.error(logPrefix, 'failed fetch and append: code=' +
1370
1360
  error.code);
@@ -1379,9 +1369,10 @@ shaka.media.StreamingEngine = class {
1379
1369
 
1380
1370
  /**
1381
1371
  * Clear per-stream error states and retry any failed streams.
1372
+ * @param {number} delaySeconds
1382
1373
  * @return {boolean} False if unable to retry.
1383
1374
  */
1384
- retry() {
1375
+ retry(delaySeconds) {
1385
1376
  if (this.destroyer_.destroyed()) {
1386
1377
  shaka.log.error('Unable to retry after StreamingEngine is destroyed!');
1387
1378
  return false;
@@ -1398,7 +1389,7 @@ shaka.media.StreamingEngine = class {
1398
1389
  if (mediaState.hasError) {
1399
1390
  shaka.log.info(logPrefix, 'Retrying after failure...');
1400
1391
  mediaState.hasError = false;
1401
- this.scheduleUpdate_(mediaState, 0.1);
1392
+ this.scheduleUpdate_(mediaState, delaySeconds);
1402
1393
  }
1403
1394
  }
1404
1395
 
package/lib/player.js CHANGED
@@ -1933,6 +1933,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1933
1933
  shaka.util.StreamUtils.variantToTrack(initialVariant));
1934
1934
  }
1935
1935
 
1936
+ this.playhead_.ready();
1937
+
1936
1938
  // Decide if text should be shown automatically.
1937
1939
  // similar to video/audio track, we would skip switch initial text track
1938
1940
  // if user already pick text track (via selectTextTrack api)
@@ -2263,6 +2265,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2263
2265
  HTMLMediaElement.HAVE_METADATA,
2264
2266
  this.loadEventManager_,
2265
2267
  () => {
2268
+ this.playhead_.ready();
2266
2269
  fullyLoaded.resolve();
2267
2270
  });
2268
2271
 
@@ -4792,12 +4795,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4792
4795
  * If the player has loaded content, and streaming seen an error, but the
4793
4796
  * could not resume streaming, this will return <code>false</code>.
4794
4797
  *
4798
+ * @param {number=} retryDelaySeconds
4795
4799
  * @return {boolean}
4796
4800
  * @export
4797
4801
  */
4798
- retryStreaming() {
4802
+ retryStreaming(retryDelaySeconds = 0.1) {
4799
4803
  return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ?
4800
- this.streamingEngine_.retry() :
4804
+ this.streamingEngine_.retry(retryDelaySeconds) :
4801
4805
  false;
4802
4806
  }
4803
4807
 
@@ -4893,17 +4897,26 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4893
4897
  * @private
4894
4898
  */
4895
4899
  defaultStreamingFailureCallback_(error) {
4896
- const retryErrorCodes = [
4897
- shaka.util.Error.Code.BAD_HTTP_STATUS,
4898
- shaka.util.Error.Code.HTTP_ERROR,
4899
- shaka.util.Error.Code.TIMEOUT,
4900
- ];
4900
+ // For live streams, we retry streaming automatically for certain errors.
4901
+ // For VOD streams, all streaming failures are fatal.
4902
+ if (!this.isLive()) {
4903
+ return;
4904
+ }
4901
4905
 
4902
- if (this.isLive() && retryErrorCodes.includes(error.code)) {
4903
- error.severity = shaka.util.Error.Severity.RECOVERABLE;
4906
+ let retryDelaySeconds = null;
4907
+ if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS ||
4908
+ error.code == shaka.util.Error.Code.HTTP_ERROR) {
4909
+ // These errors can be near-instant, so delay a bit before retrying.
4910
+ retryDelaySeconds = 1;
4911
+ } else if (error.code == shaka.util.Error.Code.TIMEOUT) {
4912
+ // We already waited for a timeout, so retry quickly.
4913
+ retryDelaySeconds = 0.1;
4914
+ }
4904
4915
 
4916
+ if (retryDelaySeconds != null) {
4917
+ error.severity = shaka.util.Error.Severity.RECOVERABLE;
4905
4918
  shaka.log.warning('Live streaming error. Retrying automatically...');
4906
- this.retryStreaming();
4919
+ this.retryStreaming(retryDelaySeconds);
4907
4920
  }
4908
4921
  }
4909
4922
 
@@ -6431,7 +6444,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6431
6444
  * @define {string} A version number taken from git at compile time.
6432
6445
  * @export
6433
6446
  */
6434
- shaka.Player.version = 'v3.2.14-uncompiled';
6447
+ shaka.Player.version = 'v3.2.16-uncompiled';
6435
6448
 
6436
6449
  // Initialize the deprecation system using the version string we just set
6437
6450
  // on the player.
@@ -8,6 +8,7 @@ goog.provide('shaka.polyfill.MediaCapabilities');
8
8
 
9
9
  goog.require('shaka.log');
10
10
  goog.require('shaka.polyfill');
11
+ goog.require('shaka.util.Error');
11
12
  goog.require('shaka.util.Platform');
12
13
 
13
14
 
@@ -84,37 +85,48 @@ shaka.polyfill.MediaCapabilities = class {
84
85
  return res;
85
86
  }
86
87
 
88
+ const videoConfig = mediaDecodingConfig['video'];
89
+ const audioConfig = mediaDecodingConfig['audio'];
90
+
87
91
  if (mediaDecodingConfig.type == 'media-source') {
88
92
  if (!shaka.util.Platform.supportsMediaSource()) {
89
93
  return res;
90
94
  }
91
95
  // Use 'MediaSource.isTypeSupported' to check if the stream is supported.
92
- if (mediaDecodingConfig['video']) {
93
- const contentType = mediaDecodingConfig['video'].contentType;
94
- const isSupported = MediaSource.isTypeSupported(contentType);
96
+ // Cast platforms will additionally check canDisplayType(), which
97
+ // accepts extended MIME type parameters.
98
+ // See: https://github.com/shaka-project/shaka-player/issues/4726
99
+ if (videoConfig) {
100
+ let isSupported;
101
+ if (shaka.util.Platform.isChromecast()) {
102
+ isSupported =
103
+ shaka.polyfill.MediaCapabilities.canCastDisplayType_(videoConfig);
104
+ } else {
105
+ isSupported = MediaSource.isTypeSupported(videoConfig.contentType);
106
+ }
95
107
  if (!isSupported) {
96
108
  return res;
97
109
  }
98
110
  }
99
111
 
100
- if (mediaDecodingConfig['audio']) {
101
- const contentType = mediaDecodingConfig['audio'].contentType;
112
+ if (audioConfig) {
113
+ const contentType = audioConfig.contentType;
102
114
  const isSupported = MediaSource.isTypeSupported(contentType);
103
115
  if (!isSupported) {
104
116
  return res;
105
117
  }
106
118
  }
107
119
  } else if (mediaDecodingConfig.type == 'file') {
108
- if (mediaDecodingConfig['video']) {
109
- const contentType = mediaDecodingConfig['video'].contentType;
120
+ if (videoConfig) {
121
+ const contentType = videoConfig.contentType;
110
122
  const isSupported = shaka.util.Platform.supportsMediaType(contentType);
111
123
  if (!isSupported) {
112
124
  return res;
113
125
  }
114
126
  }
115
127
 
116
- if (mediaDecodingConfig['audio']) {
117
- const contentType = mediaDecodingConfig['audio'].contentType;
128
+ if (audioConfig) {
129
+ const contentType = audioConfig.contentType;
118
130
  const isSupported = shaka.util.Platform.supportsMediaType(contentType);
119
131
  if (!isSupported) {
120
132
  return res;
@@ -189,6 +201,44 @@ shaka.polyfill.MediaCapabilities = class {
189
201
 
190
202
  return res;
191
203
  }
204
+
205
+ /**
206
+ * Checks if the given media parameters of the video or audio streams are
207
+ * supported by the Cast platform.
208
+ * @param {!VideoConfiguration} videoConfig The 'video' field of the
209
+ * MediaDecodingConfiguration.
210
+ * @return {boolean} `true` when the stream can be displayed on a Cast device.
211
+ * @private
212
+ */
213
+ static canCastDisplayType_(videoConfig) {
214
+ if (!(window.cast)) {
215
+ shaka.log.error('Expected cast namespace to be available!');
216
+ throw new shaka.util.Error(
217
+ shaka.util.Error.Severity.CRITICAL,
218
+ shaka.util.Error.Category.CAST,
219
+ shaka.util.Error.Code.CAST_API_UNAVAILABLE);
220
+ } else if (!(cast.__platform__ && cast.__platform__.canDisplayType)) {
221
+ shaka.log.warning('Expected cast APIs to be available! Falling back to ' +
222
+ 'MediaSource.isTypeSupported() for type support.');
223
+ return MediaSource.isTypeSupported(videoConfig.contentType);
224
+ }
225
+
226
+ let displayType = videoConfig.contentType;
227
+ if (videoConfig.width && videoConfig.height) {
228
+ displayType +=
229
+ `; width=${videoConfig.width}; height=${videoConfig.height}`;
230
+ }
231
+ if (videoConfig.framerate) {
232
+ displayType += `; framerate=${videoConfig.framerate}`;
233
+ }
234
+ if (videoConfig.transferFunction === 'pq') {
235
+ // A "PQ" transfer function indicates this is an HDR-capable stream;
236
+ // "smpte2084" is the published standard. We need to inform the platform
237
+ // this query is specifically for HDR.
238
+ displayType += '; eotf=smpte2084';
239
+ }
240
+ return cast.__platform__.canDisplayType(displayType);
241
+ }
192
242
  };
193
243
 
194
244
  /**
@@ -128,7 +128,7 @@ shaka.text.TtmlTextParser = class {
128
128
  }
129
129
 
130
130
  const cue = TtmlTextParser.parseCue_(
131
- body, time.periodStart, rateInfo, metadataElements, styles,
131
+ body, time, rateInfo, metadataElements, styles,
132
132
  regionElements, cueRegions, whitespaceTrim,
133
133
  cellResolutionInfo, /* parentCueElement= */ null,
134
134
  /* isContent= */ false);
@@ -149,7 +149,7 @@ shaka.text.TtmlTextParser = class {
149
149
  * Parses a TTML node into a Cue.
150
150
  *
151
151
  * @param {!Node} cueNode
152
- * @param {number} offset
152
+ * @param {shaka.extern.TextParser.TimeContext} timeContext
153
153
  * @param {!shaka.text.TtmlTextParser.RateInfo_} rateInfo
154
154
  * @param {!Array.<!Element>} metadataElements
155
155
  * @param {!Array.<!Element>} styles
@@ -163,7 +163,7 @@ shaka.text.TtmlTextParser = class {
163
163
  * @private
164
164
  */
165
165
  static parseCue_(
166
- cueNode, offset, rateInfo, metadataElements, styles, regionElements,
166
+ cueNode, timeContext, rateInfo, metadataElements, styles, regionElements,
167
167
  cueRegions, whitespaceTrim, cellResolution, parentCueElement, isContent) {
168
168
  /** @type {Element} */
169
169
  let cueElement;
@@ -228,7 +228,7 @@ shaka.text.TtmlTextParser = class {
228
228
  for (const childNode of cueElement.childNodes) {
229
229
  const nestedCue = shaka.text.TtmlTextParser.parseCue_(
230
230
  childNode,
231
- offset,
231
+ timeContext,
232
232
  rateInfo,
233
233
  metadataElements,
234
234
  styles,
@@ -285,15 +285,20 @@ shaka.text.TtmlTextParser = class {
285
285
  if (start == null) {
286
286
  start = 0;
287
287
  }
288
- start += offset;
288
+ start += timeContext.periodStart;
289
289
 
290
290
  // If end is null, that means the duration is effectively infinite.
291
291
  if (end == null) {
292
292
  end = Infinity;
293
293
  } else {
294
- end += offset;
294
+ end += timeContext.periodStart;
295
295
  }
296
296
 
297
+ // Clip times to segment boundaries.
298
+ // https://github.com/shaka-project/shaka-player/issues/4631
299
+ start = Math.max(start, timeContext.segmentStart);
300
+ end = Math.min(end, timeContext.segmentEnd);
301
+
297
302
  if (!hasTimeAttributes && nestedCues.length > 0) {
298
303
  // If no time is defined for this cue, base the timing information on
299
304
  // the time of the nested cues. In the case of multiple nested cues with
@@ -562,8 +567,28 @@ shaka.text.TtmlTextParser = class {
562
567
 
563
568
  const fontFamily = TtmlTextParser.getStyleAttribute_(
564
569
  cueElement, region, styles, 'fontFamily', shouldInheritRegionStyles);
570
+ // See https://github.com/sandflow/imscJS/blob/1.1.3/src/main/js/html.js#L1384
565
571
  if (fontFamily) {
566
- cue.fontFamily = fontFamily;
572
+ switch (fontFamily) {
573
+ case 'monospaceSerif':
574
+ cue.fontFamily = 'Courier New,Liberation Mono,Courier,monospace';
575
+ break;
576
+ case 'proportionalSansSerif':
577
+ cue.fontFamily = 'Arial,Helvetica,Liberation Sans,sans-serif';
578
+ break;
579
+ case 'sansSerif':
580
+ cue.fontFamily = 'sans-serif';
581
+ break;
582
+ case 'monospaceSansSerif':
583
+ cue.fontFamily = 'Consolas,monospace';
584
+ break;
585
+ case 'proportionalSerif':
586
+ cue.fontFamily = 'serif';
587
+ break;
588
+ default:
589
+ cue.fontFamily = fontFamily;
590
+ break;
591
+ }
567
592
  }
568
593
 
569
594
  const fontWeight = TtmlTextParser.getStyleAttribute_(
@@ -200,6 +200,19 @@ shaka.text.UITextDisplayer = class {
200
200
  this.isTextVisible_ = on;
201
201
  }
202
202
 
203
+ /**
204
+ * @private
205
+ */
206
+ isElementUnderTextContainer_(elemToCheck) {
207
+ while (elemToCheck != null) {
208
+ if (elemToCheck == this.textContainer_) {
209
+ return true;
210
+ }
211
+ elemToCheck = elemToCheck.parentElement;
212
+ }
213
+ return false;
214
+ }
215
+
203
216
  /**
204
217
  * @param {!Array.<!shaka.extern.Cue>} cues
205
218
  * @param {!HTMLElement} container
@@ -261,6 +274,9 @@ shaka.text.UITextDisplayer = class {
261
274
  cueRegistry = this.currentCuesMap_.get(cue);
262
275
  wrapper = cueRegistry.wrapper;
263
276
  updateDOM = true;
277
+ } else if (!this.isElementUnderTextContainer_(wrapper)) {
278
+ // We found that the wrapper needs to be in the DOM
279
+ updateDOM = true;
264
280
  }
265
281
  }
266
282
 
@@ -277,6 +293,7 @@ shaka.text.UITextDisplayer = class {
277
293
  const topCue = parents.pop();
278
294
  goog.asserts.assert(topCue == cue, 'Parent cues should be kept in order');
279
295
  }
296
+
280
297
  if (updateDOM) {
281
298
  for (const element of toUproot) {
282
299
  // NOTE: Because we uproot shared region elements, too, we might hit an
@@ -583,7 +600,7 @@ shaka.text.UITextDisplayer = class {
583
600
  // (for vertical growing left) is aligned at the line.
584
601
  // TODO: Implement line alignment with line number.
585
602
  // TODO: Implement lineAlignment of 'CENTER'.
586
- if (cue.line) {
603
+ if (cue.line != null) {
587
604
  if (cue.lineInterpretation == Cue.lineInterpretation.PERCENTAGE) {
588
605
  // When setting absolute positioning, you need to set x/y/width/height
589
606
  // so the element is positioned correctly. Set these as default and
@@ -596,21 +613,21 @@ shaka.text.UITextDisplayer = class {
596
613
  if (cue.lineAlign == Cue.lineAlign.START) {
597
614
  style.top = cue.line + '%';
598
615
  } else if (cue.lineAlign == Cue.lineAlign.END) {
599
- style.bottom = cue.line + '%';
616
+ style.bottom = (100 - cue.line) + '%';
600
617
  }
601
618
  } else if (cue.writingMode == Cue.writingMode.VERTICAL_LEFT_TO_RIGHT) {
602
619
  style.height = '100%';
603
620
  if (cue.lineAlign == Cue.lineAlign.START) {
604
621
  style.left = cue.line + '%';
605
622
  } else if (cue.lineAlign == Cue.lineAlign.END) {
606
- style.right = cue.line + '%';
623
+ style.right = (100 - cue.line) + '%';
607
624
  }
608
625
  } else {
609
626
  style.height = '100%';
610
627
  if (cue.lineAlign == Cue.lineAlign.START) {
611
628
  style.right = cue.line + '%';
612
629
  } else if (cue.lineAlign == Cue.lineAlign.END) {
613
- style.left = cue.line + '%';
630
+ style.left = (100 - cue.line) + '%';
614
631
  }
615
632
  }
616
633
  }
@@ -620,7 +637,7 @@ shaka.text.UITextDisplayer = class {
620
637
 
621
638
  // The position defines the indent of the text container in the
622
639
  // direction defined by the writing direction.
623
- if (cue.position) {
640
+ if (cue.position != null) {
624
641
  if (cue.writingMode == Cue.writingMode.HORIZONTAL_TOP_TO_BOTTOM) {
625
642
  style.paddingLeft = cue.position;
626
643
  } else {
@@ -168,6 +168,7 @@ shaka.util.ManifestParserUtils.VIDEO_CODEC_REGEXPS_ = [
168
168
  /^hvc/,
169
169
  /^vp0?[89]/,
170
170
  /^av01/,
171
+ /^dvhe/,
171
172
  ];
172
173
 
173
174
 
@@ -183,6 +184,9 @@ shaka.util.ManifestParserUtils.AUDIO_CODEC_REGEXPS_ = [
183
184
  /^flac$/,
184
185
  /^mp4a/,
185
186
  /^[ae]c-3$/,
187
+ /^ac-4$/,
188
+ /^dts[cx]$/, // DTS Surround Sound
189
+ /^[du]dts$/, // DTS Surround Sound
186
190
  ];
187
191
 
188
192