shaka-player 4.0.2 → 4.1.1

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 (86) hide show
  1. package/AUTHORS +1 -0
  2. package/CHANGELOG.md +36 -16
  3. package/CONTRIBUTORS +2 -0
  4. package/build/test.py +1 -1
  5. package/demo/common/assets.js +1 -1
  6. package/demo/common/message_ids.js +1 -0
  7. package/demo/config.js +3 -1
  8. package/demo/locales/en.json +1 -0
  9. package/demo/locales/source.json +4 -0
  10. package/demo/main.js +3 -3
  11. package/dist/demo.compiled.debug.js +7 -7
  12. package/dist/demo.compiled.debug.map +2 -2
  13. package/dist/demo.compiled.js +7 -7
  14. package/dist/demo.compiled.map +2 -2
  15. package/dist/deps.js +2 -2
  16. package/dist/locales.js +30 -30
  17. package/dist/receiver.compiled.debug.js +3 -3
  18. package/dist/receiver.compiled.debug.map +2 -2
  19. package/dist/receiver.compiled.js +3 -3
  20. package/dist/receiver.compiled.map +2 -2
  21. package/dist/shaka-player.compiled.d.ts +14 -8
  22. package/dist/shaka-player.compiled.debug.d.ts +14 -8
  23. package/dist/shaka-player.compiled.debug.externs.js +13 -4
  24. package/dist/shaka-player.compiled.debug.js +296 -282
  25. package/dist/shaka-player.compiled.debug.map +4 -4
  26. package/dist/shaka-player.compiled.externs.js +13 -4
  27. package/dist/shaka-player.compiled.js +781 -773
  28. package/dist/shaka-player.compiled.map +4 -4
  29. package/dist/shaka-player.ui.d.ts +15 -9
  30. package/dist/shaka-player.ui.debug.d.ts +15 -9
  31. package/dist/shaka-player.ui.debug.externs.js +13 -4
  32. package/dist/shaka-player.ui.debug.js +345 -326
  33. package/dist/shaka-player.ui.debug.map +4 -4
  34. package/dist/shaka-player.ui.externs.js +13 -4
  35. package/dist/shaka-player.ui.js +955 -949
  36. package/dist/shaka-player.ui.map +4 -4
  37. package/docs/design/chromecast.md +3 -1
  38. package/docs/tutorials/drm-config.md +16 -0
  39. package/docs/tutorials/ui.md +26 -0
  40. package/externs/chromecast.js +9 -2
  41. package/externs/shaka/manifest.js +7 -0
  42. package/externs/shaka/player.js +25 -4
  43. package/externs/shaka/text.js +7 -0
  44. package/lib/cast/cast_proxy.js +19 -8
  45. package/lib/cast/cast_receiver.js +3 -3
  46. package/lib/cast/cast_sender.js +12 -2
  47. package/lib/hls/hls_parser.js +189 -69
  48. package/lib/media/drm_engine.js +17 -8
  49. package/lib/media/gap_jumping_controller.js +23 -1
  50. package/lib/media/playhead.js +47 -4
  51. package/lib/media/segment_index.js +26 -1
  52. package/lib/media/segment_reference.js +10 -3
  53. package/lib/media/stall_detector.js +19 -2
  54. package/lib/media/streaming_engine.js +9 -2
  55. package/lib/offline/manifest_converter.js +1 -0
  56. package/lib/player.js +175 -69
  57. package/lib/text/cue.js +6 -0
  58. package/lib/text/ui_text_displayer.js +1 -0
  59. package/lib/text/vtt_text_parser.js +4 -0
  60. package/lib/util/error.js +6 -0
  61. package/lib/util/fake_event.js +39 -0
  62. package/lib/util/player_configuration.js +2 -0
  63. package/lib/util/stats.js +28 -1
  64. package/lib/util/stream_utils.js +8 -0
  65. package/package.json +2 -2
  66. package/test/cast/cast_proxy_unit.js +3 -1
  67. package/test/cast/cast_sender_unit.js +21 -6
  68. package/test/demo/demo_unit.js +2 -1
  69. package/test/hls/hls_parser_unit.js +102 -8
  70. package/test/media/adaptation_set_unit.js +1 -0
  71. package/test/media/playhead_unit.js +114 -25
  72. package/test/media/stall_detector_unit.js +20 -2
  73. package/test/media/streaming_engine_integration.js +2 -1
  74. package/test/media/streaming_engine_unit.js +1 -0
  75. package/test/player_integration.js +3 -0
  76. package/test/player_unit.js +142 -1
  77. package/test/test/util/manifest_generator.js +2 -0
  78. package/test/test/util/simple_fakes.js +14 -0
  79. package/test/test/util/streaming_engine_util.js +1 -0
  80. package/test/text/vtt_text_parser_unit.js +4 -0
  81. package/test/ui/ui_unit.js +18 -5
  82. package/test/util/stream_utils_unit.js +108 -0
  83. package/ui/controls.js +12 -8
  84. package/ui/externs/ui.js +10 -1
  85. package/ui/statistics_button.js +10 -0
  86. package/ui/ui.js +12 -1
