shaka-player 4.3.5 → 4.3.7

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 (102) hide show
  1. package/.github/workflows/demo-version-index.yaml +3 -3
  2. package/.github/workflows/selenium-lab-tests.yaml +23 -1
  3. package/AUTHORS +2 -1
  4. package/CHANGELOG.md +53 -0
  5. package/CONTRIBUTORS +4 -1
  6. package/README.md +3 -0
  7. package/demo/demo.less +1 -0
  8. package/dist/controls.css +2 -2
  9. package/dist/controls.css.map +1 -1
  10. package/dist/demo.compiled.debug.js +167 -296
  11. package/dist/demo.compiled.debug.map +4 -4
  12. package/dist/demo.compiled.js +167 -296
  13. package/dist/demo.compiled.map +4 -4
  14. package/dist/demo.css +1 -1
  15. package/dist/demo.css.map +1 -1
  16. package/dist/deps.js +1 -1
  17. package/dist/locales.js +32 -32
  18. package/dist/receiver.compiled.debug.js +34 -63
  19. package/dist/receiver.compiled.debug.map +4 -4
  20. package/dist/receiver.compiled.js +34 -63
  21. package/dist/receiver.compiled.map +4 -4
  22. package/dist/shaka-player.compiled.d.ts +20 -3
  23. package/dist/shaka-player.compiled.debug.d.ts +20 -3
  24. package/dist/shaka-player.compiled.debug.externs.js +29 -14
  25. package/dist/shaka-player.compiled.debug.js +303 -292
  26. package/dist/shaka-player.compiled.debug.map +4 -4
  27. package/dist/shaka-player.compiled.externs.js +29 -14
  28. package/dist/shaka-player.compiled.js +886 -879
  29. package/dist/shaka-player.compiled.map +3 -3
  30. package/dist/shaka-player.ui.d.ts +20 -3
  31. package/dist/shaka-player.ui.debug.d.ts +20 -3
  32. package/dist/shaka-player.ui.debug.externs.js +17 -2
  33. package/dist/shaka-player.ui.debug.js +321 -309
  34. package/dist/shaka-player.ui.debug.map +3 -3
  35. package/dist/shaka-player.ui.externs.js +17 -2
  36. package/dist/shaka-player.ui.js +1027 -1021
  37. package/dist/shaka-player.ui.map +3 -3
  38. package/docs/tutorials/a11y.md +0 -4
  39. package/docs/tutorials/ad_monetization.md +0 -4
  40. package/docs/tutorials/basic-usage.md +0 -5
  41. package/docs/tutorials/config.md +0 -5
  42. package/docs/tutorials/debugging.md +0 -5
  43. package/docs/tutorials/drm-config.md +0 -5
  44. package/docs/tutorials/fairplay.md +2 -2
  45. package/docs/tutorials/license-server-auth.md +0 -5
  46. package/docs/tutorials/license-wrapping.md +0 -5
  47. package/docs/tutorials/network-and-buffering-config.md +0 -5
  48. package/docs/tutorials/ui-customization.md +0 -5
  49. package/docs/tutorials/ui.md +0 -7
  50. package/docs/tutorials/welcome.md +1 -6
  51. package/externs/network_information.js +7 -2
  52. package/externs/shaka/abr_manager.js +6 -0
  53. package/externs/shaka/net.js +1 -0
  54. package/externs/shaka/player.js +13 -7
  55. package/lib/abr/simple_abr_manager.js +27 -3
  56. package/lib/ads/client_side_ad_manager.js +6 -3
  57. package/lib/cea/mp4_cea_parser.js +5 -2
  58. package/lib/dash/dash_parser.js +49 -48
  59. package/lib/debug/log.js +9 -13
  60. package/lib/hls/hls_parser.js +60 -7
  61. package/lib/hls/manifest_text_parser.js +10 -1
  62. package/lib/media/drm_engine.js +9 -6
  63. package/lib/media/media_source_engine.js +33 -13
  64. package/lib/media/presentation_timeline.js +9 -2
  65. package/lib/media/segment_index.js +2 -0
  66. package/lib/media/segment_reference.js +25 -2
  67. package/lib/media/streaming_engine.js +40 -11
  68. package/lib/net/http_fetch_plugin.js +79 -57
  69. package/lib/net/networking_engine.js +1 -0
  70. package/lib/player.js +26 -3
  71. package/lib/polyfill/media_capabilities.js +13 -0
  72. package/lib/text/mp4_vtt_parser.js +4 -0
  73. package/lib/text/text_engine.js +1 -0
  74. package/lib/util/manifest_parser_utils.js +1 -2
  75. package/lib/util/mime_utils.js +11 -0
  76. package/lib/util/periods.js +5 -5
  77. package/lib/util/stream_utils.js +2 -2
  78. package/maintained-branches.md +2 -3
  79. package/package.json +8 -3
  80. package/test/cea/mp4_cea_parser_unit.js +19 -0
  81. package/test/dash/dash_parser_live_unit.js +3 -2
  82. package/test/hls/hls_live_unit.js +3 -1
  83. package/test/hls/hls_parser_unit.js +156 -0
  84. package/test/media/content_workarounds_unit.js +43 -0
  85. package/test/media/drm_engine_integration.js +4 -17
  86. package/test/media/media_source_engine_unit.js +30 -0
  87. package/test/media/streaming_engine_unit.js +6 -1
  88. package/test/net/networking_engine_unit.js +6 -1
  89. package/test/player_integration.js +39 -0
  90. package/test/player_unit.js +57 -0
  91. package/test/polyfill/media_capabilities_unit.js +1 -0
  92. package/test/test/assets/h265-cea-init.mp4 +0 -0
  93. package/test/test/assets/h265-cea-segment.mp4 +0 -0
  94. package/test/test/assets/text-clip-alt.vtt +63 -0
  95. package/test/test/util/fake_drm_engine.js +3 -0
  96. package/test/test/util/fake_media_source_engine.js +4 -0
  97. package/test/test/util/layout_tests.js +4 -3
  98. package/test/test/util/simple_fakes.js +6 -0
  99. package/test/test/util/test_scheme.js +9 -14
  100. package/test/text/mp4_vtt_parser_unit.js +8 -0
  101. package/test/util/mime_utils_unit.js +10 -0
  102. package/ui/less/containers.less +4 -0
