shaka-player 3.2.15 → 3.2.17

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 (42) hide show
  1. package/.github/workflows/build-and-test.yaml +13 -0
  2. package/AUTHORS +2 -0
  3. package/CHANGELOG.md +28 -0
  4. package/CONTRIBUTORS +2 -0
  5. package/dist/deps.js +1 -1
  6. package/dist/locales.js +30 -30
  7. package/dist/shaka-player.compiled.debug.js +184 -176
  8. package/dist/shaka-player.compiled.debug.map +3 -3
  9. package/dist/shaka-player.compiled.js +707 -704
  10. package/dist/shaka-player.compiled.map +3 -3
  11. package/dist/shaka-player.ui.debug.js +221 -213
  12. package/dist/shaka-player.ui.debug.map +3 -3
  13. package/dist/shaka-player.ui.js +820 -818
  14. package/dist/shaka-player.ui.map +3 -3
  15. package/docs/tutorials/welcome.md +2 -2
  16. package/externs/shaka/player.js +2 -1
  17. package/lib/ads/ad_manager.js +2 -2
  18. package/lib/cea/cea708_window.js +2 -2
  19. package/lib/dash/segment_template.js +1 -1
  20. package/lib/hls/hls_parser.js +8 -1
  21. package/lib/media/content_workarounds.js +12 -0
  22. package/lib/media/streaming_engine.js +20 -8
  23. package/lib/player.js +11 -2
  24. package/lib/polyfill/media_capabilities.js +59 -9
  25. package/lib/text/ttml_text_parser.js +32 -7
  26. package/lib/text/ui_text_displayer.js +22 -5
  27. package/lib/util/platform.js +21 -2
  28. package/lib/util/stream_utils.js +30 -10
  29. package/package.json +1 -1
  30. package/test/dash/dash_parser_manifest_unit.js +1 -0
  31. package/test/hls/hls_parser_unit.js +1 -0
  32. package/test/player_integration.js +10 -3
  33. package/test/polyfill/media_capabilities_unit.js +261 -0
  34. package/test/test/boot.js +29 -11
  35. package/test/test/util/test_scheme.js +2 -2
  36. package/test/text/mp4_ttml_parser_unit.js +4 -4
  37. package/test/text/ttml_text_parser_unit.js +117 -84
  38. package/test/text/ui_text_displayer_unit.js +76 -0
  39. package/test/ui/text_displayer_layout_unit.js +14 -0
  40. package/test/util/stream_utils_unit.js +18 -0
  41. package/test/util/xml_utils_unit.js +6 -1
  42. package/ui/controls.js +1 -1
@@ -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
@@ -19,8 +19,8 @@ goog.require('shaka.util.IReleasable');
19
19
 
20
20
 