@@ -94,6 +94,25 @@ shaka.media.SegmentIndex = class {
94
94
  }
95
95
 
96
96
 
97
+ /**
98
+ * Return the earliest reference, or null if empty.
99
+ * @return {shaka.media.SegmentReference}
100
+ */
101
+ earliestReference() {
102
+ return this.references[0] || null;
103
+ }
104
+
105
+
106
+ /**
107
+ * Drop the first N references.
108
+ * Used in early HLS synchronization, and does not count as eviction.
109
+ * @param {number} n
110
+ */
111
+ dropFirstReferences(n) {
112
+ this.references.splice(0, n);
113
+ }
114
+
115
+
97
116
  /**
98
117
  * Finds the position of the segment for the given time, in seconds, relative
99
118
  * to the start of the presentation. Returns the position of the segment
@@ -166,7 +185,13 @@ shaka.media.SegmentIndex = class {
166
185
  for (const ref of this.references) {
167
186
  ref.startTime += offset;
168
187
  ref.endTime += offset;
169
- ref.timestampOffset += offset;
188
+ ref.trueEndTime += offset;
189
+
190
+ for (const partial of ref.partialReferences) {
191
+ partial.startTime += offset;
192
+ partial.endTime += offset;
193
+ partial.trueEndTime += offset;
194
+ }
170
195
  }
171
196
  }
172
197
  }
@@ -160,8 +160,9 @@ shaka.media.SegmentReference = class {
160
160
  * If not provided, the duration should be automatically calculated based on
161
161
  * the duration of the reference.
162
162
  * @param {?number=} syncTime
163
- * A time value, expressed in the same scale as the start and end time, which
164
- * is used to synchronize between streams.
163
+ * A time value, expressed in seconds since 1970, which is used to
164
+ * synchronize between streams. Both produced and consumed by the HLS
165
+ * parser. Other components should not need this value.
165
166
  * @param {shaka.media.SegmentReference.Status=} status
166
167
  * The segment status is used to indicate that a segment does not exist or is
167
168
  * not available.
@@ -221,7 +222,13 @@ shaka.media.SegmentReference = class {
221
222
  /** @type {?number} */
222
223
  this.tileDuration = tileDuration;
223
224
 
224
- /** @type {?number} */
225
+ /**
226
+ * A time value, expressed in seconds since 1970, which is used to
227
+ * synchronize between streams. Both produced and consumed by the HLS
228
+ * parser. Other components should not need this value.
229
+ *
230
+ * @type {?number}
231
+ */
225
232
  this.syncTime = syncTime;
226
233
 
227
234
  /** @type {shaka.media.SegmentReference.Status} */
@@ -9,6 +9,7 @@ goog.provide('shaka.media.StallDetector.Implementation');
9
9
  goog.provide('shaka.media.StallDetector.MediaElementImplementation');
10
10
 
11
11
  goog.require('shaka.media.TimeRangesUtils');
