shaka-player 4.7.13 → 4.7.15

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/CONTRIBUTORS +2 -0
  3. package/dist/cast-boot.js +37 -0
  4. package/dist/deps.js +5 -4
  5. package/dist/locales.js +31 -31
  6. package/dist/shaka-player.compiled.d.ts +37 -14
  7. package/dist/shaka-player.compiled.debug.d.ts +37 -14
  8. package/dist/shaka-player.compiled.debug.externs.js +14 -0
  9. package/dist/shaka-player.compiled.debug.js +360 -350
  10. package/dist/shaka-player.compiled.debug.map +4 -4
  11. package/dist/shaka-player.compiled.externs.js +14 -0
  12. package/dist/shaka-player.compiled.js +1107 -1105
  13. package/dist/shaka-player.compiled.map +4 -4
  14. package/dist/shaka-player.ui.d.ts +37 -14
  15. package/dist/shaka-player.ui.debug.d.ts +37 -14
  16. package/dist/shaka-player.ui.debug.externs.js +14 -0
  17. package/dist/shaka-player.ui.debug.js +280 -269
  18. package/dist/shaka-player.ui.debug.map +4 -4
  19. package/dist/shaka-player.ui.externs.js +14 -0
  20. package/dist/shaka-player.ui.js +1267 -1265
  21. package/dist/shaka-player.ui.map +4 -4
  22. package/externs/cast-namespace.js +14 -0
  23. package/externs/{chromecast.js → cast-sdk-v2.js} +0 -4
  24. package/externs/cast-sdk-v3.js +35 -0
  25. package/externs/ima.js +2 -0
  26. package/externs/mediatailor.js +0 -0
  27. package/externs/{cast.js → remote-playback.js} +2 -2
  28. package/externs/shaka/player.js +5 -2
  29. package/lib/ads/client_side_ad_manager.js +9 -5
  30. package/lib/cea/cea608_data_channel.js +9 -0
  31. package/lib/cea/cea_decoder.js +9 -2
  32. package/lib/dash/segment_template.js +30 -2
  33. package/lib/hls/hls_parser.js +19 -10
  34. package/lib/media/closed_caption_parser.js +16 -4
  35. package/lib/media/drm_engine.js +17 -3
  36. package/lib/media/gap_jumping_controller.js +9 -1
  37. package/lib/media/media_source_engine.js +20 -13
  38. package/lib/media/play_rate_controller.js +36 -8
  39. package/lib/media/playhead.js +3 -3
  40. package/lib/media/segment_index.js +10 -0
  41. package/lib/media/segment_reference.js +12 -0
  42. package/lib/media/stall_detector.js +3 -0
  43. package/lib/media/streaming_engine.js +8 -14
  44. package/lib/net/http_fetch_plugin.js +3 -1
  45. package/lib/net/http_xhr_plugin.js +6 -1
  46. package/lib/player.js +111 -53
  47. package/lib/polyfill/media_capabilities.js +7 -5
  48. package/lib/polyfill/patchedmediakeys_cert.js +62 -0
  49. package/lib/text/ui_text_displayer.js +10 -1
  50. package/lib/transmuxer/ts_transmuxer.js +6 -24
  51. package/lib/util/object_utils.js +1 -2
  52. package/lib/util/platform.js +100 -11
  53. package/lib/util/stream_utils.js +31 -14
  54. package/package.json +7 -2
  55. package/support.html +2 -0
  56. package/ui/audio_language_selection.js +4 -0
  57. package/ui/range_element.js +8 -2
  58. package/ui/resolution_selection.js +5 -0
  59. package/ui/text_selection.js +4 -0
@@ -0,0 +1,14 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2024 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * @fileoverview Google Cast namespace definitions, shared by other externs
9
+ * files.
10
+ * @externs
11
+ */
12
+
13
+ /** @const */
14
+ var cast = {};
@@ -15,10 +15,6 @@
15
15
  var __onGCastApiAvailable;
16
16
 
17
17
 
18
- /** @const */
19
- var cast = {};
20
-
21
-
22
18
  /** @const */
23
19
  cast.receiver = {};