@@ -319,6 +319,9 @@ shaka.hls.HlsParser = class {
319
319
  this.presentationTimeline_.setDuration(Math.min(...maxTimestamps));
320
320
  this.playerInterface_.updateDuration();
321
321
  }
322
+ if (stillLive) {
323
+ this.determineDuration_();
324
+ }
322
325
  }
323
326
 
324
327
  /**
@@ -400,9 +403,16 @@ shaka.hls.HlsParser = class {
400
403
  mediaSequenceNumber);
401
404
  stream.segmentIndex.evict(playlistStartTime);
402
405
  }
406
+ const oldSegment = segments[0];
407
+ goog.asserts.assert(oldSegment, 'Should have segments!');
408
+
409
+ streamInfo.minTimestamp = oldSegment.startTime;
410
+
403
411
  const newestSegment = segments[segments.length - 1];
404
412
  goog.asserts.assert(newestSegment, 'Should have segments!');
405
413
 
414
+ streamInfo.maxTimestamp = newestSegment.endTime;
415
+
406
416
  // Once the last segment has been added to the playlist,
407
417
  // #EXT-X-ENDLIST tag will be appended.
408
418
  // If that happened, treat the rest of the EVENT presentation as VOD.
@@ -413,7 +423,6 @@ shaka.hls.HlsParser = class {
413
423
  // Flag this for later. We don't convert the whole presentation into VOD
414
424
  // until we've seen the ENDLIST tag for all active playlists.
415
425
  streamInfo.hasEndList = true;
416
- streamInfo.maxTimestamp = newestSegment.endTime;
417
426
  }
418
427
  }
419
428
 
@@ -576,6 +585,9 @@ shaka.hls.HlsParser = class {
576
585
  * @private
577
586
  */