12
+ goog.require('shaka.util.FakeEvent');
12
13
  goog.require('shaka.util.IReleasable');
13
14
 
14
15
  /**
@@ -23,11 +24,14 @@ shaka.media.StallDetector = class {
23
24
  /**
24
25
  * @param {shaka.media.StallDetector.Implementation} implementation
25
26
  * @param {number} stallThresholdSeconds
27
+ * @param {function(!Event)} onEvent
28
+ * Called when an event is raised to be sent to the application.
26
29
  */
27
- constructor(implementation, stallThresholdSeconds) {
30
+ constructor(implementation, stallThresholdSeconds, onEvent) {
31
+ /** @private {?function(!Event)} */
32
+ this.onEvent_ = onEvent;
28
33
  /** @private {shaka.media.StallDetector.Implementation} */
29
34
  this.implementation_ = implementation;
30
-
31
35
  /** @private {boolean} */
32
36
  this.wasMakingProgress_ = implementation.shouldBeMakingProgress();
33
37
  /** @private {number} */
@@ -36,6 +40,8 @@ shaka.media.StallDetector = class {
36
40
  this.lastUpdateSeconds_ = implementation.getWallSeconds();
37
41
  /** @private {boolean} */
38
42
  this.didJump_ = false;
43
+ /** @private {number} */
44
+ this.stallsDetected_ = 0;
39
45
 
40
46
  /**
41
47
  * The amount of time in seconds that we must have the same value of
@@ -53,6 +59,7 @@ shaka.media.StallDetector = class {
53
59
  release() {
54
60
  // Drop external references to make things easier on the GC.
55
61
  this.implementation_ = null;
62
+ this.onEvent_ = null;
56
63
  this.onStall_ = () => {};
57
64
  }
58
65
 
@@ -66,6 +73,13 @@ shaka.media.StallDetector = class {
66
73
  this.onStall_ = doThis;
67
74
  }
68
75
 
76
+ /**
77
+ * Returns the number of playback stalls detected.
78
+ */
79
+ getStallsDetected() {
80
+ return this.stallsDetected_;
81
+ }
82
+
69
83
  /**
70
84
  * Have the detector update itself and fire the "on stall" callback if a stall
71
85
  * was detected.
@@ -100,6 +114,9 @@ shaka.media.StallDetector = class {
100
114
  // If the onStall_ method updated the current time, update our stored
101
115
  // value so we don't think that was an update.
102
116
  this.value_ = impl.getPresentationSeconds();
117
+ this.stallsDetected_++;
118
+ this.onEvent_(new shaka.util.FakeEvent(
119
+ shaka.util.FakeEvent.EventName.StallDetected));
103
120
  }
104
121
 
105
122
  return triggerCallback;
@@ -1215,6 +1215,13 @@ shaka.media.StreamingEngine = class {
1215
1215
  mediaState.performingUpdate = true;
1216
1216
 
1217
1217
  try {
1218
+ if (reference.getStatus() ==
1219
+ shaka.media.SegmentReference.Status.MISSING) {
1220
+ throw new shaka.util.Error(
1221
+ shaka.util.Error.Severity.RECOVERABLE,
1222
+ shaka.util.Error.Category.NETWORK,
1223
+ shaka.util.Error.Code.SEGMENT_MISSING);
1224
+ }
1218
1225
  await this.initSourceBuffer_(mediaState, reference);
1219
1226
  this.destroyer_.ensureNotDestroyed();
1220
1227
  if (this.fatalError_) {
@@ -1602,7 +1609,7 @@ shaka.media.StreamingEngine = class {
1602
1609
  await this.playerInterface_.mediaSourceEngine.appendBuffer(
1603
1610
  mediaState.type,
1604
1611
  segment,
1605
- reference.syncTime == null ? reference.startTime : reference.syncTime,
1612
+ reference.startTime,
1606
1613
  reference.endTime,
1607
1614
  hasClosedCaptions,
1608
1615
  seeked);
@@ -1694,7 +1701,7 @@ shaka.media.StreamingEngine = class {
1694
1701
  };
1695
1702
 
1696
1703
  // Dispatch an event to notify the application about the emsg box.
1697
- const eventName = shaka.Player.EventName.Emsg;
1704
+ const eventName = shaka.util.FakeEvent.EventName.Emsg;
1698
1705
  const data = (new Map()).set('detail', emsg);
1699
1706
  const event = new shaka.util.FakeEvent(eventName, data);
1700
1707
  this.playerInterface_.onEvent(event);
@@ -302,6 +302,7 @@ shaka.offline.ManifestConverter = class {
302
302
  return {
303
303
  id: id,
304
304
  language: '',
305
+ disabledUntilTime: 0,
305
306
  primary: false,
306
307
  audio: null,
307
308
  video: null,
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
  *
@@ -652,7 +672,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
652
672
  },
653
673
  enterNode: (node, has, wants) => {
654
674
  this.dispatchEvent(this.makeEvent_(
655
- /* name= */ shaka.Player.EventName.OnStateChange,
675
+ /* name= */ shaka.util.FakeEvent.EventName.OnStateChange,
656
676
  /* data= */ (new Map()).set('state', node.name)));
657
677
 
658
678
  const action = actions.get(node);
@@ -683,7 +703,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
683
703
  },
