shaka-player 4.0.0 → 4.1.0

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 (114) hide show
  1. package/.github/config.yml +1 -0
  2. package/.github/workflows/release-please.yaml +6 -11
  3. package/CHANGELOG.md +33 -0
  4. package/CONTRIBUTORS +1 -0
  5. package/README.md +4 -4
  6. package/build/misspellings.txt +1 -0
  7. package/demo/common/message_ids.js +1 -0
  8. package/demo/config.js +3 -1
  9. package/demo/demo.less +0 -1
  10. package/demo/locales/en.json +1 -0
  11. package/demo/locales/source.json +4 -0
  12. package/dist/controls.css +3 -3
  13. package/dist/controls.css.map +1 -1
  14. package/dist/demo.compiled.debug.js +4 -4
  15. package/dist/demo.compiled.debug.map +2 -2
  16. package/dist/demo.compiled.js +4 -4
  17. package/dist/demo.compiled.map +2 -2
  18. package/dist/demo.css +1 -1
  19. package/dist/demo.css.map +1 -1
  20. package/dist/deps.js +3 -3
  21. package/dist/locales.js +32 -32
  22. package/dist/receiver.compiled.debug.js +2 -2
  23. package/dist/receiver.compiled.debug.map +2 -2
  24. package/dist/receiver.compiled.js +2 -2
  25. package/dist/receiver.compiled.map +2 -2
  26. package/dist/shaka-player.compiled.d.ts +15 -8
  27. package/dist/shaka-player.compiled.debug.d.ts +15 -8
  28. package/dist/shaka-player.compiled.debug.externs.js +11 -2
  29. package/dist/shaka-player.compiled.debug.js +266 -253
  30. package/dist/shaka-player.compiled.debug.map +3 -3
  31. package/dist/shaka-player.compiled.externs.js +11 -2
  32. package/dist/shaka-player.compiled.js +792 -784
  33. package/dist/shaka-player.compiled.map +3 -3
  34. package/dist/shaka-player.ui.d.ts +16 -9
  35. package/dist/shaka-player.ui.debug.d.ts +16 -9
  36. package/dist/shaka-player.ui.debug.externs.js +11 -2
  37. package/dist/shaka-player.ui.debug.js +299 -282
  38. package/dist/shaka-player.ui.debug.map +3 -3
  39. package/dist/shaka-player.ui.externs.js +11 -2
  40. package/dist/shaka-player.ui.js +948 -941
  41. package/dist/shaka-player.ui.map +3 -3
  42. package/docs/design/chromecast.md +3 -1
  43. package/docs/tutorials/drm-config.md +16 -0
  44. package/docs/tutorials/ui.md +26 -0
  45. package/docs/tutorials/upgrade.md +4 -2
  46. package/externs/chromecast.js +9 -2
  47. package/externs/shaka/manifest.js +7 -0
  48. package/externs/shaka/player.js +32 -2
  49. package/externs/shaka/text.js +7 -0
  50. package/karma.conf.js +4 -0
  51. package/lib/abr/simple_abr_manager.js +2 -1
  52. package/lib/cast/cast_proxy.js +19 -8
  53. package/lib/cast/cast_receiver.js +3 -3
  54. package/lib/cast/cast_sender.js +12 -2
  55. package/lib/dash/dash_parser.js +2 -2
  56. package/lib/hls/hls_parser.js +46 -15
  57. package/lib/media/drm_engine.js +36 -8
  58. package/lib/media/gap_jumping_controller.js +23 -1
  59. package/lib/media/media_source_engine.js +91 -37
  60. package/lib/media/playhead.js +47 -4
  61. package/lib/media/stall_detector.js +19 -2
  62. package/lib/media/streaming_engine.js +9 -3
  63. package/lib/offline/indexeddb/storage_mechanism.js +4 -2
  64. package/lib/offline/manifest_converter.js +1 -0
  65. package/lib/player.js +339 -175
  66. package/lib/text/cue.js +6 -0
  67. package/lib/text/ui_text_displayer.js +1 -0
  68. package/lib/text/vtt_text_parser.js +11 -7
  69. package/lib/util/config_utils.js +25 -0
  70. package/lib/util/error.js +13 -0
  71. package/lib/util/fake_event.js +39 -0
  72. package/lib/util/manifest_parser_utils.js +1 -1
  73. package/lib/util/mime_utils.js +62 -6
  74. package/lib/util/periods.js +5 -4
  75. package/lib/util/player_configuration.js +16 -14
  76. package/lib/util/stats.js +28 -1
  77. package/lib/util/stream_utils.js +11 -2
  78. package/package.json +5 -2
  79. package/test/cast/cast_proxy_unit.js +3 -1
  80. package/test/cast/cast_sender_unit.js +21 -6
  81. package/test/demo/demo_unit.js +2 -1
  82. package/test/hls/hls_live_unit.js +79 -7
  83. package/test/hls/manifest_text_parser_unit.js +19 -0
  84. package/test/media/adaptation_set_unit.js +1 -0
  85. package/test/media/playhead_unit.js +114 -25
  86. package/test/media/stall_detector_unit.js +20 -2
  87. package/test/media/streaming_engine_integration.js +2 -1
  88. package/test/media/streaming_engine_unit.js +1 -0
  89. package/test/offline/indexeddb_storage_unit.js +1 -1
  90. package/test/offline/storage_compatibility_unit.js +1 -1
  91. package/test/player_external.js +7 -1
  92. package/test/player_integration.js +9 -10
  93. package/test/player_src_equals_integration.js +81 -83
  94. package/test/player_unit.js +240 -2
  95. package/test/test/boot.js +9 -0
  96. package/test/test/externs/filters.js +4 -0
  97. package/test/test/util/manifest_generator.js +2 -0
  98. package/test/test/util/simple_fakes.js +45 -2
  99. package/test/test/util/streaming_engine_util.js +1 -0
  100. package/test/text/vtt_text_parser_unit.js +68 -5
  101. package/test/ui/ui_unit.js +18 -5
  102. package/test/util/mime_utils_unit.js +54 -0
  103. package/test/util/periods_unit.js +79 -0
  104. package/test/util/stream_utils_unit.js +108 -0
  105. package/ui/controls.js +12 -8
  106. package/ui/externs/ui.js +10 -1
  107. package/ui/less/containers.less +1 -1
  108. package/ui/less/general.less +1 -1
  109. package/ui/less/range_elements.less +1 -0
  110. package/ui/resolution_selection.js +1 -1
  111. package/ui/statistics_button.js +10 -0
  112. package/ui/ui.js +12 -1
  113. package/.github/pull_request_template.md +0 -10
  114. package/docs/tutorials/gsoc-ideas-list.md +0 -3
package/lib/player.js CHANGED
@@ -385,6 +385,26 @@ goog.requireType('shaka.routing.Payload');
385
385
  */
386
386
 
387
387
 