21
21
  /**
22
- * @event shaka.ads.AdManager.ADS_LOADED
23
- * @description Fired when an ad has started playing.
22
+ * @event shaka.ads.AdManager.AdsLoadedEvent
23
+ * @description Fired when a sequence of ads has been loaded.
24
24
  * @property {string} type
25
25
  * 'ads-loaded'
26
26
  * @property {number} loadTime
@@ -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} */
@@ -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,
@@ -598,6 +598,11 @@ shaka.media.StreamingEngine = class {
598
598
  * within the presentation timeline.
599
599
  */
600
600
  seeked() {
601
+ if (!this.playerInterface_) {
602
+ // Already destroyed.
603
+ return;
604
+ }
605
+
601
606
  const presentationTime = this.playerInterface_.getPresentationTime();
602
607
  const smallGapLimit = this.config_.smallGapLimit;
603
608
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
@@ -652,7 +657,7 @@ shaka.media.StreamingEngine = class {
652
657
 
653
658
  /**
654
659
  * Clear the buffer for a given stream. Unlike clearBuffer_, this will handle
655
- * cases where a MediaState is performing an update. After this runs, every
660
+ * cases where a MediaState is performing an update. After this runs, the
656
661
  * MediaState will have a pending update.
657
662
  * @param {!shaka.media.StreamingEngine.MediaState_} mediaState
658
663
  * @private
@@ -767,9 +772,9 @@ shaka.media.StreamingEngine = class {
767
772
  for (const type of streamsByType.keys()) {
768
773
  const stream = streamsByType.get(type);
769
774
  if (!this.mediaStates_.has(type)) {
770
- const state = this.createMediaState_(stream);
771
- this.mediaStates_.set(type, state);
772
- this.scheduleUpdate_(state, 0);
775
+ const mediaState = this.createMediaState_(stream);
776
+ this.mediaStates_.set(type, mediaState);
777
+ this.scheduleUpdate_(mediaState, 0);
773
778
  }
774
779
  }
775
780
  }
@@ -873,7 +878,7 @@ shaka.media.StreamingEngine = class {
873
878
  // We switched streams while in the middle of this async call to
874
879
  // createSegmentIndex. Abandon this update and schedule a new one if
875
880
  // there's not already one pending.
876
- if (mediaState.updateTimer == null) {
881
+ if (!mediaState.performingUpdate && !mediaState.updateTimer) {
877
882
  this.scheduleUpdate_(mediaState, 0);
878
883
  }
879
884
  return;
@@ -1341,7 +1346,7 @@ shaka.media.StreamingEngine = class {
1341
1346
  // If the network slows down, abort the current fetch request and start
1342
1347
  // a new one, and ignore the error message.
1343
1348
  mediaState.performingUpdate = false;
1344
- mediaState.updateTimer = null;
1349
+ this.cancelUpdate_(mediaState);
1345
1350
  this.scheduleUpdate_(mediaState, 0);
1346
1351
  } else if (mediaState.type == ContentType.TEXT &&
1347
1352
  this.config_.ignoreTextStreamFailures) {
@@ -1386,7 +1391,10 @@ shaka.media.StreamingEngine = class {
1386
1391
 
1387
1392
  for (const mediaState of this.mediaStates_.values()) {
1388
1393
  const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
1389
- if (mediaState.hasError) {
1394
+ // Only schedule an update if it has an error, but it's not mid-update
1395
+ // and there is not already an update scheduled.
1396
+ if (mediaState.hasError && !mediaState.performingUpdate &&
1397
+ !mediaState.updateTimer) {
1390
1398
  shaka.log.info(logPrefix, 'Retrying after failure...');
1391
1399
  mediaState.hasError = false;
1392
1400
  this.scheduleUpdate_(mediaState, delaySeconds);
@@ -1851,7 +1859,11 @@ shaka.media.StreamingEngine = class {
1851
1859
  shaka.log.debug(logPrefix, 'cleared buffer');
1852
1860
  mediaState.clearingBuffer = false;
1853
1861
  mediaState.endOfStream = false;
1854
- this.scheduleUpdate_(mediaState, 0);
1862
+ // Since the clear operation was async, check to make sure we're not doing
1863
+ // another update and we don't have one scheduled yet.
1864
+ if (!mediaState.performingUpdate && !mediaState.updateTimer) {
1865
+ this.scheduleUpdate_(mediaState, 0);
1866
+ }
1855
1867
  }
1856
1868
 
1857
1869
 
package/lib/player.js CHANGED
@@ -3952,16 +3952,25 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3952
3952
  return [];
3953
3953
  }
3954
3954
  const chapters = [];
3955
+ const uniqueChapters = new Set();
3955
3956
  for (const chaptersTrack of chaptersTracksWithLanguage) {
3956
3957
  if (chaptersTrack && chaptersTrack.cues) {
3957
3958
  for (const cue of chaptersTrack.cues) {
3959
+ let id = cue.id;
3960
+ if (!id || id == '') {
3961
+ id = cue.startTime + '-' + cue.endTime + '-' + cue.text;
3962
+ }
3958
3963
  /** @type {shaka.extern.Chapter} */
3959
3964
  const chapter = {
3965
+ id: id,
3960
3966
  title: cue.text,
3961
3967
  startTime: cue.startTime,
3962
3968
  endTime: cue.endTime,
3963
3969
  };
3964
- chapters.push(chapter);
3970
+ if (!uniqueChapters.has(id)) {
3971
+ chapters.push(chapter);
3972
+ uniqueChapters.add(id);
3973
+ }
3965
3974
  }
3966
3975
  }
3967
3976
  }