684
704
  onIdle: (node) => {
685
705
  this.dispatchEvent(this.makeEvent_(
686
- /* name= */ shaka.Player.EventName.OnStateIdle,
706
+ /* name= */ shaka.util.FakeEvent.EventName.OnStateIdle,
687
707
  /* data= */ (new Map()).set('state', node.name)));
688
708
  },
689
709
  };
@@ -694,6 +714,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
694
714
  shaka.Player.createEmptyPayload_(),
695
715
  walkerImplementation);
696
716
 
717
+ /** @private {shaka.util.Timer} */
718
+ this.checkVariantsTimer_ =
719
+ new shaka.util.Timer(() => this.checkVariants_());
720
+
697
721
  // Even though |attach| will start in later interpreter cycles, it should be
698
722
  // the LAST thing we do in the constructor because conceptually it relies on
699
723
  // player having been initialized.
@@ -703,7 +727,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
703
727
  }
704
728
 
705
729
  /**
706
- * @param {!shaka.Player.EventName} name
730
+ * @param {!shaka.util.FakeEvent.EventName} name
707
731
  * @param {Map.<string, Object>=} data
708
732
  * @return {!shaka.util.FakeEvent}
709
733
  * @private
@@ -1129,7 +1153,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1129
1153
 
1130
1154
  // We dispatch the loading event when someone calls |load| because we want
1131
1155
  // to surface the user intent.
1132
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Loading));
1156
+ this.dispatchEvent(this.makeEvent_(shaka.util.FakeEvent.EventName.Loading));
1133
1157
 
1134
1158
  // Right away we know what the asset uri and start-of-load time are. We will
1135
1159
  // fill-in the rest of the information later.
@@ -1193,7 +1217,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1193
1217
  events.onEnd = () => {
1194
1218
  resolve();
1195
1219
  // We dispatch the loaded event when the load promise is resolved
1196
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Loaded));
1220
+ this.dispatchEvent(
1221
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Loaded));
1197
1222
  };
1198
1223
  events.onCancel = () => reject(this.createAbortLoadError_());
1199
1224
  events.onError = (e) => reject(e);
@@ -1398,7 +1423,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1398
1423
  await Promise.all(cleanupTasks);
1399
1424
 
1400
1425
  // Dispatch the unloading event.
1401
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Unloading));
1426
+ this.dispatchEvent(
1427
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Unloading));
1402
1428
 
1403
1429
  // Remove everything that has to do with loading content from our payload
1404
1430
  // since we are releasing everything that depended on it.
@@ -1412,6 +1438,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1412
1438
  this.loadEventManager_.removeAll();
1413
1439
  }
1414
1440
 
1441
+ // Stop the variant checker timer
1442
+ this.checkVariantsTimer_.stop();
1443
+
1415
1444
  // Some observers use some playback components, shutting down the observers
1416
1445
  // first ensures that they don't try to use the playback components
1417
1446
  // mid-destroy.
@@ -1699,7 +1728,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1699
1728
  this.regionTimeline_.addEventListener('regionadd', (event) => {
1700
1729
  /** @type {shaka.extern.TimelineRegionInfo} */