388
+ /**
389
+ * @event shaka.Player.StallDetectedEvent
390
+ * @description Fired when a stall in playback is detected by the StallDetector.
391
+ * Not all stalls are caused by gaps in the buffered ranges.
392
+ * @property {string} type
393
+ * 'stalldetected'
394
+ * @exportDoc
395
+ */
396
+
397
+
398
+ /**
399
+ * @event shaka.Player.GapJumpedEvent
400
+ * @description Fired when the GapJumpingController jumps over a gap in the
401
+ * buffered ranges.
402
+ * @property {string} type
403
+ * 'gapjumped'
404
+ * @exportDoc
405
+ */
406
+
407
+
388
408
  /**
389
409
  * @summary The main player object for Shaka Player.
390
410
  *
@@ -423,8 +443,23 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
423
443
  */
424
444
  this.isTextVisible_ = false;
425
445
 
426
- /** @private {shaka.util.EventManager} */
427
- this.eventManager_ = new shaka.util.EventManager();
446
+ /**
447
+ * For listeners scoped to the lifetime of the Player instance.
448
+ * @private {shaka.util.EventManager}
449
+ */
450
+ this.globalEventManager_ = new shaka.util.EventManager();
451
+
452
+ /**
453
+ * For listeners scoped to the lifetime of the media element attachment.
454
+ * @private {shaka.util.EventManager}
455
+ */
456
+ this.attachEventManager_ = new shaka.util.EventManager();
457
+
458
+ /**
459
+ * For listeners scoped to the lifetime of the loaded content.
460
+ * @private {shaka.util.EventManager}
461
+ */
462
+ this.loadEventManager_ = new shaka.util.EventManager();
428
463
 
429
464
  /** @private {shaka.net.NetworkingEngine} */
430
465
  this.networkingEngine_ = null;
@@ -564,7 +599,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
564
599
 
565
600
  // If the browser comes back online after being offline, then try to play
566
601
  // again.
567
- this.eventManager_.listen(window, 'online', () => {
602
+ this.globalEventManager_.listen(window, 'online', () => {
568
603
  this.retryStreaming();
569
604
  });
570
605
 
@@ -637,7 +672,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
637
672
  },
638
673
  enterNode: (node, has, wants) => {
639
674
  this.dispatchEvent(this.makeEvent_(
640
- /* name= */ shaka.Player.EventName.OnStateChange,
675
+ /* name= */ shaka.util.FakeEvent.EventName.OnStateChange,
641
676
  /* data= */ (new Map()).set('state', node.name)));
642
677
 
643
678
  const action = actions.get(node);
@@ -668,7 +703,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
668
703
  },
669
704
  onIdle: (node) => {
670
705
  this.dispatchEvent(this.makeEvent_(
671
- /* name= */ shaka.Player.EventName.OnStateIdle,
706
+ /* name= */ shaka.util.FakeEvent.EventName.OnStateIdle,
672
707
  /* data= */ (new Map()).set('state', node.name)));
673
708
  },
674
709
  };
@@ -679,6 +714,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
679
714
  shaka.Player.createEmptyPayload_(),
680
715
  walkerImplementation);
681
716
 
717
+ /** @private {shaka.util.Timer} */
718
+ this.checkVariantsTimer_ =
719
+ new shaka.util.Timer(() => this.checkVariants_());
720
+
682
721
  // Even though |attach| will start in later interpreter cycles, it should be
683
722
  // the LAST thing we do in the constructor because conceptually it relies on
684
723
  // player having been initialized.
@@ -688,7 +727,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
688
727
  }
689
728
 
690
729
  /**
691
- * @param {!shaka.Player.EventName} name
730
+ * @param {!shaka.util.FakeEvent.EventName} name
692
731
  * @param {Map.<string, Object>=} data
693
732
  * @return {!shaka.util.FakeEvent}
694
733
  * @private
@@ -752,10 +791,18 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
752
791
  });
753
792
  await this.walker_.destroy();
754
793
 
755
- // Tear-down the event manager to ensure messages stop moving around.
756
- if (this.eventManager_) {
757
- this.eventManager_.release();
758
- this.eventManager_ = null;
794
+ // Tear-down the event managers to ensure handlers stop firing.
795
+ if (this.globalEventManager_) {
796
+ this.globalEventManager_.release();
797
+ this.globalEventManager_ = null;
798
+ }
799
+ if (this.attachEventManager_) {
800
+ this.attachEventManager_.release();
801
+ this.attachEventManager_ = null;
802
+ }
803
+ if (this.loadEventManager_) {
804
+ this.loadEventManager_.release();
805
+ this.loadEventManager_ = null;
759
806
  }
760
807
 
761
808
  this.abrManagerFactory_ = null;
@@ -1106,7 +1153,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1106
1153
 
1107
1154
  // We dispatch the loading event when someone calls |load| because we want
1108
1155
  // to surface the user intent.
1109
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Loading));
1156
+ this.dispatchEvent(this.makeEvent_(shaka.util.FakeEvent.EventName.Loading));
1110
1157
 
1111
1158
  // Right away we know what the asset uri and start-of-load time are. We will
1112
1159
  // fill-in the rest of the information later.
@@ -1170,7 +1217,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1170
1217
  events.onEnd = () => {
1171
1218
  resolve();
1172
1219
  // We dispatch the loaded event when the load promise is resolved
1173
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Loaded));
1220
+ this.dispatchEvent(
1221
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Loaded));
1174
1222
  };
1175
1223
  events.onCancel = () => reject(this.createAbortLoadError_());
1176
1224
  events.onError = (e) => reject(e);
@@ -1298,7 +1346,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1298
1346
  has.mediaElement = wants.mediaElement;
1299
1347
 
1300
1348
  const onError = (error) => this.onVideoError_(error);
1301
- this.eventManager_.listen(has.mediaElement, 'error', onError);
1349
+ this.attachEventManager_.listen(has.mediaElement, 'error', onError);
1302
1350
  }
1303
1351
 
1304
1352
  this.video_ = has.mediaElement;
@@ -1325,10 +1373,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1325
1373
  * @private
1326
1374
  */
1327
1375
  onDetach_(has, wants) {
1328
- // If we are going from "detached" to "detached" we wouldn't have
1376
+ // If we were going from "detached" to "detached" we wouldn't have
1329
1377
  // a media element to detach from.
1330
1378
  if (has.mediaElement) {
1331
- this.eventManager_.unlisten(has.mediaElement, 'error');
1379
+ this.attachEventManager_.removeAll();
1332
1380
  has.mediaElement = null;
1333
1381
  }
1334
1382
 
@@ -1375,7 +1423,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1375
1423
  await Promise.all(cleanupTasks);
1376
1424
 
1377
1425
  // Dispatch the unloading event.
1378
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Unloading));
1426
+ this.dispatchEvent(
1427
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Unloading));
1379
1428
 
1380
1429
  // Remove everything that has to do with loading content from our payload
1381
1430
  // since we are releasing everything that depended on it.