24
20
 
@@ -0,0 +1,35 @@
1
+ /*! @license
2
+ * Shaka Player
3
+ * Copyright 2024 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * @fileoverview Externs for the limited subset of the Cast Application
9
+ * Framework (Receiver SDK v3) that we use in our test infrastructure.
10
+ *
11
+ * @externs
12
+ */
13
+
14
+ /** @const */
15
+ cast.framework = {};
16
+
17
+ /**
18
+ * @typedef {{
19
+ * statusText: string,
20
+ * disableIdleTimeout: boolean,
21
+ * skipPlayersLoad: boolean
22
+ * }}
23
+ */
24
+ cast.framework.CastReceiverOptions;
25
+
26
+ cast.framework.CastReceiverContext = class {
27
+ /** @return {!cast.framework.CastReceiverContext} */
28
+ static getInstance() {}
29
+
30
+ /**
31
+ * @param {!cast.framework.CastReceiverOptions} options
32
+ * @return {!cast.framework.CastReceiverContext}
33
+ */
34
+ start(options) {}
35
+ };
package/externs/ima.js CHANGED
@@ -204,6 +204,8 @@ google.ima.AdDisplayContainer = class {
204
204
  constructor(adContainer, video) {}
205
205
 
206
206
  initialize() {}
207
+
208
+ destroy() {}
207
209
  };
208
210
 
209
211
 
File without changes
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  /**
8
- * @fileoverview Externs for HTMLMediaElement related to casting which were
9
- * missing in the Closure compiler.
8
+ * @fileoverview Externs for HTMLMediaElement related to remote playback which
9
+ * were missing in the Closure compiler.
10
10
  *
11
11
  * @externs
12
12
  */
@@ -1376,11 +1376,14 @@ shaka.extern.AdsConfiguration;
1376
1376
  * @property {boolean} enabled
1377
1377
  * If true, enable adaptation by the current AbrManager. Defaults to true.
1378
1378
  * @property {boolean} useNetworkInformation
1379
- * If true, use Network Information API in the current AbrManager.
1379
+ * If true, use the Network Information API in the current AbrManager, if it
1380
+ * is available in the browser environment. If the Network Information API is
1381
+ * used, Shaka Player will ignore the defaultBandwidthEstimate config.
1380
1382
  * Defaults to true.
1381
1383
  * @property {number} defaultBandwidthEstimate
1382
1384
  * The default bandwidth estimate to use if there is not enough data, in
1383
- * bit/sec.
1385
+ * bit/sec. Only used if useNetworkInformation is false, or if the Network
1386
+ * Information API is not available.
1384
1387
  * @property {shaka.extern.Restrictions} restrictions
1385
1388
  * The restrictions to apply to ABR decisions. These are "soft" restrictions.
1386
1389
  * Any track that fails to meet these restrictions will not be selected
@@ -63,16 +63,17 @@ shaka.ads.ClientSideAdManager = class {
63
63
 
64
64
  google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true);
65
65
 
66
- const adDisplayContainer = new google.ima.AdDisplayContainer(
66
+ /** @private {!google.ima.AdDisplayContainer} */
67
+ this.adDisplayContainer_ = new google.ima.AdDisplayContainer(
67
68
  this.adContainer_,
68
69
  this.video_);
69
70
 
70
71
  // TODO: IMA: Must be done as the result of a user action on mobile
71
- adDisplayContainer.initialize();
72
+ this.adDisplayContainer_.initialize();
72
73
 
73
74
  // IMA: This instance should be re-used for the entire lifecycle of
74
75
  // the page.
75
- this.adsLoader_ = new google.ima.AdsLoader(adDisplayContainer);
76
+ this.adsLoader_ = new google.ima.AdsLoader(this.adDisplayContainer_);
76
77
 
77
78
  this.adsLoader_.getSettings().setPlayerType('shaka-player');
78
79
  this.adsLoader_.getSettings().setPlayerVersion(shaka.Player.version);
@@ -176,6 +177,7 @@ shaka.ads.ClientSideAdManager = class {
176
177
  this.imaAdsManager_.destroy();
177
178
  }
178
179
  this.adsLoader_.destroy();
180
+ this.adDisplayContainer_.destroy();
179
181
  }