@@ -6444,7 +6453,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6444
6453
  * @define {string} A version number taken from git at compile time.
6445
6454
  * @export
6446
6455
  */
6447
- shaka.Player.version = 'v3.2.15-uncompiled';
6456
+ shaka.Player.version = 'v3.2.17-uncompiled';
6448
6457
 
6449
6458
  // Initialize the deprecation system using the version string we just set
6450
6459
  // 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 {
@@ -184,7 +184,7 @@ shaka.util.Platform = class {
184
184
  * @return {?number} A major version number or null if not Chromium-based.
185
185
  */
186
186
  static chromeVersion() {
187
- if (!shaka.util.Platform.userAgentContains_('Chrome')) {
187
+ if (!shaka.util.Platform.isChrome()) {
188
188
  return null;
189
189
  }
190
190
 
@@ -223,7 +223,9 @@ shaka.util.Platform = class {
223
223
  return !!navigator.vendor && navigator.vendor.includes('Apple') &&
224
224
  !shaka.util.Platform.isTizen() &&
225
225
  !shaka.util.Platform.isEOS() &&
226
- !shaka.util.Platform.isPS4();
226
+ !shaka.util.Platform.isVirginMedia() &&
227
+ !shaka.util.Platform.isPS4() &&
228
+ !shaka.util.Platform.isAmazonFireTV();
227
229
  }
228
230
 
229
231
  /**
@@ -246,6 +248,23 @@ shaka.util.Platform = class {
246
248
  return shaka.util.Platform.userAgentContains_('PlayStation 4');
247
249
  }
248
250
 
251
+ /**
252
+ * Check if the current platform is Virgin Media device.
253
+ */
254
+ static isVirginMedia() {
255
+ return shaka.util.Platform.userAgentContains_('VirginMedia');
256
+ }
257
+
258
+ /**
259
+ * Check if the current platform is Amazon Fire TV.
260
+ * https://developer.amazon.com/docs/fire-tv/identify-amazon-fire-tv-devices.html
261
+ *
262
+ * @return {boolean}
263
+ */
264
+ static isAmazonFireTV() {
265
+ return shaka.util.Platform.userAgentContains_('AFT');
266
+ }
267
+
249
268
  /**
250
269
  * Returns a major version number for Safari, or Safari-based iOS browsers.
251
270
  *
@@ -431,15 +431,20 @@ shaka.util.StreamUtils = class {
431
431
  const video = variant.video;
432
432
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
433
433
  if (video) {
434
- let videoCodecs = shaka.util.StreamUtils.patchVp9(video.codecs);
434
+ let videoCodecs =
435
+ shaka.util.StreamUtils.getCorrectVideoCodecs_(video.codecs);
435
436
  // For multiplexed streams. Here we must check the audio of the
436
437
  // stream to see if it is compatible.
437
438
  if (video.codecs.includes(',')) {
438
439
  const allCodecs = video.codecs.split(',');
439
440
  videoCodecs = shaka.util.ManifestParserUtils.guessCodecs(
440
441
  ContentType.VIDEO, allCodecs);
441
- const audioCodecs = shaka.util.ManifestParserUtils.guessCodecs(
442
+ videoCodecs =
443
+ shaka.util.StreamUtils.getCorrectVideoCodecs_(videoCodecs);
444
+ let audioCodecs = shaka.util.ManifestParserUtils.guessCodecs(
442
445
  ContentType.AUDIO, allCodecs);
446
+ audioCodecs =
447
+ shaka.util.StreamUtils.getCorrectAudioCodecs_(audioCodecs);
443
448
  const audioFullType = shaka.util.MimeUtils.getFullOrConvertedType(
444
449
  video.mimeType, audioCodecs, ContentType.AUDIO);
445
450
  if (!MediaSource.isTypeSupported(audioFullType)) {
@@ -559,7 +564,8 @@ shaka.util.StreamUtils = class {
559
564
  const allCodecs = video.codecs.split(',');
560
565
  videoCodecs = shaka.util.ManifestParserUtils.guessCodecs(
561
566
  ContentType.VIDEO, allCodecs);
562
- videoCodecs = shaka.util.StreamUtils.patchVp9(videoCodecs);
567
+ videoCodecs =
568
+ shaka.util.StreamUtils.getCorrectVideoCodecs_(videoCodecs);
563
569
  const audioCodecs = shaka.util.ManifestParserUtils.guessCodecs(
564
570
  ContentType.AUDIO, allCodecs);
565
571
 
@@ -573,7 +579,7 @@ shaka.util.StreamUtils = class {
573
579
  spatialRendering: false,
574
580
  };
575
581
  }
576
- videoCodecs = shaka.util.StreamUtils.patchVp9(videoCodecs);
582
+ videoCodecs = shaka.util.StreamUtils.getCorrectVideoCodecs_(videoCodecs);
577
583
  const fullType = shaka.util.MimeUtils.getFullOrConvertedType(
578
584
  video.mimeType, videoCodecs, ContentType.VIDEO);
579
585
  // VideoConfiguration
@@ -742,15 +748,29 @@ shaka.util.StreamUtils = class {
742
748
 
743
749
 
744
750
  /**
745
- * MediaCapabilities supports 'vp09...' codecs, but not 'vp9'. Translate vp9
746
- * codec strings into 'vp09...', to allow such content to play with
747
- * mediaCapabilities enabled.
748
- *
751
+ * Generates the correct video codec for MediaDecodingConfiguration and
752
+ * for MediaSource.isTypeSupported.
749
753
  * @param {string} codec
750
754
  * @return {string}
755
+ * @private
751
756
  */
752
- static patchVp9(codec) {
753
- if (codec == 'vp9') {
757
+ static getCorrectVideoCodecs_(codec) {
758
+ if (codec.includes('avc1')) {
759
+ // Convert avc1 codec string from RFC-4281 to RFC-6381 for
760
+ // MediaSource.isTypeSupported
761
+ // Example, convert avc1.66.30 to avc1.42001e (0x42 == 66 and 0x1e == 30)
762
+ const avcdata = codec.split('.');
763
+ if (avcdata.length == 3) {
764
+ let result = avcdata.shift() + '.';
765
+ result += parseInt(avcdata.shift(), 10).toString(16);
766
+ result +=
767
+ ('000' + parseInt(avcdata.shift(), 10).toString(16)).slice(-4);
768
+ return result;
769
+ }
770
+ } else if (codec == 'vp9') {
771
+ // MediaCapabilities supports 'vp09...' codecs, but not 'vp9'. Translate
772
+ // vp9 codec strings into 'vp09...', to allow such content to play with
773
+ // mediaCapabilities enabled.
754
774
  // This means profile 0, level 4.1, 8-bit color. This supports 1080p @
755
775
  // 60Hz. See https://en.wikipedia.org/wiki/VP9#Levels
756
776
  //
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shaka-player",
3
3
  "description": "DASH/EME video player library",
4
- "version": "3.2.15",
4
+ "version": "3.2.17",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -2103,6 +2103,7 @@ describe('DashParser Manifest', () => {
2103
2103
  /** @type {shaka.extern.Manifest} */
2104
2104
  const manifest = await parser.start('dummy://foo', playerInterface);
2105
2105
  expect(manifest.imageStreams.length).toBe(1);
2106
+ expect(manifest.presentationTimeline.getMaxSegmentDuration()).toBe(1);
2106
2107
  const imageStream = manifest.imageStreams[0];
2107
2108
  expect(imageStream.width).toBe(1024);
2108
2109
  expect(imageStream.height).toBe(1152);
@@ -37,6 +37,7 @@ describe('HlsParser', () => {
37
37
 
38
38
  afterEach(() => {
39
39
  shaka.log.alwaysWarn = originalAlwaysWarn;
40
+ parser.stop();
40
41
  });
41
42
 
42
43
  beforeEach(() => {