1701
1730
  const region = event['region'];
1702
- this.onRegionEvent_(shaka.Player.EventName.TimelineRegionAdded, region);
1731
+ this.onRegionEvent_(
1732
+ shaka.util.FakeEvent.EventName.TimelineRegionAdded, region);
1703
1733
 
1704
1734
  if (this.adManager_) {
1705
1735
  this.adManager_.onDashTimedMetadata(region);
@@ -1754,7 +1784,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1754
1784
 
1755
1785
  // This event is fired after the manifest is parsed, but before any
1756
1786
  // filtering takes place.
1757
- const event = this.makeEvent_(shaka.Player.EventName.ManifestParsed);
1787
+ const event =
1788
+ this.makeEvent_(shaka.util.FakeEvent.EventName.ManifestParsed);
1758
1789
  this.dispatchEvent(event);
1759
1790
 
1760
1791
  // We require all manifests to have at least one variant.
@@ -1828,7 +1859,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1828
1859
  },
1829
1860
  onEvent: (e) => {
1830
1861
  this.dispatchEvent(e);
1831
- if (e.type == shaka.Player.EventName.DrmSessionUpdate && firstEvent) {
1862
+ if (e.type == shaka.util.FakeEvent.EventName.DrmSessionUpdate &&
1863
+ firstEvent) {
1832
1864
  firstEvent = false;
1833
1865
  const now = Date.now() / 1000;
1834
1866
  const delta = now - startTime;
@@ -1993,7 +2025,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1993
2025
  // The event must be fired after we filter by restrictions but before the
1994
2026
  // active stream is picked to allow those listening for the "streaming"
1995
2027
  // event to make changes before streaming starts.
1996
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Streaming));
2028
+ this.dispatchEvent(
2029
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Streaming));
1997
2030
 
1998
2031
  // Pick the initial streams to play.
1999
2032
  // however, we would skip switch to initial variant
@@ -2121,7 +2154,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2121
2154
  },