578
587
  offsetStreamInfo_(streamInfo, offset) {
588
+ // Adjust our accounting of the minimum timestamp.
589
+ streamInfo.minTimestamp += offset;
590
+
579
591
  // Adjust our accounting of the maximum timestamp.
580
592
  streamInfo.maxTimestamp += offset;
581
593
  goog.asserts.assert(streamInfo.maxTimestamp >= 0,
@@ -804,7 +816,7 @@ shaka.hls.HlsParser = class {
804
816
  const PresentationType = shaka.hls.HlsParser.PresentationType_;
805
817
 
806
818
  if (this.presentationType_ == PresentationType.LIVE) {
807
- let segmentAvailabilityDuration = this.getMinDuration_();
819
+ let segmentAvailabilityDuration = this.getLiveDuration_();
808
820
 
809
821
  // This defaults to the presentation delay, which has the effect of
810
822
  // making the live stream unseekable. This is consistent with Apple's
@@ -1458,9 +1470,9 @@ shaka.hls.HlsParser = class {
1458
1470
  let codecs = '';
1459
1471
  /** @type {string} */
1460
1472
  const type = this.getType_(tag);
1461
- // Text does not require a codec.
1462
- if (type != shaka.util.ManifestParserUtils.ContentType.TEXT && groupId &&
1463
- this.groupIdToCodecsMap_.has(groupId)) {
1473
+ if (type == shaka.util.ManifestParserUtils.ContentType.TEXT) {
1474
+ codecs = tag.getAttributeValue('CODECS') || '';
1475
+ } else if (groupId && this.groupIdToCodecsMap_.has(groupId)) {
1464
1476
  codecs = this.groupIdToCodecsMap_.get(groupId);
1465
1477
  }
1466
1478
 
@@ -1655,6 +1667,7 @@ shaka.hls.HlsParser = class {
1655
1667
  verbatimMediaPlaylistUri,
1656
1668
  // These values are filled out or updated after lazy-loading:
1657
1669
  absoluteMediaPlaylistUri: initialMediaPlaylistUri,
1670
+ minTimestamp: 0,
1658
1671
  maxTimestamp: 0,
1659
1672
  mediaSequenceToStartTime: new Map(),
1660
1673
  canSkipSegments: false,
@@ -1700,6 +1713,7 @@ shaka.hls.HlsParser = class {
1700
1713
 
1701
1714
  // Copy values from the real stream info to our initial one.
1702
1715
  streamInfo.absoluteMediaPlaylistUri = absoluteMediaPlaylistUri;
1716
+ streamInfo.minTimestamp = realStreamInfo.minTimestamp;
1703
1717
  streamInfo.maxTimestamp = realStreamInfo.maxTimestamp;
1704
1718
  streamInfo.canSkipSegments = realStreamInfo.canSkipSegments;
1705
1719
  streamInfo.hasEndList = realStreamInfo.hasEndList;
@@ -1736,6 +1750,13 @@ shaka.hls.HlsParser = class {
1736
1750
  }
1737
1751
  }
1738
1752
 
1753
+ if (type == ContentType.TEXT) {
1754
+ const firstSegment = realStream.segmentIndex.get(0);
1755
+ if (firstSegment && firstSegment.initSegmentReference) {
1756
+ stream.mimeType = 'application/mp4';
1757
+ }
1758
+ }
1759
+
1739
1760
  // Add finishing touches to the stream that can only be done once we have
1740
1761
  // more full context on the media as a whole.
1741
1762
  if (this.hasEnoughInfoToFinalizeStreams_()) {
@@ -1826,6 +1847,22 @@ shaka.hls.HlsParser = class {
1826
1847
  return minDuration;
1827
1848
  }
1828
1849
 
1850
+ /**
1851
+ * @return {number}
1852
+ * @private
1853
+ */
1854
+ getLiveDuration_() {
1855
+ let maxTimestamp = Infinity;
1856
+ let minTimestamp = Infinity;
1857
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
1858
+ if (streamInfo.stream.segmentIndex && streamInfo.stream.type != 'text') {
1859
+ maxTimestamp = Math.min(maxTimestamp, streamInfo.maxTimestamp);
1860
+ minTimestamp = Math.min(minTimestamp, streamInfo.minTimestamp);
1861
+ }
1862
+ }
1863
+ return maxTimestamp - minTimestamp;
1864
+ }
1865
+
1829
1866
  /**
1830
1867
  * @param {!Array.<!shaka.extern.Stream>} streams
1831
1868
  * @private
@@ -1971,6 +2008,7 @@ shaka.hls.HlsParser = class {
1971
2008
  this.determinePresentationType_(playlist);
1972
2009
  }
1973
2010
 
2011
+ const firstStartTime = segments[0].startTime;
1974
2012
  const lastEndTime = segments[segments.length - 1].endTime;
1975
2013
  /** @type {!shaka.media.SegmentIndex} */
1976
2014
  const segmentIndex = new shaka.media.SegmentIndex(segments);
@@ -1993,6 +2031,7 @@ shaka.hls.HlsParser = class {
1993
2031
  type,
1994
2032
  verbatimMediaPlaylistUri,
1995
2033
  absoluteMediaPlaylistUri,
2034
+ minTimestamp: firstStartTime,
1996
2035
  maxTimestamp: lastEndTime,
1997
2036
  canSkipSegments,
1998
2037
  hasEndList: false,
@@ -2371,13 +2410,15 @@ shaka.hls.HlsParser = class {
2371
2410
  * @param {!Map.<string, string>} variables
2372
2411
  * @param {string} absoluteMediaPlaylistUri
2373
2412
  * @param {string} type
2413
+ * @param {number} timestampOffset
2374
2414
  * @param {shaka.extern.HlsAes128Key=} hlsAes128Key
2375
2415
  * @return {shaka.media.SegmentReference}
2376
2416
  * @private
2377
2417
  */
2378
2418
  createSegmentReference_(
2379
2419
  initSegmentReference, previousReference, hlsSegment, startTime,
2380
- variables, absoluteMediaPlaylistUri, type, hlsAes128Key) {
2420
+ variables, absoluteMediaPlaylistUri, type, timestampOffset,
2421
+ hlsAes128Key) {
2381
2422
  const tags = hlsSegment.tags;
2382
2423
  const absoluteSegmentUri = this.variableSubstitution_(
2383
2424
  hlsSegment.absoluteUri, variables);
@@ -2489,6 +2530,9 @@ shaka.hls.HlsParser = class {
2489
2530
  /* tileDuration= */ null,
2490
2531
  /* syncTime= */ null,
2491
2532
  partialStatus);
2533
+ if (item.name == 'EXT-X-PRELOAD-HINT') {
2534
+ partial.markAsPreload();
2535
+ }
2492
2536
  partialSegmentRefs.push(partial);
2493
2537
  } // for-loop of hlsSegment.partialSegments
2494
2538
  }
@@ -2547,7 +2591,7 @@ shaka.hls.HlsParser = class {
2547
2591
  startByte,
2548
2592
  endByte,
2549
2593
  initSegmentReference,
2550
- /* timestampOffset= */ 0, // This value is ignored in sequence mode.
2594
+ timestampOffset, // This value is ignored in sequence mode.
2551
2595
  /* appendWindowStart= */ 0,
2552
2596
  /* appendWindowEnd= */ Infinity,
2553
2597
  partialSegmentRefs,
@@ -2632,6 +2676,7 @@ shaka.hls.HlsParser = class {
2632
2676
  const references = [];
2633
2677
 
2634
2678
  let previousReference = null;
2679
+ let lastDiscontinuityStartTime = firstStartTime;
2635
2680
 
2636
2681
  for (let i = 0; i < hlsSegments.length; i++) {
2637
2682
  const item = hlsSegments[i];
@@ -2643,6 +2688,7 @@ shaka.hls.HlsParser = class {
2643
2688
  item.tags, 'EXT-X-DISCONTINUITY');
2644
2689
  if (discontinuityTag) {
2645
2690
  discontinuitySequence++;
2691
+ lastDiscontinuityStartTime = startTime;
2646
2692
  }
2647
2693
 
2648
2694
  // Apply new AES-128 tags as you see them, keeping a running total.
@@ -2681,6 +2727,7 @@ shaka.hls.HlsParser = class {
2681
2727
  variables,
2682
2728
  playlist.absoluteUri,
2683
2729
  type,
2730
+ lastDiscontinuityStartTime,
2684
2731
  hlsAes128Key);
2685
2732
  previousReference = reference;
2686
2733
 
@@ -2795,6 +2842,9 @@ shaka.hls.HlsParser = class {
2795
2842
  * @private
2796
2843
  */
2797
2844
  variableSubstitution_(uri, variables) {
2845
+ if (!variables.size) {
2846
+ return uri;
2847
+ }
2798
2848
  let newUri = String(uri).replace(/%7B/g, '{').replace(/%7D/g, '}');
2799
2849
 
2800
2850
  const uriVariables = newUri.match(/{\$\w*}/g);
@@ -3240,6 +3290,7 @@ shaka.hls.HlsParser = class {
3240
3290
  * type: string,
3241
3291
  * verbatimMediaPlaylistUri: string,
3242
3292
  * absoluteMediaPlaylistUri: string,
3293
+ * minTimestamp: number,
3243
3294
  * maxTimestamp: number,
3244
3295
  * mediaSequenceToStartTime: !Map.<number, number>,
3245
3296
  * canSkipSegments: boolean,
@@ -3263,6 +3314,8 @@ shaka.hls.HlsParser = class {
3263
3314
  * @property {string} absoluteMediaPlaylistUri
3264
3315
  * The absolute media playlist URI, resolved relative to the master playlist
3265
3316
  * and updated to reflect any redirects.
3317
+ * @property {number} minTimestamp
3318
+ * The minimum timestamp found in the stream.
3266
3319
  * @property {number} maxTimestamp
3267
3320
  * The maximum timestamp found in the stream.
3268
3321
  * @property {!Map.<number, number>} mediaSequenceToStartTime
@@ -158,7 +158,15 @@ shaka.hls.ManifestTextParser = class {
158
158
  partialSegmentTags.push(tag);
159
159
  } else if (tag.name == 'EXT-X-PRELOAD-HINT') {
160
160
  if (tag.getAttributeValue('TYPE') == 'PART') {
161
- partialSegmentTags.push(tag);
161
+ // Note: BYTERANGE-START without BYTERANGE-LENGTH is being
162
+ // ignored.
163
+ if (tag.getAttributeValue('BYTERANGE-START') != null) {
164
+ if (tag.getAttributeValue('BYTERANGE-LENGTH') != null) {
165
+ partialSegmentTags.push(tag);
166
+ }
167
+ } else {
168
+ partialSegmentTags.push(tag);
169
+ }
162
170
  } else if (tag.getAttributeValue('TYPE') == 'MAP') {
163
171
  // Rename the Preload Hint tag to be a Map tag.
164
172
  tag.setName('EXT-X-MAP');
@@ -300,6 +308,7 @@ shaka.hls.ManifestTextParser.MEDIA_PLAYLIST_TAGS = [
300
308
  'EXT-X-ENDLIST',
301
309
  'EXT-X-SERVER-CONTROL',
302
310
  'EXT-X-SKIP',
311
+ 'EXT-X-PART-INF',
303
312
  ];
304
313
 
305
314
 
@@ -133,6 +133,9 @@ shaka.media.DrmEngine = class {
133
133
 
134
134
  /** @private {Promise} */
135
135
  this.mediaKeysAttached_ = null;
136
+
137
+ /** @private {?shaka.extern.InitDataOverride} */
138
+ this.manifestInitData_ = null;
136
139
  }
137
140
 
138
141
  /** @override */
@@ -480,10 +483,10 @@ shaka.media.DrmEngine = class {
480
483
  () => this.closeOpenSessions_());
481
484
  }
482
485
 
483
- const manifestInitData = this.currentDrmInfo_ ?
484
- this.currentDrmInfo_.initData.find(
486
+ this.manifestInitData_ = this.currentDrmInfo_ ?
487
+ (this.currentDrmInfo_.initData.find(
485
488
  (initDataOverride) => initDataOverride.initData.length > 0,
486
- ) : null;
489
+ ) || null) : null;
487
490
 
488
491
  /**
489
492
  * We can attach media keys before the playback actually begins when:
@@ -491,7 +494,7 @@ shaka.media.DrmEngine = class {
491
494
  * - Some initData already has been generated (through the manifest)
492
495
  * - In case of an offline session
493
496
  */
494
- if (manifestInitData ||
497
+ if (this.manifestInitData_ ||
495
498
  this.currentDrmInfo_.keySystem !== 'com.apple.fps' ||
496
499
  this.offlineSessionIds_.length) {
497
500
  await this.attachMediaKeys_();
@@ -504,7 +507,7 @@ shaka.media.DrmEngine = class {
504
507
  // Also suppress 'encrypted' events when parsing in-band ppsh
505
508
  // from media segments because that serves the same purpose as the
506
509
  // 'encrypted' events.
507
- if (!manifestInitData && !this.offlineSessionIds_.length &&
510
+ if (!this.manifestInitData_ && !this.offlineSessionIds_.length &&
508
511
  !this.config_.parseInbandPsshEnabled) {
509
512
  this.eventManager_.listen(
510
513
  this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e));
@@ -2361,7 +2364,7 @@ shaka.media.DrmEngine = class {
2361
2364
  * @return {!Promise<void>}
2362
2365
  */
2363
2366
  parseInbandPssh(contentType, mediaSegment) {
2364
- if (!this.config_.parseInbandPsshEnabled) {
2367
+ if (!this.config_.parseInbandPsshEnabled || this.manifestInitData_) {
2365
2368
  return Promise.resolve();
2366
2369
  }
2367
2370
 
@@ -109,15 +109,15 @@ shaka.media.MediaSourceEngine = class {
109
109
  /** @private {!shaka.util.PublicPromise} */
110
110
  this.mediaSourceOpen_ = new shaka.util.PublicPromise();
111
111
 
112
+ /** @private {string} */
113
+ this.url_ = '';
114
+
112
115
  /** @private {MediaSource} */
113
116
  this.mediaSource_ = this.createMediaSource(this.mediaSourceOpen_);
114
117
 
115
118
  /** @type {!shaka.util.Destroyer} */
116
119
  this.destroyer_ = new shaka.util.Destroyer(() => this.doDestroy_());
117
120
 
118
- /** @private {string} */
119
- this.url_ = '';
120
-
121
121
  /** @private {boolean} */
122
122
  this.sequenceMode_ = false;
123
123
 
@@ -154,6 +154,8 @@ shaka.media.MediaSourceEngine = class {
154
154
  * @private
155
155
  */
156
156
  onSourceOpen_(p) {
157
+ goog.asserts.assert(this.url_, 'Must have object URL');
158
+
157
159
  // Release the object URL that was previously created, to prevent memory
158
160
  // leak.
159
161
  // createObjectURL creates a strong reference to the MediaSource object
@@ -202,6 +204,9 @@ shaka.media.MediaSourceEngine = class {
202
204
  'audio/mp4; codecs="ec-3"',
203
205
  'audio/mp4; codecs="opus"',
204
206
  'audio/mp4; codecs="flac"',
207
+ 'audio/mp4; codecs="dtsc"', // DTS Digital Surround
208
+ 'audio/mp4; codecs="dtse"', // DTS Express
209
+ 'audio/mp4; codecs="dtsx"', // DTS:X
205
210
  // WebM types
206
211
  'video/webm; codecs="vp8"',
207
212
  'video/webm; codecs="vp9"',
@@ -375,7 +380,22 @@ shaka.media.MediaSourceEngine = class {
375
380
  shaka.media.Transmuxer.convertCodecs(contentType, mimeType);
376
381
  }
377
382
  const type = mimeType + this.config_.sourceBufferExtraFeatures;
378
- const sourceBuffer = this.mediaSource_.addSourceBuffer(type);
383
+
384
+ this.destroyer_.ensureNotDestroyed();
385
+
386
+ let sourceBuffer;
387
+
388
+ try {
389
+ sourceBuffer = this.mediaSource_.addSourceBuffer(type);
390
+ } catch (exception) {
391
+ throw new shaka.util.Error(
392
+ shaka.util.Error.Severity.CRITICAL,
393
+ shaka.util.Error.Category.MEDIA,
394
+ shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW,
395
+ exception,
396
+ 'The mediaSource_ status was' + this.mediaSource_.readyState +
397
+ 'expected \'open\'');
398
+ }
379
399
 
380
400
  this.eventManager_.listen(
381
401
  sourceBuffer, 'error',
@@ -628,7 +648,8 @@ shaka.media.MediaSourceEngine = class {
628
648
  // For HLS CEA-608/708 CLOSED-CAPTIONS, text data is embedded in
629
649
  // the video stream, so textEngine may not have been initialized.
630
650
  if (!this.textEngine_) {
631
- this.reinitText('text/vtt', this.sequenceMode_);
651
+ this.reinitText(shaka.util.MimeUtils.CEA608_CLOSED_CAPTION_MIMETYPE,
652
+ this.sequenceMode_);
632
653
  }
633
654
  // This doesn't work for native TS support (ex. Edge/Chromecast),
634
655
  // since no transmuxing is needed for native TS.
@@ -647,7 +668,8 @@ shaka.media.MediaSourceEngine = class {
647
668
  data = transmuxedData.data;
648
669
  } else if (hasClosedCaptions && contentType == ContentType.VIDEO) {
649
670
  if (!this.textEngine_) {
650
- this.reinitText('text/vtt', this.sequenceMode_);
671
+ this.reinitText(shaka.util.MimeUtils.CEA608_CLOSED_CAPTION_MIMETYPE,
672
+ this.sequenceMode_);
651
673
  }
652
674
  if (!this.captionParser_) {
653
675
  this.captionParser_ = this.getCaptionParser(mimeType);
@@ -771,7 +793,7 @@ shaka.media.MediaSourceEngine = class {
771
793
  this.append_(contentType, data);
772
794
  });
773
795
 
774
- if (goog.DEBUG && reference) {
796
+ if (goog.DEBUG && reference && !reference.isPreload()) {
775
797
  const bufferedAfter = this.getBuffered_(contentType);
776
798
  const newBuffered = shaka.media.TimeRangesUtils.computeAddedRange(
777
799
  bufferedBefore, bufferedAfter);
@@ -937,7 +959,6 @@ shaka.media.MediaSourceEngine = class {
937
959
 
938
960
  /**
939
961
  * Adjust timestamp offset to maintain AV sync across discontinuities.
940
- * Only used in sequence mode.
941
962
  *
942
963
  * @param {shaka.util.ManifestParserUtils.ContentType} contentType
943
964
  * @param {number} timestampOffset
@@ -946,9 +967,6 @@ shaka.media.MediaSourceEngine = class {
946
967
  async resync(contentType, timestampOffset) {
947
968
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
948
969
 
949
- goog.asserts.assert(this.sequenceMode_,
950
- 'resync only used with sequence mode!');
951
-
952
970
  if (contentType == ContentType.TEXT) {
953
971
  // This operation is for audio and video only.
954
972
  return;
@@ -978,8 +996,10 @@ shaka.media.MediaSourceEngine = class {
978
996
  async endOfStream(reason) {
979
997
  await this.enqueueBlockingOperation_(() => {
980
998
  // If endOfStream() has already been called on the media source,
981
- // don't call it again.
982
- if (this.ended()) {
999
+ // don't call it again. Also do not call if readyState is
1000
+ // 'closed' (not attached to video element) since it is not a
1001
+ // valid operation.
1002
+ if (this.ended() || this.mediaSource_.readyState === 'closed') {
983
1003
  return;
984
1004
  }
985
1005
  // Tizen won't let us pass undefined, but it will let us omit the
@@ -236,7 +236,16 @@ shaka.media.PresentationTimeline = class {
236
236
 
237
237
  let firstReferenceStartTime = references[0].startTime;
238
238
  let lastReferenceEndTime = references[0].endTime;
239
+
240
+ // Date.now() is in milliseconds, from which we compute "now" in seconds.
241
+ const now = (Date.now() + this.clockOffset_) / 1000.0;
242
+
239
243
  for (const reference of references) {
244
+ // Exclude segments that are in the "future".
245
+ if (now < reference.startTime) {
246
+ continue;
247
+ }
248
+
240
249
  firstReferenceStartTime = Math.min(
241
250
  firstReferenceStartTime, reference.startTime);
242
251
  lastReferenceEndTime = Math.max(lastReferenceEndTime, reference.endTime);
@@ -251,8 +260,6 @@ shaka.media.PresentationTimeline = class {
251
260
  !this.startTimeLocked_) {
252
261
  // Since we have explicit segment end times, calculate a presentation
253
262
  // start based on them. This start time accounts for drift.
254
- // Date.now() is in milliseconds, from which we compute "now" in seconds.
255
- const now = (Date.now() + this.clockOffset_) / 1000.0;
256
263
  this.presentationStartTime_ =
257
264
  now - this.maxSegmentEndTime_ - this.maxSegmentDuration_;
258
265
  }
@@ -363,6 +363,8 @@ shaka.media.SegmentIndex = class {
363
363
  lastReference.status,
364
364
  lastReference.hlsAes128Key,
365
365
  );
366
+ this.references[this.references.length - 1].discontinuitySequence =
367
+ lastReference.discontinuitySequence;
366
368
  }
367
369
 
368
370
 
@@ -144,8 +144,9 @@ shaka.media.SegmentReference = class {
144
144
  * minus the first segment's tfdt box's 'baseMediaDecodeTime' field (after
145
145
  * it has been converted to seconds).
146
146
  * <br>
147
- * For HLS, this value should be 0 to keep the presentation time at the most
148
- * recent discontinuity minus the corresponding media time.
147
+ * For HLS, this value should be the start time of the most recent
148
+ * discontinuity, or 0 if there is no preceding discontinuity. Only used
149
+ * in segments mode.
149
150
  * @param {number} appendWindowStart
150
151
  * The start of the append window for this reference, relative to the
151
152
  * presentation. Any content from before this time will be removed by
@@ -242,6 +243,9 @@ shaka.media.SegmentReference = class {
242
243
  /** @type {shaka.media.SegmentReference.Status} */
243
244
  this.status = status;
244
245
 
246
+ /** @type {boolean} */
247
+ this.preload = false;
248
+
245
249
  /** @type {?shaka.extern.HlsAes128Key} */
246
250
  this.hlsAes128Key = hlsAes128Key;
247
251
 
@@ -365,6 +369,25 @@ shaka.media.SegmentReference = class {
365
369
  this.status = shaka.media.SegmentReference.Status.UNAVAILABLE;
366
370
  }
367
371
 
372
+ /**
373
+ * Mark the reference as preload.
374
+ *
375
+ * @export
376
+ */
377
+ markAsPreload() {
378
+ this.preload = true;
379
+ }
380
+
381
+ /**
382
+ * Returns true if the segment is preloaded.
383
+ *
384
+ * @return {boolean}
385
+ * @export
386
+ */
387
+ isPreload() {
388
+ return this.preload;
389
+ }
390
+
368
391
  /**
369
392
  * Set the segment's thumbnail sprite.
370
393
  *
@@ -78,6 +78,9 @@ shaka.media.StreamingEngine = class {
78
78
  /** @private {?shaka.extern.Stream} */
79
79
  this.currentTextStream_ = null;
80
80
 
81
+ /** @private {number} */
82
+ this.textStreamSequenceId_ = 0;
83
+
81
84
  /** @private {boolean} */
82
85
  this.parsedPrftEventRaised_ = false;
83
86
 
@@ -224,6 +227,8 @@ shaka.media.StreamingEngine = class {
224
227
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
225
228
  goog.asserts.assert(!this.mediaStates_.has(ContentType.TEXT),
226
229
  'Should not call loadNewTextStream_ while streaming text!');
230
+ this.textStreamSequenceId_++;
231
+ const currentSequenceId = this.textStreamSequenceId_;
227
232
 
228
233
  try {
229
234
  // Clear MediaSource's buffered text, so that the new text stream will
@@ -246,7 +251,7 @@ shaka.media.StreamingEngine = class {
246
251
  const streamText =
247
252
  textDisplayer.isTextVisible() || this.config_.alwaysStreamText;
248
253
 
249
- if (streamText) {
254
+ if (streamText && (this.textStreamSequenceId_ == currentSequenceId)) {
250
255
  const state = this.createMediaState_(stream);
251
256
  this.mediaStates_.set(ContentType.TEXT, state);
252
257
  this.scheduleUpdate_(state, 0);
@@ -357,7 +362,7 @@ shaka.media.StreamingEngine = class {
357
362
  /**
358
363
  * @param {shaka.extern.Stream} textStream
359
364
  */
360
- switchTextStream(textStream) {
365
+ async switchTextStream(textStream) {
361
366
  this.currentTextStream_ = textStream;
362
367
 
363
368
  if (!this.startupComplete_) {
@@ -369,6 +374,13 @@ shaka.media.StreamingEngine = class {
369
374
  goog.asserts.assert(textStream && textStream.type == ContentType.TEXT,
370
375
  'Wrong stream type passed to switchTextStream!');
371
376
 
377
+ // In HLS it is possible that the mimetype changes when the media
378
+ // playlist is downloaded, so it is necessary to have the updated data
379
+ // here.
380
+ if (!textStream.segmentIndex) {
381
+ await textStream.createSegmentIndex();
382
+ }
383
+
372
384
  this.switchInternal_(
373
385
  textStream, /* clearBuffer= */ true,
374
386
  /* safeMargin= */ 0, /* force= */ false);
@@ -639,10 +651,15 @@ shaka.media.StreamingEngine = class {
639
651
  const mediaState = this.mediaStates_.get(type);
640
652
  const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
641
653
 
642
- // Always clear the iterator since we need to start streaming from the
643
- // new time. This also happens in clearBuffer_, but if we don't clear,
644
- // we still want to reset the iterator.
645
- mediaState.segmentIterator = null;
654
+ let segment = null;
655
+ if (mediaState.segmentIterator) {
656
+ segment = mediaState.segmentIterator.current();
657
+ }
658
+ // Only reset the iterator if we seek outside the current segment.
659
+ if (!segment || segment.startTime > presentationTime ||
660
+ segment.endTime < presentationTime) {
661
+ mediaState.segmentIterator = null;
662
+ }
646
663
 
647
664
  if (!newTimeIsBuffered(type)) {
648
665
  const bufferEnd =
@@ -1564,10 +1581,10 @@ shaka.media.StreamingEngine = class {
1564
1581
  'waiting for another stream to recover...');
1565
1582
  }
1566
1583
 
1567
- // QuotaExceededError gets thrown if evication didn't help to make room
1584
+ // QuotaExceededError gets thrown if eviction didn't help to make room
1568
1585
  // for a segment. We want to wait for a while (4 seconds is just an
1569
1586
  // arbitrary number) before updating to give the playhead a chance to
1570
- // advance, so we don't immidiately throw again.
1587
+ // advance, so we don't immediately throw again.
1571
1588
  this.scheduleUpdate_(mediaState, 4);
1572
1589
  }
1573
1590
 
@@ -1675,19 +1692,31 @@ shaka.media.StreamingEngine = class {
1675
1692
  }
1676
1693
  }
1677
1694
 
1695
+ const lastDiscontinuitySequence =
1696
+ mediaState.lastSegmentReference ?
1697
+ mediaState.lastSegmentReference.discontinuitySequence : null;
1678
1698
  if (this.manifest_.sequenceMode) {
1679
1699
  // Across discontinuity bounds, we should resync timestamps for
1680
1700
  // sequence mode playbacks. The next segment appended should
1681
1701
  // land at its theoretical timestamp from the segment index.
1682
- const lastDiscontinuitySequence =
1683
- mediaState.lastSegmentReference ?
1684
- mediaState.lastSegmentReference.discontinuitySequence : null;
1685
1702
  if (reference.discontinuitySequence != lastDiscontinuitySequence ||
1686
1703
  mediaState.needsResync) {
1687
1704
  mediaState.needsResync = false;
1688
1705
  operations.push(this.playerInterface_.mediaSourceEngine.resync(
1689
1706
  mediaState.type, reference.startTime));
1690
1707
  }
1708
+ } else {
1709
+ // In segments mode, we need to resync to set the timestampOffset
1710
+ // to the start of the current discontinuity sequence. This is
1711
+ // because individual discontinuity sequences may have internal
1712
+ // timestamps that overlap, so we adjust the timestampOffset to avoid
1713
+ // having the SourceBuffer get overwritten.
1714
+ if (reference.discontinuitySequence != lastDiscontinuitySequence) {
1715
+ operations.push(
1716
+ this.playerInterface_.mediaSourceEngine.resync(
1717
+ mediaState.type,
1718
+ reference.timestampOffset));
1719
+ }
1691
1720
  }
1692
1721
 
1693
1722
  await Promise.all(operations);