@@ -1386,13 +1435,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1386
1435
  // In most cases we should have a media element. The one exception would
1387
1436
  // be if there was an error and we, by chance, did not have a media element.
1388
1437
  if (has.mediaElement) {
1389
- this.eventManager_.unlisten(has.mediaElement, 'loadedmetadata');
1390
- this.eventManager_.unlisten(has.mediaElement, 'playing');
1391
- this.eventManager_.unlisten(has.mediaElement, 'pause');
1392
- this.eventManager_.unlisten(has.mediaElement, 'ended');
1393
- this.eventManager_.unlisten(has.mediaElement, 'ratechange');
1438
+ this.loadEventManager_.removeAll();
1394
1439
  }
1395
1440
 
1441
+ // Stop the variant checker timer
1442
+ this.checkVariantsTimer_.stop();
1443
+
1396
1444
  // Some observers use some playback components, shutting down the observers
1397
1445
  // first ensures that they don't try to use the playback components
1398
1446
  // mid-destroy.
@@ -1680,7 +1728,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1680
1728
  this.regionTimeline_.addEventListener('regionadd', (event) => {
1681
1729
  /** @type {shaka.extern.TimelineRegionInfo} */
1682
1730
  const region = event['region'];
1683
- this.onRegionEvent_(shaka.Player.EventName.TimelineRegionAdded, region);
1731
+ this.onRegionEvent_(
1732
+ shaka.util.FakeEvent.EventName.TimelineRegionAdded, region);
1684
1733
 
1685
1734
  if (this.adManager_) {
1686
1735
  this.adManager_.onDashTimedMetadata(region);
@@ -1735,7 +1784,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1735
1784
 
1736
1785
  // This event is fired after the manifest is parsed, but before any
1737
1786
  // filtering takes place.
1738
- const event = this.makeEvent_(shaka.Player.EventName.ManifestParsed);
1787
+ const event =
1788
+ this.makeEvent_(shaka.util.FakeEvent.EventName.ManifestParsed);
1739
1789
  this.dispatchEvent(event);
1740
1790
 
1741
1791
  // We require all manifests to have at least one variant.
@@ -1809,7 +1859,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1809
1859
  },
1810
1860
  onEvent: (e) => {
1811
1861
  this.dispatchEvent(e);
1812
- if (e.type == shaka.Player.EventName.DrmSessionUpdate && firstEvent) {
1862
+ if (e.type == shaka.util.FakeEvent.EventName.DrmSessionUpdate &&
1863
+ firstEvent) {
1813
1864
  firstEvent = false;
1814
1865
  const now = Date.now() / 1000;
1815
1866
  const delta = now - startTime;
@@ -1885,18 +1936,18 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1885
1936
  this.assetUri_ = assetUri;
1886
1937
 
1887
1938
  this.playRateController_ = new shaka.media.PlayRateController({
1888
- getRate: () => has.mediaElement.playbackRate,
1889
- getDefaultRate: () => has.mediaElement.defaultPlaybackRate,
1890
- setRate: (rate) => { has.mediaElement.playbackRate = rate; },
1891
- movePlayhead: (delta) => { has.mediaElement.currentTime += delta; },
1939
+ getRate: () => mediaElement.playbackRate,
1940
+ getDefaultRate: () => mediaElement.defaultPlaybackRate,
1941
+ setRate: (rate) => { mediaElement.playbackRate = rate; },
1942
+ movePlayhead: (delta) => { mediaElement.currentTime += delta; },
1892
1943
  });
1893
1944
 
1894
1945
  const updateStateHistory = () => this.updateStateHistory_();
1895
1946
  const onRateChange = () => this.onRateChange_();
1896
- this.eventManager_.listen(mediaElement, 'playing', updateStateHistory);
1897
- this.eventManager_.listen(mediaElement, 'pause', updateStateHistory);
1898
- this.eventManager_.listen(mediaElement, 'ended', updateStateHistory);
1899
- this.eventManager_.listen(mediaElement, 'ratechange', onRateChange);
1947
+ this.loadEventManager_.listen(mediaElement, 'playing', updateStateHistory);
1948
+ this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory);
1949
+ this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory);
1950
+ this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange);
1900
1951
 
1901
1952
  const abrFactory = this.config_.abrFactory;
1902
1953
  if (!this.abrManager_ || this.abrManagerFactory_ != abrFactory) {
@@ -1953,10 +2004,29 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1953
2004
  // "streaming" so that they can access internal information.
1954
2005
  this.loadMode_ = shaka.Player.LoadMode.MEDIA_SOURCE;
1955
2006
 
2007
+ if (mediaElement.textTracks) {
2008
+ this.loadEventManager_.listen(
2009
+ mediaElement.textTracks, 'addtrack', (e) => {
2010
+ const trackEvent = /** @type {!TrackEvent} */(e);
2011
+ if (trackEvent.track) {
2012
+ const track = trackEvent.track;
2013
+ goog.asserts.assert(
2014
+ track instanceof TextTrack, 'Wrong track type!');
2015
+
2016
+ switch (track.kind) {
2017
+ case 'chapters':
2018
+ this.activateChaptersTrack_(track);
2019
+ break;
2020
+ }
2021
+ }
2022
+ });
2023
+ }
2024
+
1956
2025
  // The event must be fired after we filter by restrictions but before the
1957
2026
  // active stream is picked to allow those listening for the "streaming"
1958
2027
  // event to make changes before streaming starts.
1959
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Streaming));
2028
+ this.dispatchEvent(
2029
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Streaming));
1960
2030
 
1961
2031
  // Pick the initial streams to play.
1962
2032
  // however, we would skip switch to initial variant
@@ -2039,7 +2109,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2039
2109
  }
2040
2110
 
2041
2111
  // Wait for the 'loadedmetadata' event to measure load() latency.