2122
2155
  onEvent: (e) => {
2123
2156
  this.dispatchEvent(e);
2124
- if (e.type == shaka.Player.EventName.DrmSessionUpdate && firstEvent) {
2157
+ if (e.type == shaka.util.FakeEvent.EventName.DrmSessionUpdate &&
2158
+ firstEvent) {
2125
2159
  firstEvent = false;
2126
2160
  const now = Date.now() / 1000;
2127
2161
  const delta = now - startTime;
@@ -2154,6 +2188,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2154
2188
  const variant = {
2155
2189
  id: 0,
2156
2190
  language: 'und',
2191
+ disabledUntilTime: 0,
2157
2192
  primary: false,
2158
2193
  audio: null,
2159
2194
  video: {
@@ -2346,7 +2381,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2346
2381
  // The event doesn't mean as much for src= playback, since we don't control
2347
2382
  // streaming. But we should fire it in this path anyway since some
2348
2383
  // applications may be expecting it as a life-cycle event.
2349
- this.dispatchEvent(this.makeEvent_(shaka.Player.EventName.Streaming));
2384
+ this.dispatchEvent(
2385
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Streaming));
2350
2386
 
2351
2387
  // The "load" Promise is resolved when we have loaded the metadata. If we
2352
2388
  // wait for the full data, that won't happen on Safari until the play button
@@ -2558,7 +2594,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2558
2594
  dispatchMetadataEvent_(startTime, endTime, metadataType, payload) {
2559
2595
  goog.asserts.assert(!endTime || startTime <= endTime,
2560
2596
  'Metadata start time should be less or equal to the end time!');
2561
- const eventName = shaka.Player.EventName.Metadata;
2597
+ const eventName = shaka.util.FakeEvent.EventName.Metadata;
2562
2598
  const data = new Map()
2563
2599
  .set('startTime', startTime)
2564
2600
  .set('endTime', endTime)
@@ -2655,7 +2691,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2655
2691
  /** @type {shaka.net.NetworkingEngine.OnHeadersReceived} */
2656
2692
  const onHeadersReceived_ = (headers, request, requestType) => {
2657
2693
  // Release a 'downloadheadersreceived' event.
2658
- const name = shaka.Player.EventName.DownloadHeadersReceived;
2694
+ const name = shaka.util.FakeEvent.EventName.DownloadHeadersReceived;
2659
2695
  const data = new Map()
2660
2696
  .set('headers', headers)
2661
2697
  .set('request', request)
@@ -2665,7 +2701,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2665
2701
  /** @type {shaka.net.NetworkingEngine.OnDownloadFailed} */
2666
2702
  const onDownloadFailed_ = (request, error, httpResponseCode, aborted) => {
2667
2703
  // Release a 'downloadfailed' event.
2668
- const name = shaka.Player.EventName.DownloadFailed;
2704
+ const name = shaka.util.FakeEvent.EventName.DownloadFailed;
2669
2705
  const data = new Map()
2670
2706
  .set('request', request)
2671
2707
  .set('error', error)
@@ -2693,7 +2729,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2693
2729
  this.manifest_,
2694
2730
  this.config_.streaming,
2695
2731
  startTime,
2696
- () => this.onSeek_());
2732
+ () => this.onSeek_(),
2733
+ (event) => this.dispatchEvent(event));
2697
2734
  }
2698
2735
 
2699
2736
  /**
@@ -2715,13 +2752,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2715
2752
  regionObserver.addEventListener('enter', (event) => {
2716
2753
  /** @type {shaka.extern.TimelineRegionInfo} */
2717
2754
  const region = event['region'];
2718
- this.onRegionEvent_(shaka.Player.EventName.TimelineRegionEnter, region);
2755
+ this.onRegionEvent_(
2756
+ shaka.util.FakeEvent.EventName.TimelineRegionEnter, region);
2719
2757
  });
2720
2758
 
2721
2759
  regionObserver.addEventListener('exit', (event) => {
2722
2760
  /** @type {shaka.extern.TimelineRegionInfo} */
2723
2761
  const region = event['region'];
2724
- this.onRegionEvent_(shaka.Player.EventName.TimelineRegionExit, region);
2762
+ this.onRegionEvent_(
2763
+ shaka.util.FakeEvent.EventName.TimelineRegionExit, region);
2725
2764
  });
2726
2765
 
2727
2766
  regionObserver.addEventListener('skip', (event) => {
@@ -2732,8 +2771,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2732
2771
  // If we are seeking, we don't want to surface the enter/exit events since
2733
2772
  // they didn't play through them.
2734
2773
  if (!seeking) {
2735
- this.onRegionEvent_(shaka.Player.EventName.TimelineRegionEnter, region);
2736
- 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);
2737
2778
  }
2738
2779
  });
2739
2780
 
@@ -4089,8 +4130,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4089
4130
  for (const chaptersTrack of chaptersTracksWithLanguage) {
4090
4131
  if (chaptersTrack && chaptersTrack.cues) {
4091
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
+ }
4092
4137
  /** @type {shaka.extern.Chapter} */
4093
4138
  const chapter = {
4139
+ id: id,
4094
4140
  title: cue.text,
4095
4141
  startTime: cue.startTime,
4096
4142
  endTime: cue.endTime,
@@ -4355,6 +4401,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4355
4401
  this.stats_.setCompletionPercent(Math.round(100 * completionRatio));
4356
4402
  }
4357
4403
 
4404
+ if (this.playhead_) {
4405
+ this.stats_.setGapsJumped(this.playhead_.getGapsJumped());
4406
+ this.stats_.setStallsDetected(this.playhead_.getStallsDetected());
4407
+ }
4408
+
4358
4409
  if (element.getVideoPlaybackQuality) {
4359
4410
  const info = element.getVideoPlaybackQuality();
4360
4411
 
@@ -5118,7 +5169,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5118
5169
 
5119
5170
  // Surface the buffering event so that the app knows if/when we are
5120
5171
  // buffering.
5121
- const eventName = shaka.Player.EventName.Buffering;
5172
+ const eventName = shaka.util.FakeEvent.EventName.Buffering;
5122
5173
  const data = (new Map()).set('buffering', isBuffering);
5123
5174
  this.dispatchEvent(this.makeEvent_(eventName, data));
5124
5175
  }
@@ -5153,7 +5204,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5153
5204
  this.playRateController_.set(newRate);
5154
5205
  }
5155
5206
 
5156
- const event = this.makeEvent_(shaka.Player.EventName.RateChange);
5207
+ const event = this.makeEvent_(shaka.util.FakeEvent.EventName.RateChange);
5157
5208
  this.dispatchEvent(event);
5158
5209
  }