180
182
 
181
183
  /**
@@ -533,7 +535,9 @@ shaka.ads.ClientSideAdManager = class {
533
535
  shaka.ads.AdManager.AD_STARTED, data));
534
536
  if (this.ad_.isLinear()) {
535
537
  this.adContainer_.setAttribute('ad-active', 'true');
536
- this.video_.pause();
538
+ if (!this.config_.customPlayheadTracker) {
539
+ this.video_.pause();
540
+ }
537
541
  if (this.video_.muted) {
538
542
  this.ad_.setInitialMuted(this.video_.volume);
539
543
  } else {
@@ -551,7 +555,7 @@ shaka.ads.ClientSideAdManager = class {
551
555
  (new Map()).set('originalEvent', e)));
552
556
  if (this.ad_ && this.ad_.isLinear()) {
553
557
  this.adContainer_.removeAttribute('ad-active');
554
- if (!this.video_.ended) {
558
+ if (!this.config_.customPlayheadTracker && !this.video_.ended) {
555
559
  this.video_.play();
556
560
  }
557
561
  }
@@ -642,6 +642,15 @@ shaka.cea.Cea608DataChannel = class {
642
642
  // b1 -> |P|0|0|1|X|X|X|X|
643
643
  return (b1 & 0x70) === 0x10;
644
644
  }
645
+
646
+ /**
647
+ * Checks if the data contains a XDS control code.
648
+ * @param {number} b1 Byte 1.
649
+ * @return {boolean}
650
+ */
651
+ static isXdsControlCode(b1) {
652
+ return b1 >= 0x01 && b1 <= 0x0F;
653
+ }
645
654
  };
646
655
 
647
656
  /**
@@ -97,6 +97,7 @@ shaka.cea.CeaDecoder = class {
97
97
  * @override
98
98
  */