2042
- this.eventManager_.listenOnce(mediaElement, 'loadedmetadata', () => {
2112
+ this.loadEventManager_.listenOnce(mediaElement, 'loadedmetadata', () => {
2043
2113
  const now = Date.now() / 1000;
2044
2114
  const delta = now - wants.startTimeOfLoad;
2045
2115
  this.stats_.setLoadLatency(delta);
@@ -2084,7 +2154,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2084
2154
  },
2085
2155
  onEvent: (e) => {
2086
2156
  this.dispatchEvent(e);
2087
- if (e.type == shaka.Player.EventName.DrmSessionUpdate && firstEvent) {
2157
+ if (e.type == shaka.util.FakeEvent.EventName.DrmSessionUpdate &&
2158
+ firstEvent) {
2088
2159
  firstEvent = false;
2089
2160
  const now = Date.now() / 1000;
2090
2161
  const delta = now - startTime;
@@ -2117,6 +2188,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2117
2188
  const variant = {
2118
2189
  id: 0,
2119
2190
  language: 'und',
2191
+ disabledUntilTime: 0,
2120
2192
  primary: false,
2121
2193
  audio: null,
2122
2194
  video: {
@@ -2192,11 +2264,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2192
2264
  // Save the uri so that it can be used outside of the load-graph.
2193
2265
  this.assetUri_ = has.uri;
2194
2266
 
2195
- this.playhead_ = new shaka.media.SrcEqualsPlayhead(has.mediaElement);
2267
+ const mediaElement = has.mediaElement;
2196
2268
 
2197
- // This flag is used below in the language preference setup and
2198
- // track-management to check if this load was canceled before the necessary
2199
- // events fired.
2269
+ this.playhead_ = new shaka.media.SrcEqualsPlayhead(mediaElement);
2270
+
2271
+ // This flag is used below in the language preference setup to check if
2272
+ // this load was canceled before the necessary awaits completed.
2200
2273
  let unloaded = false;
2201
2274
  this.cleanupOnUnload_.push(() => {
2202
2275
  unloaded = true;
@@ -2207,10 +2280,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2207
2280
  }
2208
2281
 
2209
2282
  this.playRateController_ = new shaka.media.PlayRateController({
2210
- getRate: () => has.mediaElement.playbackRate,
2211
- getDefaultRate: () => has.mediaElement.defaultPlaybackRate,
2212
- setRate: (rate) => { has.mediaElement.playbackRate = rate; },
2213
- movePlayhead: (delta) => { has.mediaElement.currentTime += delta; },
2283
+ getRate: () => mediaElement.playbackRate,
2284
+ getDefaultRate: () => mediaElement.defaultPlaybackRate,
2285
+ setRate: (rate) => { mediaElement.playbackRate = rate; },
2286
+ movePlayhead: (delta) => { mediaElement.currentTime += delta; },
2214
2287
  });
2215
2288
 
2216
2289
  // We need to start the buffer management code near the end because it will
@@ -2222,17 +2295,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2222
2295
  // Add all media element listeners.
2223
2296
  const updateStateHistory = () => this.updateStateHistory_();
2224
2297
  const onRateChange = () => this.onRateChange_();
2225
- this.eventManager_.listen(has.mediaElement, 'playing', updateStateHistory);
2226
- this.eventManager_.listen(has.mediaElement, 'pause', updateStateHistory);
2227
- this.eventManager_.listen(has.mediaElement, 'ended', updateStateHistory);
2228
- this.eventManager_.listen(has.mediaElement, 'ratechange', onRateChange);
2298
+ this.loadEventManager_.listen(mediaElement, 'playing', updateStateHistory);
2299
+ this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory);
2300
+ this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory);
2301
+ this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange);
2229
2302
 
2230
2303
  // Wait for the 'loadedmetadata' event to measure load() latency, but only
2231
2304
  // if preload is set in a way that would result in this event firing
2232
2305
  // automatically.
2233
2306
  // See https://github.com/shaka-project/shaka-player/issues/2483
2234
- if (this.video_.preload != 'none') {
2235
- this.eventManager_.listenOnce(this.video_, 'loadedmetadata', () => {
2307
+ if (mediaElement.preload != 'none') {
2308
+ this.loadEventManager_.listenOnce(mediaElement, 'loadedmetadata', () => {
2236
2309
  const now = Date.now() / 1000;
2237
2310
  const delta = now - wants.startTimeOfLoad;
2238
2311
  this.stats_.setLoadLatency(delta);
@@ -2242,44 +2315,47 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2242
2315
  // The audio tracks are only available on Safari at the moment, but this
2243
2316
  // drives the tracks API for Safari's native HLS. So when they change,
2244
2317
  // fire the corresponding Shaka Player event.
2245
- if (this.video_.audioTracks) {
2246
- this.eventManager_.listen(
2247
- this.video_.audioTracks, 'addtrack', () => this.onTracksChanged_());
2248
- this.eventManager_.listen(
2249
- this.video_.audioTracks, 'removetrack',
2318
+ if (mediaElement.audioTracks) {
2319
+ this.loadEventManager_.listen(
2320
+ mediaElement.audioTracks, 'addtrack', () => this.onTracksChanged_());
2321
+ this.loadEventManager_.listen(
2322
+ mediaElement.audioTracks, 'removetrack',
2250
2323
  () => this.onTracksChanged_());
2251
- this.eventManager_.listen(
2252
- this.video_.audioTracks, 'change', () => this.onTracksChanged_());
2253
- }
2254
- if (this.video_.textTracks) {
2255
- this.eventManager_.listen(this.video_.textTracks, 'addtrack', (e) => {
2256
- // If we have moved on to another piece of content while waiting for
2257
- // the above event, we should not process tracks here.
2258
- if (unloaded) {
2259
- return;
2260
- }
2324
+ this.loadEventManager_.listen(
2325
+ mediaElement.audioTracks, 'change', () => this.onTracksChanged_());
2326
+ }
2327
+
2328
+ if (mediaElement.textTracks) {
2329
+ this.loadEventManager_.listen(
2330
+ mediaElement.textTracks, 'addtrack', (e) => {
2331
+ const trackEvent = /** @type {!TrackEvent} */(e);
2332
+ if (trackEvent.track) {
2333
+ const track = trackEvent.track;
2334
+ goog.asserts.assert(
2335
+ track instanceof TextTrack, 'Wrong track type!');
2336
+
2337
+ switch (track.kind) {
2338
+ case 'metadata':
2339
+ this.processTimedMetadataSrcEqls_(track);
2340
+ break;
2341
+
2342
+ case 'chapters':
2343
+ this.activateChaptersTrack_(track);
2344
+ break;
2345
+
2346
+ default:
2347
+ this.onTracksChanged_();
2348
+ break;
2349
+ }
2350
+ }
2351
+ });
2261
2352
 
2262
- const trackEvent = /** @type {!TrackEvent} */(e);
2263
- if (trackEvent.track) {
2264
- const track = trackEvent.track;
2265
- goog.asserts.assert(track instanceof TextTrack, 'Wrong track type!');
2266
- switch (track.kind) {
2267
- case 'metadata':
2268
- this.processTimedMetadataSrcEqls_(track);
2269
- break;
2270
- case 'chapters':
2271
- this.processChaptersTrack_(track);
2272
- break;
2273
- default:
2274
- this.onTracksChanged_();
2275
- break;
2276
- }
2277
- }
2278
- });
2279
- this.eventManager_.listen(
2280
- this.video_.textTracks, 'removetrack', () => this.onTracksChanged_());
2281
- this.eventManager_.listen(
2282
- this.video_.textTracks, 'change', () => this.onTracksChanged_());
2353
+ this.loadEventManager_.listen(
2354
+ mediaElement.textTracks, 'removetrack',
2355
+ () => this.onTracksChanged_());
2356
+ this.loadEventManager_.listen(
2357
+ mediaElement.textTracks, 'change',
2358
+ () => this.onTracksChanged_());
2283
2359
  }
2284
2360
 
2285
2361
  const extension = shaka.media.ManifestParser.getExtension(has.uri);
@@ -2288,14 +2364,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2288
2364
 
2289
2365
  // By setting |src| we are done "loading" with src=. We don't need to set
2290
2366
  // the current time because |playhead| will do that for us.
2291
- has.mediaElement.src = this.cmcdManager_.appendSrcData(has.uri, mimeType);
2367
+ mediaElement.src = this.cmcdManager_.appendSrcData(has.uri, mimeType);
2292
2368
 
2293
2369
  // Tizen 3 / WebOS won't load anything unless you call load() explicitly,
2294
2370
  // no matter the value of the preload attribute. This is harmful on some
2295
2371
  // other platforms by triggering unbounded loading of media data, but is
2296
2372
  // necessary here.
2297
2373
  if (shaka.util.Platform.isTizen() || shaka.util.Platform.isWebOS()) {
2298
- has.mediaElement.load();
2374
+ mediaElement.load();
2299
2375
  }
2300
2376
 
2301
2377
  // Set the load mode last so that we know that all our components are
@@ -2305,30 +2381,25 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2305
2381
  // The event doesn't mean as much for src= playback, since we don't control
2306
2382
  // streaming. But we should fire it in this path anyway since some
2307
2383
  // applications may be expecting it as a life-cycle event.
2308
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Streaming));
2384
+ this.dispatchEvent(
2385
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Streaming));
2309
2386
 
2310
2387
  // The "load" Promise is resolved when we have loaded the metadata. If we
2311
2388
  // wait for the full data, that won't happen on Safari until the play button
2312
2389
  // is hit.
2313
2390
  const fullyLoaded = new shaka.util.PublicPromise();
2314
- shaka.util.MediaReadyState.waitForReadyState(this.video_,
2391
+ shaka.util.MediaReadyState.waitForReadyState(mediaElement,
2315
2392
  HTMLMediaElement.HAVE_METADATA,
2316
- this.eventManager_,
2393
+ this.loadEventManager_,
2317
2394
  () => {
2318
2395
  fullyLoaded.resolve();
2319
2396
  });
2320
2397
 
2321
2398
  // We can't switch to preferred languages, though, until the data is loaded.
2322
- shaka.util.MediaReadyState.waitForReadyState(this.video_,
2399
+ shaka.util.MediaReadyState.waitForReadyState(mediaElement,
2323
2400
  HTMLMediaElement.HAVE_CURRENT_DATA,
2324
- this.eventManager_,
2401
+ this.loadEventManager_,
2325
2402
  async () => {
2326
- // If we have moved on to another piece of content while waiting for
2327
- // the above event, we should not change tracks here.
2328
- if (unloaded) {
2329
- return;
2330
- }
2331
-
2332
2403
  this.setupPreferredAudioOnSrc_();
2333
2404
 
2334
2405
  // Applying the text preference too soon can result in it being
@@ -2336,8 +2407,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2336
2407
  const textTracks = this.getFilteredTextTracks_();
2337
2408
  if (!textTracks.find((t) => t.mode != 'disabled')) {
2338
2409
  await new Promise((resolve) => {
2339
- this.eventManager_.listenOnce(
2340
- this.video_.textTracks, 'change', resolve);
2410
+ this.loadEventManager_.listenOnce(
2411
+ mediaElement.textTracks, 'change', resolve);
2412
+
2341
2413
  // We expect the event to fire because it does on Safari.
2342
2414
  // But in case it doesn't on some other platform or future
2343
2415
  // version, move on in 1 second no matter what. This keeps the
@@ -2356,10 +2428,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2356
2428
  this.setupPreferredTextOnSrc_();
2357
2429
  });
2358
2430
 
2359
- if (this.video_.error) {
2431
+ if (mediaElement.error) {
2360
2432
  // Already failed!
2361
2433
  fullyLoaded.reject(this.videoErrorToShakaError_());
2362
- } else if (this.video_.preload == 'none') {
2434
+ } else if (mediaElement.preload == 'none') {
2363
2435
  shaka.log.alwaysWarn(
2364
2436
  'With <video preload="none">, the browser will not load anything ' +
2365
2437
  'until play() is called. We are unable to measure load latency in ' +
@@ -2370,7 +2442,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2370
2442
  fullyLoaded.resolve();
2371
2443
  }
2372
2444
 
2373
- this.eventManager_.listenOnce(this.video_, 'error', () => {
2445
+ this.loadEventManager_.listenOnce(mediaElement, 'error', () => {
2374
2446
  fullyLoaded.reject(this.videoErrorToShakaError_());
2375
2447
  });
2376
2448
 
@@ -2454,7 +2526,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2454
2526
 
2455
2527
  // Hidden mode is required for the cuechange event to launch correctly
2456
2528
  track.mode = 'hidden';
2457
- this.eventManager_.listen(track, 'cuechange', () => {
2529
+ this.loadEventManager_.listen(track, 'cuechange', () => {
2458
2530
  if (!track.activeCues) {
2459
2531
  return;
2460
2532
  }
@@ -2522,7 +2594,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2522
2594
  dispatchMetadataEvent_(startTime, endTime, metadataType, payload) {
2523
2595
  goog.asserts.assert(!endTime || startTime <= endTime,
2524
2596
  'Metadata start time should be less or equal to the end time!');
2525
- const eventName = shaka.Player.EventName.Metadata;
2597
+ const eventName = shaka.util.FakeEvent.EventName.Metadata;
2526
2598
  const data = new Map()
2527
2599
  .set('startTime', startTime)
2528
2600
  .set('endTime', endTime)
@@ -2532,12 +2604,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2532
2604
  }
2533
2605
 
2534
2606
  /**
2535
- * We're looking for chapters tracks to process the chapters.
2607
+ * Set the mode on a chapters track so that it loads.
2536
2608
  *
2537
2609
  * @param {?TextTrack} track
2538
2610
  * @private
2539
2611
  */
2540
- processChaptersTrack_(track) {
2612
+ activateChaptersTrack_(track) {
2541
2613
  if (!track || track.kind != 'chapters') {
2542
2614
  return;
2543
2615
  }
@@ -2550,10 +2622,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2550
2622
  // this process to be repeated several times to ensure that it has been put
2551
2623
  // in the correct mode.
2552
2624
  const timer = new shaka.util.Timer(() => {
2553
- const chaptersTracks = this.getChaptersTracks_();
2554
- for (const chaptersTrack of chaptersTracks) {
2555
- chaptersTrack.mode = 'hidden';
2556
- }
2625
+ track.mode = 'hidden';
2557
2626
  }).tickNow().tickAfter(0.5);
2558
2627
 
2559
2628
  this.cleanupOnUnload_.push(() => {
@@ -2622,7 +2691,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2622
2691
  /** @type {shaka.net.NetworkingEngine.OnHeadersReceived} */
2623
2692
  const onHeadersReceived_ = (headers, request, requestType) => {
2624
2693
  // Release a 'downloadheadersreceived' event.
2625
- const name = shaka.Player.EventName.DownloadHeadersReceived;
2694
+ const name = shaka.util.FakeEvent.EventName.DownloadHeadersReceived;
2626
2695
  const data = new Map()
2627
2696
  .set('headers', headers)
2628
2697
  .set('request', request)
@@ -2632,7 +2701,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2632
2701
  /** @type {shaka.net.NetworkingEngine.OnDownloadFailed} */
2633
2702
  const onDownloadFailed_ = (request, error, httpResponseCode, aborted) => {
2634
2703
  // Release a 'downloadfailed' event.
2635
- const name = shaka.Player.EventName.DownloadFailed;
2704
+ const name = shaka.util.FakeEvent.EventName.DownloadFailed;
2636
2705
  const data = new Map()
2637
2706
  .set('request', request)
2638
2707
  .set('error', error)
@@ -2660,7 +2729,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2660
2729
  this.manifest_,
2661
2730
  this.config_.streaming,
2662
2731
  startTime,
2663
- () => this.onSeek_());
2732
+ () => this.onSeek_(),
2733
+ (event) => this.dispatchEvent(event));
2664
2734
  }
2665
2735
 
2666
2736
  /**
@@ -2682,13 +2752,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2682
2752
  regionObserver.addEventListener('enter', (event) => {
2683
2753
  /** @type {shaka.extern.TimelineRegionInfo} */
2684
2754
  const region = event['region'];
2685
- this.onRegionEvent_(shaka.Player.EventName.TimelineRegionEnter, region);
2755
+ this.onRegionEvent_(
2756
+ shaka.util.FakeEvent.EventName.TimelineRegionEnter, region);
2686
2757
  });
2687
2758
 
2688
2759
  regionObserver.addEventListener('exit', (event) => {
2689
2760
  /** @type {shaka.extern.TimelineRegionInfo} */
2690
2761
  const region = event['region'];
2691
- this.onRegionEvent_(shaka.Player.EventName.TimelineRegionExit, region);
2762
+ this.onRegionEvent_(
2763
+ shaka.util.FakeEvent.EventName.TimelineRegionExit, region);
2692
2764
  });
2693
2765
 
2694
2766
  regionObserver.addEventListener('skip', (event) => {
@@ -2699,8 +2771,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2699
2771
  // If we are seeking, we don't want to surface the enter/exit events since
2700
2772
  // they didn't play through them.
2701
2773
  if (!seeking) {
2702
- this.onRegionEvent_(shaka.Player.EventName.TimelineRegionEnter, region);
2703
- this.onRegionEvent_(shaka.Player.EventName.TimelineRegionExit, region);
2774
+ this.onRegionEvent_(
2775
+ shaka.util.FakeEvent.EventName.TimelineRegionEnter, region);
2776
+ this.onRegionEvent_(
2777
+ shaka.util.FakeEvent.EventName.TimelineRegionExit, region);
2704
2778
  }
2705
2779
  });
2706
2780
 
@@ -4056,8 +4130,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4056
4130
  for (const chaptersTrack of chaptersTracksWithLanguage) {
4057
4131
  if (chaptersTrack && chaptersTrack.cues) {
4058
4132
  for (const cue of chaptersTrack.cues) {
4133
+ let id = cue.id;
4134
+ if (!id || id == '') {
4135
+ id = cue.startTime + '-' + cue.endTime + '-' + cue.text;
4136
+ }
4059
4137
  /** @type {shaka.extern.Chapter} */
4060
4138
  const chapter = {
4139
+ id: id,
4061
4140
  title: cue.text,
4062
4141
  startTime: cue.startTime,
4063
4142
  endTime: cue.endTime,
@@ -4322,6 +4401,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4322
4401
  this.stats_.setCompletionPercent(Math.round(100 * completionRatio));
4323
4402
  }
4324
4403
 
4404
+ if (this.playhead_) {
4405
+ this.stats_.setGapsJumped(this.playhead_.getGapsJumped());
4406
+ this.stats_.setStallsDetected(this.playhead_.getStallsDetected());
4407
+ }
4408
+
4325
4409
  if (element.getVideoPlaybackQuality) {
4326
4410
  const info = element.getVideoPlaybackQuality();
4327
4411
 
@@ -4537,25 +4621,41 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4537
4621
  if (!mimeType) {
4538
4622
  mimeType = await this.getTextMimetype_(uri);
4539
4623
  }
4624
+
4540
4625
  let adCuePoints = [];
4541
4626
  if (this.adManager_) {
4542
4627
  try {
4543
4628
  adCuePoints = this.adManager_.getServerSideCuePoints();
4544
4629
  } catch (error) {}
4545
4630
  }
4546
- await this.addSrcTrackElement_(uri, language, /* kind= */ 'chapters',
4547
- mimeType, /* label= */ '', adCuePoints);
4631
+
4632
+ /** @type {!HTMLTrackElement} */
4633
+ const trackElement = await this.addSrcTrackElement_(
4634
+ uri, language, /* kind= */ 'chapters', mimeType, /* label= */ '',
4635
+ adCuePoints);
4636
+
4548
4637
  const chaptersTracks = this.getChaptersTracks();
4549
4638
  const chaptersTrack = chaptersTracks.find((t) => {
4550
4639
  return t.language == language;
4551
4640
  });
4641
+
4552
4642
  if (chaptersTrack) {
4553
- const html5ChaptersTracks = this.getChaptersTracks_();
4554
- for (const html5ChaptersTrack of html5ChaptersTracks) {
4555
- this.processChaptersTrack_(html5ChaptersTrack);
4556
- }
4643
+ await new Promise((resolve, reject) => {
4644
+ // The chapter data isn't available until the 'load' event fires, and
4645
+ // that won't happen until the chapters track is activated by the
4646
+ // activateChaptersTrack_ method.
4647
+ this.loadEventManager_.listenOnce(trackElement, 'load', resolve);
4648
+ this.loadEventManager_.listenOnce(trackElement, 'error', (event) => {
4649
+ reject(new shaka.util.Error(
4650
+ shaka.util.Error.Severity.RECOVERABLE,
4651
+ shaka.util.Error.Category.TEXT,
4652
+ shaka.util.Error.Code.CHAPTERS_TRACK_FAILED));
4653
+ });
4654
+ });
4655
+
4557
4656
  return chaptersTrack;
4558
4657
  }
4658
+
4559
4659
  // This should not happen, but there are browser implementations that may
4560
4660
  // not support the Track element.
4561
4661
  shaka.log.error('Cannot add this text when loaded with src=');
@@ -4609,6 +4709,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4609
4709
  * @param {string} mimeType
4610
4710
  * @param {string} label
4611
4711
  * @param {!Array.<!shaka.extern.AdCuePoint>} adCuePoints
4712
+ * @return {!Promise.<!HTMLTrackElement>}
4612
4713
  * @private
4613
4714
  */
4614
4715
  async addSrcTrackElement_(uri, language, kind, mimeType, label,
@@ -4624,12 +4725,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4624
4725
  uri = shaka.media.MediaSourceEngine.createObjectURL(blob);
4625
4726
  mimeType = 'text/vtt';
4626
4727
  }
4728
+
4627
4729
  const trackElement =
4628
4730
  /** @type {!HTMLTrackElement} */(document.createElement('track'));
4629
4731
  trackElement.src = this.cmcdManager_.appendTextTrackData(uri);
4630
4732
  trackElement.label = label;
4631
4733
  trackElement.kind = kind;
4632
4734
  trackElement.srclang = language;
4735
+
4633
4736
  // Because we're pulling in the text track file via Javascript, the
4634
4737
  // same-origin policy applies. If you'd like to have a player served
4635
4738
  // from one domain, but the text track served from another, you'll
@@ -4639,7 +4742,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4639
4742
  if (!this.video_.getAttribute('crossorigin')) {
4640
4743
  this.video_.setAttribute('crossorigin', 'anonymous');
4641
4744
  }
4745
+
4642
4746
  this.video_.appendChild(trackElement);
4747
+ return trackElement;
4643
4748
  }
4644
4749
 
4645
4750
  /**
@@ -5010,7 +5115,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5010
5115
  }
5011
5116
 
5012
5117
  await stream.createSegmentIndex();
5013
- const ref = stream.segmentIndex[Symbol.iterator]().seek(time);
5118
+ const iter = stream.segmentIndex.getIteratorForTime(time);
5119
+ const ref = iter ? iter.next().value : null;
5014
5120
  if (!ref) {
5015
5121
  return null;
5016
5122
  }
@@ -5063,7 +5169,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5063
5169
 
5064
5170
  // Surface the buffering event so that the app knows if/when we are
5065
5171
  // buffering.
5066
- const eventName = shaka.Player.EventName.Buffering;
5172
+ const eventName = shaka.util.FakeEvent.EventName.Buffering;
5067
5173
  const data = (new Map()).set('buffering', isBuffering);
5068
5174
  this.dispatchEvent(this.makeEvent_(eventName, data));
5069
5175
  }
@@ -5098,7 +5204,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5098
5204
  this.playRateController_.set(newRate);
5099
5205
  }
5100
5206
 
5101
- const event = this.makeEvent_(shaka.Player.EventName.RateChange);
5207
+ const event = this.makeEvent_(shaka.util.FakeEvent.EventName.RateChange);
5102
5208
  this.dispatchEvent(event);
5103
5209
  }
5104
5210
 
@@ -5198,6 +5304,21 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5198
5304
  }
5199
5305
  }
5200
5306
 
5307
+ /**
5308
+ * Re-apply restrictions to the variants, to re-enable variants that were
5309
+ * temporarily disabled due to network errors.
5310
+ * If any variants are enabled this way, a new variant might be chosen for
5311
+ * playback.
5312
+ * @private
5313
+ */
5314
+ checkVariants_() {
5315
+ const tracksChanged = shaka.util.StreamUtils.applyRestrictions(
5316
+ this.manifest_.variants, this.config_.restrictions, this.maxHwRes_);
5317
+ if (tracksChanged) {
5318
+ this.chooseVariant_();
5319
+ }
5320
+ }
5321
+
5201
5322
  /**
5202
5323
  * Choose a text stream from all possible text streams while taking into
5203
5324
  * account user preference.
@@ -5430,7 +5551,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5430
5551
  const data = new Map()
5431
5552
  .set('oldTrack', from)
5432
5553
  .set('newTrack', to);
5433
- const event = this.makeEvent_(shaka.Player.EventName.Adaptation, data);
5554
+ const event =
5555
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Adaptation, data);
5434
5556
  this.delayDispatchEvent_(event);
5435
5557
  }
5436
5558
 
@@ -5441,7 +5563,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5441
5563
  onTracksChanged_() {
5442
5564
  // Delay the 'trackschanged' event so StreamingEngine has time to absorb the
5443
5565
  // changes before the user tries to query it.
5444
- const event = this.makeEvent_(shaka.Player.EventName.TracksChanged);
5566
+ const event = this.makeEvent_(shaka.util.FakeEvent.EventName.TracksChanged);
5445
5567
  this.delayDispatchEvent_(event);
5446
5568
  }
5447
5569
 
@@ -5457,7 +5579,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5457
5579
  const data = new Map()
5458
5580
  .set('oldTrack', from)
5459
5581
  .set('newTrack', to);
5460
- const event = this.makeEvent_(shaka.Player.EventName.VariantChanged, data);
5582
+ const event =
5583
+ this.makeEvent_(shaka.util.FakeEvent.EventName.VariantChanged, data);
5461
5584
  this.delayDispatchEvent_(event);
5462
5585
  }
5463
5586
 
@@ -5468,13 +5591,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5468
5591
  onTextChanged_() {
5469
5592
  // Delay the 'textchanged' event so StreamingEngine time to absorb the
5470
5593
  // changes before the user tries to query it.
5471
- const event = this.makeEvent_(shaka.Player.EventName.TextChanged);
5594
+ const event = this.makeEvent_(shaka.util.FakeEvent.EventName.TextChanged);
5472
5595
  this.delayDispatchEvent_(event);
5473
5596
  }
5474
5597
 
5475
5598
  /** @private */
5476
5599
  onTextTrackVisibility_() {
5477
- const event = this.makeEvent_(shaka.Player.EventName.TextTrackVisibility);
5600
+ const event =
5601
+ this.makeEvent_(shaka.util.FakeEvent.EventName.TextTrackVisibility);
5478
5602
  this.delayDispatchEvent_(event);
5479
5603
  }
5480
5604
 
@@ -5482,7 +5606,73 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5482
5606
  onAbrStatusChanged_() {
5483
5607
  const data = (new Map()).set('newStatus', this.config_.abr.enabled);
5484
5608
  this.delayDispatchEvent_(this.makeEvent_(
5485
- shaka.Player.EventName.AbrStatusChanged, data));
5609
+ shaka.util.FakeEvent.EventName.AbrStatusChanged, data));
5610
+ }
5611
+
5612
+ /**
5613
+ * Tries to recover from NETWORK HTTP_ERROR, temporary disabling the current
5614
+ * problematic variant.
5615
+ * @param {!shaka.util.Error} error
5616
+ * @return {boolean}
5617
+ * @private
5618
+ */
5619
+ tryToRecoverFromError_(error) {
5620
+ if ((error.code != shaka.util.Error.Code.HTTP_ERROR &&
5621
+ error.code != shaka.util.Error.Code.SEGMENT_MISSING) ||
5622
+ error.category != shaka.util.Error.Category.NETWORK) {
5623
+ return false;
5624
+ }
5625
+
5626
+ let maxDisabledTime = this.config_.streaming.maxDisabledTime;
5627
+ if (maxDisabledTime == 0) {
5628
+ if (error.code == shaka.util.Error.Code.SEGMENT_MISSING) {
5629
+ // Spec: https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-6.3.3
5630
+ // The client SHOULD NOT attempt to load Media Segments that have been
5631
+ // marked with an EXT-X-GAP tag, or to load Partial Segments with a
5632
+ // GAP=YES attribute. Instead, clients are encouraged to look for
5633
+ // another Variant Stream of the same Rendition which does not have the
5634
+ // same gap, and play that instead.
5635
+ maxDisabledTime = 1;
5636
+ } else {
5637
+ return false;
5638
+ }
5639
+ }
5640
+
5641
+ if (error.code == shaka.util.Error.Code.HTTP_ERROR) {
5642
+ shaka.log.debug('Recoverable NETWORK HTTP_ERROR, trying to recover...');
5643
+ }
5644
+
5645
+ // Obtain the active variant and disable it from manifest variants
5646
+ const activeVariantTrack = this.getVariantTracks().find((t) => t.active);
5647
+ goog.asserts.assert(activeVariantTrack, 'Active variant should be found');
5648
+ const manifest = this.manifest_;
5649
+ for (const variant of manifest.variants) {
5650
+ if (variant.id === activeVariantTrack.id) {
5651
+ variant.disabledUntilTime = (Date.now() / 1000) + maxDisabledTime;
5652
+ }
5653
+ }
5654
+
5655
+ // Apply restrictions in order to disable variants
5656
+ shaka.util.StreamUtils.applyRestrictions(
5657
+ manifest.variants, this.config_.restrictions, this.maxHwRes_);
5658
+
5659
+ // Select for a new variant
5660
+ const chosenVariant = this.chooseVariant_();
5661
+ if (!chosenVariant) {
5662
+ shaka.log.warning('Not enough variants to recover from error');
5663
+ return false;
5664
+ }
5665
+
5666
+ // Get the safeMargin to ensure a seamless playback
5667
+ const {video} = this.getBufferedInfo();
5668
+ const safeMargin =
5669
+ video.reduce((size, {start, end}) => size + end - start, 0);
5670
+
5671
+ this.switchVariant_(chosenVariant, /* fromAdaptation= */ false,
5672
+ /* clearBuffers= */ true, /* safeMargin= */ safeMargin);
5673
+
5674
+ this.checkVariantsTimer_.tickAfter(maxDisabledTime);
5675
+ return true;
5486
5676
  }
5487
5677
 
5488
5678
  /**
@@ -5498,7 +5688,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5498
5688
  return;
5499
5689
  }
5500
5690
 
5501
- const eventName = shaka.Player.EventName.Error;
5691
+
5692
+ if (this.tryToRecoverFromError_(error)) {
5693
+ error.handled = true;
5694
+ return;
5695
+ }
5696
+
5697
+ const eventName = shaka.util.FakeEvent.EventName.Error;
5502
5698
  const event = this.makeEvent_(eventName, (new Map()).set('detail', error));
5503
5699
  this.dispatchEvent(event);
5504
5700
  if (event.defaultPrevented) {
@@ -5512,7 +5708,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5512
5708
  * copy here because this is the transition point between the player and the
5513
5709
  * app.
5514
5710
  *
5515
- * @param {!shaka.Player.EventName} eventName
5711
+ * @param {!shaka.util.FakeEvent.EventName} eventName
5516
5712
  * @param {shaka.extern.TimelineRegionInfo} region
5517
5713
  *
5518
5714
  * @private
@@ -5561,7 +5757,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5561
5757
  .set('position', position);
5562
5758
 
5563
5759
  this.dispatchEvent(this.makeEvent_(
5564
- shaka.Player.EventName.MediaQualityChanged, data));
5760
+ shaka.util.FakeEvent.EventName.MediaQualityChanged, data));
5565
5761
  }
5566
5762
 
5567
5763
  /**
@@ -5679,7 +5875,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5679
5875
  } // if (keyIds.size)
5680
5876
 
5681
5877
  if (tracksChanged) {
5682
- this.updateAbrManagerVariants_();
5878
+ const variantsUpdated = this.updateAbrManagerVariants_();
5879
+ if (!variantsUpdated) {
5880
+ return;
5881
+ }
5683
5882
  }
5684
5883
 
5685
5884
  const currentVariant = this.streamingEngine_.getCurrentVariant();
@@ -5704,7 +5903,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5704
5903
  this.parser_.onExpirationUpdated(keyId, expiration);
5705
5904
  }
5706
5905
 
5707
- const event = this.makeEvent_(shaka.Player.EventName.ExpirationUpdated);
5906
+ const event =
5907
+ this.makeEvent_(shaka.util.FakeEvent.EventName.ExpirationUpdated);
5708
5908
  this.dispatchEvent(event);
5709
5909
  }
5710
5910
 
@@ -6317,42 +6517,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6317
6517
  }
6318
6518
  };
6319
6519
 
6320
- /**
6321
- * An internal enum that contains the string values of all of the player events.
6322
- * This exists primarily to act as an implicit list of events, for tests.
6323
- *
6324
- * @enum {string}
6325
- */
6326
- shaka.Player.EventName = {
6327
- AbrStatusChanged: 'abrstatuschanged',
6328
- Adaptation: 'adaptation',
6329
- Buffering: 'buffering',
6330
- DownloadFailed: 'downloadfailed',
6331
- DownloadHeadersReceived: 'downloadheadersreceived',
6332
- DrmSessionUpdate: 'drmsessionupdate',
6333
- Emsg: 'emsg',
6334
- Error: 'error',
6335
- ExpirationUpdated: 'expirationupdated',
6336
- Loaded: 'loaded',
6337
- Loading: 'loading',
6338
- ManifestParsed: 'manifestparsed',
6339
- MediaQualityChanged: 'mediaqualitychanged',
6340
- Metadata: 'metadata',
6341
- OnStateChange: 'onstatechange',
6342
- OnStateIdle: 'onstateidle',
6343
- RateChange: 'ratechange',
6344
- SessionDataEvent: 'sessiondata',
6345
- Streaming: 'streaming',
6346
- TextChanged: 'textchanged',
6347
- TextTrackVisibility: 'texttrackvisibility',
6348
- TimelineRegionAdded: 'timelineregionadded',
6349
- TimelineRegionEnter: 'timelineregionenter',
6350
- TimelineRegionExit: 'timelineregionexit',
6351
- TracksChanged: 'trackschanged',
6352
- Unloading: 'unloading',
6353
- VariantChanged: 'variantchanged',
6354
- };
6355
-
6356
6520
 
6357
6521
  /**
6358
6522
  * In order to know what method of loading the player used for some content, we
@@ -6392,7 +6556,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6392
6556
  * @define {string} A version number taken from git at compile time.
6393
6557
  * @export
6394
6558
  */
6395
- shaka.Player.version = 'v4.0.0-uncompiled';
6559
+ shaka.Player.version = 'v4.1.0-uncompiled';
6396
6560
 
6397
6561
  // Initialize the deprecation system using the version string we just set
6398
6562
  // on the player.