5159
5210
 
@@ -5253,6 +5304,21 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5253
5304
  }
5254
5305
  }
5255
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
+
5256
5322
  /**
5257
5323
  * Choose a text stream from all possible text streams while taking into
5258
5324
  * account user preference.
@@ -5485,7 +5551,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5485
5551
  const data = new Map()
5486
5552
  .set('oldTrack', from)
5487
5553
  .set('newTrack', to);
5488
- const event = this.makeEvent_(shaka.Player.EventName.Adaptation, data);
5554
+ const event =
5555
+ this.makeEvent_(shaka.util.FakeEvent.EventName.Adaptation, data);
5489
5556
  this.delayDispatchEvent_(event);
5490
5557
  }
5491
5558
 
@@ -5496,7 +5563,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5496
5563
  onTracksChanged_() {
5497
5564
  // Delay the 'trackschanged' event so StreamingEngine has time to absorb the
5498
5565
  // changes before the user tries to query it.
5499
- const event = this.makeEvent_(shaka.Player.EventName.TracksChanged);
5566
+ const event = this.makeEvent_(shaka.util.FakeEvent.EventName.TracksChanged);
5500
5567
  this.delayDispatchEvent_(event);
5501
5568
  }
5502
5569
 
@@ -5512,7 +5579,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5512
5579
  const data = new Map()
5513
5580
  .set('oldTrack', from)
5514
5581
  .set('newTrack', to);
5515
- const event = this.makeEvent_(shaka.Player.EventName.VariantChanged, data);
5582
+ const event =
5583
+ this.makeEvent_(shaka.util.FakeEvent.EventName.VariantChanged, data);
5516
5584
  this.delayDispatchEvent_(event);
5517
5585
  }
5518
5586
 
@@ -5523,13 +5591,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5523
5591
  onTextChanged_() {
5524
5592
  // Delay the 'textchanged' event so StreamingEngine time to absorb the
5525
5593
  // changes before the user tries to query it.
5526
- const event = this.makeEvent_(shaka.Player.EventName.TextChanged);
5594
+ const event = this.makeEvent_(shaka.util.FakeEvent.EventName.TextChanged);
5527
5595
  this.delayDispatchEvent_(event);
5528
5596
  }
5529
5597
 
5530
5598
  /** @private */
5531
5599
  onTextTrackVisibility_() {
5532
- const event = this.makeEvent_(shaka.Player.EventName.TextTrackVisibility);
5600
+ const event =
5601
+ this.makeEvent_(shaka.util.FakeEvent.EventName.TextTrackVisibility);
5533
5602
  this.delayDispatchEvent_(event);
5534
5603
  }
5535
5604
 