99
99
  clear() {
100
+ shaka.log.debug('Clearing CEA decoder');
100
101
  this.badFrames_ = 0;
101
102
  this.cea608DataArray_ = [];
102
103
  this.cea708DataArray_ = [];
@@ -113,6 +114,7 @@ shaka.cea.CeaDecoder = class {
113
114
  * Resets the decoder.
114
115
  */
115
116
  reset() {
117
+ shaka.log.debug('Resetting CEA decoder');
116
118
  this.currentField1Channel_ = 0;
117
119
  this.currentField2Channel_ = 0;
118
120
  for (const stream of this.cea608ModeToStream_.values()) {
@@ -128,6 +130,7 @@ shaka.cea.CeaDecoder = class {
128
130
  */
129
131
  extract(userDataSeiMessage, pts) {
130
132
  if (this.waitingForFirstPacket_) {
133
+ shaka.log.debug('Setting first pts value to', pts);
131
134
  for (const stream of this.cea608ModeToStream_.values()) {
132
135
  stream.firstPts(pts);
133
136
  }
@@ -304,16 +307,20 @@ shaka.cea.CeaDecoder = class {
304
307
  ccPacket.ccData1 &= 0x7f;
305
308
  ccPacket.ccData2 &= 0x7f;
306
309
 
310
+ // Ignore XDS
311
+ if (shaka.cea.Cea608DataChannel.isXdsControlCode(ccPacket.ccData1)) {
312
+ return null;
313
+ }
314
+
307
315
  // Check for empty captions and skip them.
308
316
  if (!ccPacket.ccData1 && !ccPacket.ccData2) {
309
317
  return null;
310
318
  }
311
319
 
312
- this.streams_.add(selectedMode);
313
-
314
320
  // Process the clean CC data pair.
315
321
  let parsedClosedCaption = null;
316
322
  if (shaka.cea.Cea608DataChannel.isControlCode(ccPacket.ccData1)) {
323
+ this.streams_.add(selectedMode);
317
324
  parsedClosedCaption = selectedStream.handleControlCode(ccPacket);
318
325
  } else {
319
326
  // Handle as a Basic North American Character.
@@ -394,6 +394,11 @@ shaka.dash.SegmentTemplate = class {
394
394
  Math.ceil(availablePeriodTimes[1] / segmentDuration) - 1,
395
395
  ];
396
396
 
397
+ // For Low Latency we can request the partial current position.
398
+ if (context.representation.availabilityTimeOffset) {
399
+ availablePeriodPositions[1]++;
400
+ }
401
+
397
402
  // In absolute positions.
398
403
  const availablePresentationPositions =
399
404
  availablePeriodPositions.map((x) => x + startNumber);
@@ -497,7 +502,12 @@ shaka.dash.SegmentTemplate = class {
497
502
  // references were generated. Otherwise, the update would start creating
498
503
  // negative positions for segments in periods which begin in the future.
499
504
  let nextPosition = Math.max(minPosition, maxPosition + 1);
500
- segmentIndex.updateEvery(segmentDuration, () => {
505
+ let updateTime = segmentDuration;
506
+ // For low latency we need to evict very frequently.
507
+ if (context.representation.availabilityTimeOffset) {
508
+ updateTime = 0.1;
509
+ }
510
+ segmentIndex.updateEvery(updateTime, () => {
501
511
  // Evict any references outside the window.
502
512
  const availabilityStartTime =
503
513
  presentationTimeline.getSegmentAvailabilityStart();
@@ -696,7 +706,7 @@ shaka.dash.TimelineSegmentIndex = class extends shaka.media.SegmentIndex {
696
706
  */
697
707
  appendTemplateInfo(info, periodStart, periodEnd, shouldFit,
698
708
  initSegmentReference) {
699
- this.initSegmentReference_ = initSegmentReference;
709
+ this.updateInitSegmentReference(initSegmentReference);
700
710
  if (!this.templateInfo_) {
701
711
  this.templateInfo_ = info;
702
712
  this.periodStart_ = periodStart;
@@ -727,6 +737,24 @@ shaka.dash.TimelineSegmentIndex = class extends shaka.media.SegmentIndex {
727
737
  }
728
738
  }
729
739
 
740
+ /**
741
+ * Updates the init segment reference and propagates the update to all
742
+ * references.
743
+ * @param {shaka.media.InitSegmentReference} initSegmentReference
744
+ */
745
+ updateInitSegmentReference(initSegmentReference) {
746
+ if (this.initSegmentReference_ === initSegmentReference) {
747
+ return;
748
+ }
749
+
750
+ this.initSegmentReference_ = initSegmentReference;
751
+ for (const reference of this.references) {
752
+ if (reference) {
753
+ reference.updateInitSegmentReference(initSegmentReference);
754
+ }
755
+ }
756
+ }
757
+
730
758
  /**
731
759
  *
732
760
  * @param {number} time
@@ -891,13 +891,12 @@ shaka.hls.HlsParser = class {
891
891
  if (!playlist.segments.length) {
892
892
  return defaultBasicInfo;
893
893
  }
894
- const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
895
- const middleSegment = playlist.segments[middleSegmentIdx];
896
- const middleSegmentUris = shaka.hls.Utils.constructSegmentUris(
894
+ const firstSegment = playlist.segments[0];
895
+ const firstSegmentUris = shaka.hls.Utils.constructSegmentUris(
897
896
  getUris(),
898
- playlist.segments[0].verbatimSegmentUri);
899
- const middleSegmentUri = middleSegmentUris[0];
900
- const parsedUri = new goog.Uri(middleSegmentUri);
897
+ firstSegment.verbatimSegmentUri);
898
+ const firstSegmentUri = firstSegmentUris[0];
899
+ const parsedUri = new goog.Uri(firstSegmentUri);
901
900
  const extension = parsedUri.getPath().split('.').pop();
902
901
  const rawMimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
903
902
  if (rawMimeType) {
@@ -909,7 +908,7 @@ shaka.hls.HlsParser = class {
909
908
 
910
909
  let initData = null;
911
910
  const initSegmentRef = this.getInitSegmentReference_(
912
- playlist, middleSegment.tags, getUris);
911
+ playlist, firstSegment.tags, getUris);
913
912
  this.mapTagToInitSegmentRefMap_.clear();
914
913
  if (initSegmentRef) {
915
914
  const initSegmentRequest = shaka.util.Networking.createSegmentRequest(
@@ -924,8 +923,17 @@ shaka.hls.HlsParser = class {
924
923
  initData = initResponse.data;
925
924
  }
926
925
 
927
- const segmentRequest = shaka.net.NetworkingEngine.makeRequest(
928
- middleSegmentUris, this.config_.retryParameters);
926
+ let startByte = 0;
927
+ let endByte = null;
928
+ const byterangeTag = shaka.hls.Utils.getFirstTagWithName(
929
+ firstSegment.tags, 'EXT-X-BYTERANGE');
930
+ if (byterangeTag) {
931
+ [startByte, endByte] = this.parseByteRange_(
932
+ /* previousReference= */ null, byterangeTag.value);
933
+ }
934
+
935
+ const segmentRequest = shaka.util.Networking.createSegmentRequest(
936
+ firstSegmentUris, startByte, endByte, this.config_.retryParameters);
929
937
  const type = shaka.net.NetworkingEngine.AdvancedRequestType.MEDIA_SEGMENT;
930
938
  const response = await this.makeNetworkRequest_(
931
939
  segmentRequest, requestType, {type});
@@ -943,7 +951,8 @@ shaka.hls.HlsParser = class {
943
951
  return basicInfo;
944
952
  }
945
953
  } else if (extension == 'mp4' || extension == 'cmfv' ||
946
- extension == 'm4s' || contentMimeType == 'video/mp4' ||
954
+ extension == 'm4s' || extension == 'fmp4' ||
955
+ contentMimeType == 'video/mp4' ||
947
956
  contentMimeType == 'audio/mp4' ||
948
957
  contentMimeType == 'video/iso.segment') {
949
958
  const basicInfo = shaka.media.SegmentUtils.getBasicInfoFromMp4(
@@ -9,6 +9,7 @@ goog.provide('shaka.media.IClosedCaptionParser');
9
9
 
10
10
  goog.require('shaka.cea.DummyCaptionDecoder');
11
11
  goog.require('shaka.cea.DummyCeaParser');
12
+ goog.require('shaka.log');
12
13
  goog.require('shaka.util.BufferUtils');
13
14
 
14
15
 
@@ -22,10 +23,13 @@ goog.require('shaka.util.BufferUtils');
22
23
  */
23
24
  shaka.media.IClosedCaptionParser = class {
24
25
  /**
25
- * Initialize the caption parser. This should be called only once.
26
+ * Initialize the caption parser. This should be called whenever new init
27
+ * segment arrives.
26
28
  * @param {BufferSource} initSegment
29
+ * @param {boolean=} adaptation True if we just automatically switched active
30
+ * variant(s).
27
31
  */
28
- init(initSegment) {}
32
+ init(initSegment, adaptation = false) {}
29
33
 
30
34
  /**
31
35
  * Parses embedded CEA closed captions and interacts with the underlying
@@ -87,8 +91,16 @@ shaka.media.ClosedCaptionParser = class {
87
91
  /**
88
92
  * @override
89
93
  */
90
- init(initSegment) {
91
- this.ceaParser_.init(initSegment);
94
+ init(initSegment, adaptation = false) {
95
+ shaka.log.debug('Passing new init segment to CEA parser');
96
+ if (!adaptation) {
97
+ // Reset underlying decoder when new init segment arrives
98
+ // to clear stored pts values.
99
+ // This is necessary when a new Period comes in DASH or a discontinuity
100
+ // in HLS.
101
+ this.reset();
102
+ this.ceaParser_.init(initSegment);
103
+ }
92
104
  }
93
105
 
94
106
  /**
@@ -1551,6 +1551,9 @@ shaka.media.DrmEngine = class {
1551
1551
  const req = this.playerInterface_.netEngine.request(requestType, request);
1552
1552
  response = await req.promise;
1553
1553
  } catch (error) {
1554
+ if (this.destroyer_.destroyed()) {
1555
+ return;
1556
+ }
1554
1557
  // Request failed!
1555
1558
  goog.asserts.assert(error instanceof shaka.util.Error,
1556
1559
  'Wrong NetworkingEngine error type!');
@@ -1559,9 +1562,20 @@ shaka.media.DrmEngine = class {
1559
1562
  shaka.util.Error.Category.DRM,
1560
1563
  shaka.util.Error.Code.LICENSE_REQUEST_FAILED,
1561
1564
  error);
1562
- this.onError_(shakaErr);
1563
- if (metadata && metadata.updatePromise) {
1564
- metadata.updatePromise.reject(shakaErr);
1565
+ if (this.activeSessions_.size == 1) {
1566
+ this.onError_(shakaErr);
1567
+ if (metadata && metadata.updatePromise) {
1568
+ metadata.updatePromise.reject(shakaErr);
1569
+ }
1570
+ } else {
1571
+ if (metadata && metadata.updatePromise) {
1572
+ metadata.updatePromise.reject(shakaErr);
1573
+ }
1574
+ this.activeSessions_.delete(session);
1575
+ if (this.areAllSessionsLoaded_()) {
1576
+ this.allSessionsLoaded_.resolve();
1577
+ this.keyStatusTimer_.tickAfter(/* seconds= */ 0.1);
1578
+ }
1565
1579
  }
1566
1580
  return;
1567
1581
  }
@@ -13,6 +13,7 @@ goog.require('shaka.media.TimeRangesUtils');
13
13
  goog.require('shaka.util.EventManager');
14
14
  goog.require('shaka.util.FakeEvent');
15
15
  goog.require('shaka.util.IReleasable');
16
+ goog.require('shaka.util.Platform');
16
17
  goog.require('shaka.util.Timer');
17
18
 
18
19
 
@@ -217,7 +218,14 @@ shaka.media.GapJumpingController = class {
217
218
 
218
219
  // StreamingEngine can buffer past the seek end, but still don't allow
219
220
  // seeking past it.
220
- const jumpTo = buffered.start(gapIndex);
221
+ let jumpTo = buffered.start(gapIndex);
222
+ // Workaround for Xbox with Legacy Edge. On this platform video element
223
+ // often rounds value we want to set as currentTime and we are not able
224
+ // to jump over the gap.
225
+ if (shaka.util.Platform.isLegacyEdge() ||
226
+ shaka.util.Platform.isXboxOne()) {
227
+ jumpTo = Math.ceil((jumpTo + 0.01) * 100) / 100;
228
+ }
221
229
  const seekEnd = this.timeline_.getSeekRangeEnd();
222
230
  if (jumpTo >= seekEnd) {
223
231
  return;
@@ -280,7 +280,7 @@ shaka.media.MediaSourceEngine = class {
280
280
  if (contentType == ContentType.VIDEO) {
281
281
  codecs = StreamUtils.getCorrectVideoCodecs(codecs);
282
282
  } else if (contentType == ContentType.AUDIO) {
283
- codecs = StreamUtils.getCorrectAudioCodecs(codecs);
283
+ codecs = StreamUtils.getCorrectAudioCodecs(codecs, mimeType);
284
284
  }
285
285
  const extendedMimeType = MimeUtils.getExtendedType(
286
286
  stream, mimeType, codecs);
@@ -347,14 +347,11 @@ shaka.media.MediaSourceEngine = class {
347
347
 
348
348
  const support = {};
349
349
  for (const type of testMimeTypes) {
350
- if (shaka.util.Platform.supportsMediaSource()) {
351
- // Our TextEngine is only effective for MSE platforms at the moment.
352
- if (shaka.text.TextEngine.isTypeSupported(type)) {
353
- support[type] = true;
354
- } else {
355
- support[type] = shaka.media.Capabilities.isTypeSupported(type) ||
356
- shaka.transmuxer.TransmuxerEngine.isSupported(type);
357
- }
350
+ if (shaka.text.TextEngine.isTypeSupported(type)) {
351
+ support[type] = true;
352
+ } else if (shaka.util.Platform.supportsMediaSource()) {
353
+ support[type] = shaka.media.Capabilities.isTypeSupported(type) ||
354
+ shaka.transmuxer.TransmuxerEngine.isSupported(type);
358
355
  } else {
359
356
  support[type] = shaka.util.Platform.supportsMediaType(type);
360
357
  }
@@ -961,7 +958,7 @@ shaka.media.MediaSourceEngine = class {
961
958
  // If it is the init segment for closed captions, initialize the closed
962
959
  // caption parser.
963
960
  if (!reference) {
964
- this.captionParser_.init(data);
961
+ this.captionParser_.init(data, adaptation);
965
962
  } else {
966
963
  const closedCaptions = this.captionParser_.parseFrom(data);
967
964
  if (closedCaptions.length) {
@@ -1839,12 +1836,17 @@ shaka.media.MediaSourceEngine = class {
1839
1836
 
1840
1837
  const totalOfBuffers = streamsByType.size;
1841
1838
  let numberOfSourceBufferAdded = 0;
1842
- this.eventManager_.listen(sourceBuffers, 'addsourcebuffer', (event) => {
1839
+ const onSourceBufferAdded = () => {
1843
1840
  numberOfSourceBufferAdded++;
1844
1841
  if (numberOfSourceBufferAdded === totalOfBuffers) {
1845
1842
  sourceBufferAdded.resolve();
1843
+ this.eventManager_.unlisten(sourceBuffers, 'addsourcebuffer',
1844
+ onSourceBufferAdded);
1846
1845
  }
1847
- });
1846
+ };
1847
+
1848
+ this.eventManager_.listen(sourceBuffers, 'addsourcebuffer',
1849
+ onSourceBufferAdded);
1848
1850
 
1849
1851
  for (const contentType of streamsByType.keys()) {
1850
1852
  const stream = streamsByType.get(contentType);
@@ -1868,7 +1870,12 @@ shaka.media.MediaSourceEngine = class {
1868
1870
  this.destroyer_.ensureNotDestroyed();
1869
1871
 
1870
1872
  this.eventManager_.listenOnce(this.video_, 'canplaythrough', () => {
1871
- this.destroyer_.ensureNotDestroyed();
1873
+ // Don't use ensureNotDestroyed() from this event listener, because
1874
+ // that results in an uncaught exception. Instead, just check the
1875
+ // flag.
1876
+ if (this.destroyer_.destroyed()) {
1877
+ return;
1878
+ }
1872
1879
 
1873
1880
  this.video_.autoplay = previousAutoPlayState;
1874
1881
  if (!previousPausedState) {
@@ -6,6 +6,7 @@
6
6
 
7
7
  goog.provide('shaka.media.PlayRateController');
8
8
 
9
+ goog.require('goog.asserts');
9
10
  goog.require('shaka.log');
10
11
  goog.require('shaka.util.IReleasable');
11
12
  goog.require('shaka.util.Timer');
@@ -27,6 +28,9 @@ shaka.media.PlayRateController = class {
27
28
  /** @private {?shaka.media.PlayRateController.Harness} */
28
29
  this.harness_ = harness;
29
30
 
31
+ /** @private {boolean} */
32
+ this.isBuffering_ = false;
33
+
30
34
  /** @private {number} */
31
35
  this.rate_ = this.harness_.getRate();
32
36
 
@@ -51,15 +55,25 @@ shaka.media.PlayRateController = class {
51
55
  }
52
56
 
53
57
  /**
54
- * Set the playback rate.
58
+ * Sets the buffering flag, which controls the effective playback rate.
59
+ *
60
+ * @param {boolean} isBuffering If true, forces playback rate to 0 internally.
61
+ */
62
+ setBuffering(isBuffering) {
63
+ this.isBuffering_ = isBuffering;
64
+ this.apply_();
65
+ }
66
+
67
+ /**
68
+ * Set the playback rate. This rate will only be used as provided when the
69
+ * player is not buffering. You should never set the rate to 0.
55
70
  *
56
71
  * @param {number} rate
57
72
  */
58
73
  set(rate) {
59
- if (this.rate_ != rate) {
60
- this.rate_ = rate;
61
- this.apply_();
62
- }
74
+ goog.asserts.assert(rate != 0, 'Should never set rate of 0 explicitly!');
75
+ this.rate_ = rate;
76
+ this.apply_();
63
77
  }
64
78
 
65
79
  /**
@@ -93,11 +107,14 @@ shaka.media.PlayRateController = class {
93
107
  // Always stop the timer. We may not start it again.
94
108
  this.timer_.stop();
95
109
 
96
- shaka.log.v1('Changing effective playback rate to', this.rate_);
110
+ /** @type {number} */
111
+ const rate = this.calculateCurrentRate_();
112
+
113
+ shaka.log.v1('Changing effective playback rate to', rate);
97
114
 
98
- if (this.rate_ >= 0) {
115
+ if (rate >= 0) {
99
116
  try {
100
- this.applyRate_(this.rate_);
117
+ this.applyRate_(rate);
101
118
  return;
102
119
  } catch (e) {
103
120
  // Fall through to the next clause.
@@ -117,6 +134,17 @@ shaka.media.PlayRateController = class {
117
134
  this.applyRate_(0);
118
135
  }
119
136
 
137
+ /**
138
+ * Calculate the rate that the controller wants the media element to have
139
+ * based on the current state of the controller.
140
+ *
141
+ * @return {number}
142
+ * @private
143
+ */
144
+ calculateCurrentRate_() {
145
+ return this.isBuffering_ ? 0 : this.rate_;
146
+ }
147
+
120
148
  /**
121
149
  * If the new rate is different than the media element's playback rate, this
122
150
  * will change the playback rate. If the rate does not need to change, it will
@@ -382,10 +382,10 @@ shaka.media.MediaSourcePlayhead = class {
382
382
  seekStart = seekEnd - this.minSeekRange_;
383
383
  }
384
384
 
385
- if (currentTime < seekStart || currentTime > seekEnd) {
386
- // playhead is outside of the seekrange. Move playhead to catch up.
385
+ if (currentTime < seekStart) {
386
+ // The seek range has moved past the playhead. Move ahead to catch up.
387
387
  const targetTime = this.reposition_(currentTime);
388
- shaka.log.info('Jumping by ' + (targetTime - currentTime) +
388
+ shaka.log.info('Jumping forward ' + (targetTime - currentTime) +
389
389
  ' seconds to catch up with the seek range.');
390
390
  this.mediaElement_.currentTime = targetTime;
391
391
  }
@@ -694,6 +694,16 @@ shaka.media.MetaSegmentIndex = class extends shaka.media.SegmentIndex {
694
694
  this.indexes_ = [];
695
695
  }
696
696
 
697
+ /**
698
+ * @override
699
+ * @export
700
+ */
701
+ forEachTopLevelReference(fn) {
702
+ for (const index of this.indexes_) {
703
+ index.forEachTopLevelReference(fn);
704
+ }
705
+ }
706
+
697
707
  /**
698
708
  * @override
699
709
  * @export
@@ -598,6 +598,18 @@ shaka.media.SegmentReference = class {
598
598
  this.offset(offset);
599
599
  }
600
600
  }
601
+
602
+ /**
603
+ * Updates the init segment reference and propagates the update to all partial
604
+ * references.
605
+ * @param {shaka.media.InitSegmentReference} initSegmentReference
606
+ */
607
+ updateInitSegmentReference(initSegmentReference) {
608
+ this.initSegmentReference = initSegmentReference;
609
+ for (const partialReference of this.partialReferences) {
610
+ partialReference.updateInitSegmentReference(initSegmentReference);
611
+ }
612
+ }
601
613
  };
602
614
 
603
615
 
@@ -174,6 +174,9 @@ shaka.media.StallDetector.MediaElementImplementation = class {
174
174
  if (this.mediaElement_.paused) {
175
175
  return false;
176
176
  }
177
+ if (this.mediaElement_.playbackRate == 0) {
178
+ return false;
179
+ }
177
180
 
178
181
  // If we have don't have enough content, we are not stalled, we are
179
182
  // buffering.