@@ -5537,7 +5606,73 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5537
5606
  onAbrStatusChanged_() {
5538
5607
  const data = (new Map()).set('newStatus', this.config_.abr.enabled);
5539
5608
  this.delayDispatchEvent_(this.makeEvent_(
5540
- 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;
5541
5676
  }
5542
5677
 
5543
5678
  /**
@@ -5553,7 +5688,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5553
5688
  return;
5554
5689
  }
5555
5690
 
5556
- 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;
5557
5698
  const event = this.makeEvent_(eventName, (new Map()).set('detail', error));
5558
5699
  this.dispatchEvent(event);
5559
5700
  if (event.defaultPrevented) {
@@ -5567,7 +5708,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5567
5708
  * copy here because this is the transition point between the player and the
5568
5709
  * app.
5569
5710
  *
5570
- * @param {!shaka.Player.EventName} eventName
5711
+ * @param {!shaka.util.FakeEvent.EventName} eventName
5571
5712
  * @param {shaka.extern.TimelineRegionInfo} region
5572
5713
  *
5573
5714
  * @private
@@ -5616,7 +5757,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5616
5757
  .set('position', position);
5617
5758
 
5618
5759
  this.dispatchEvent(this.makeEvent_(
5619
- shaka.Player.EventName.MediaQualityChanged, data));
5760
+ shaka.util.FakeEvent.EventName.MediaQualityChanged, data));
5620
5761
  }
5621
5762
 
5622
5763
  /**
@@ -5762,7 +5903,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5762
5903
  this.parser_.onExpirationUpdated(keyId, expiration);
5763
5904
  }
5764
5905
 
5765
- const event = this.makeEvent_(shaka.Player.EventName.ExpirationUpdated);
5906
+ const event =
5907
+ this.makeEvent_(shaka.util.FakeEvent.EventName.ExpirationUpdated);
5766
5908
  this.dispatchEvent(event);
5767
5909
  }
5768
5910
 
@@ -6375,42 +6517,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6375
6517
  }
6376
6518
  };
6377
6519
 
6378
- /**
6379
- * An internal enum that contains the string values of all of the player events.
6380
- * This exists primarily to act as an implicit list of events, for tests.
6381
- *
6382
- * @enum {string}
6383
- */
6384
- shaka.Player.EventName = {
6385
- AbrStatusChanged: 'abrstatuschanged',
6386
- Adaptation: 'adaptation',
6387
- Buffering: 'buffering',
6388
- DownloadFailed: 'downloadfailed',
6389
- DownloadHeadersReceived: 'downloadheadersreceived',
6390
- DrmSessionUpdate: 'drmsessionupdate',
6391
- Emsg: 'emsg',
6392
- Error: 'error',
6393
- ExpirationUpdated: 'expirationupdated',
6394
- Loaded: 'loaded',
6395
- Loading: 'loading',
6396
- ManifestParsed: 'manifestparsed',
6397
- MediaQualityChanged: 'mediaqualitychanged',
6398
- Metadata: 'metadata',
6399
- OnStateChange: 'onstatechange',
6400
- OnStateIdle: 'onstateidle',
6401
- RateChange: 'ratechange',
6402
- SessionDataEvent: 'sessiondata',
6403
- Streaming: 'streaming',
6404
- TextChanged: 'textchanged',
6405
- TextTrackVisibility: 'texttrackvisibility',
6406
- TimelineRegionAdded: 'timelineregionadded',
6407
- TimelineRegionEnter: 'timelineregionenter',
6408
- TimelineRegionExit: 'timelineregionexit',
6409
- TracksChanged: 'trackschanged',
6410
- Unloading: 'unloading',
6411
- VariantChanged: 'variantchanged',
6412
- };
6413
-
6414
6520
 
6415
6521
  /**
6416
6522
  * In order to know what method of loading the player used for some content, we
@@ -6450,7 +6556,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6450
6556
  * @define {string} A version number taken from git at compile time.
6451
6557
  * @export
6452
6558
  */
6453
- shaka.Player.version = 'v4.0.2-uncompiled';
6559
+ shaka.Player.version = 'v4.1.1-uncompiled';
6454
6560
 
6455
6561
  // Initialize the deprecation system using the version string we just set
6456
6562
  // on the player.