shaka-player 4.7.12 → 4.8.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 (120) hide show
  1. package/AUTHORS +1 -0
  2. package/CONTRIBUTORS +1 -0
  3. package/LICENSE +54 -0
  4. package/README.md +22 -1
  5. package/dist/controls.css +1 -1
  6. package/dist/controls.css.map +1 -1
  7. package/dist/deps.js +36 -25
  8. package/dist/locales.js +40 -30
  9. package/dist/shaka-player.compiled.d.ts +196 -46
  10. package/dist/shaka-player.compiled.debug.d.ts +196 -46
  11. package/dist/shaka-player.compiled.debug.externs.js +474 -337
  12. package/dist/shaka-player.compiled.debug.js +992 -871
  13. package/dist/shaka-player.compiled.debug.map +4 -4
  14. package/dist/shaka-player.compiled.externs.js +474 -337
  15. package/dist/shaka-player.compiled.js +1248 -1174
  16. package/dist/shaka-player.compiled.map +4 -4
  17. package/dist/shaka-player.ui.d.ts +259 -49
  18. package/dist/shaka-player.ui.debug.d.ts +259 -49
  19. package/dist/shaka-player.ui.debug.externs.js +591 -376
  20. package/dist/shaka-player.ui.debug.js +1208 -1003
  21. package/dist/shaka-player.ui.debug.map +4 -4
  22. package/dist/shaka-player.ui.externs.js +591 -376
  23. package/dist/shaka-player.ui.js +1533 -1408
  24. package/dist/shaka-player.ui.map +4 -4
  25. package/externs/device_sensor_event.js +18 -0
  26. package/externs/hisense.js +16 -0
  27. package/externs/playstation.js +31 -0
  28. package/externs/shaka/abr_manager.js +3 -1
  29. package/externs/shaka/ads.js +5 -0
  30. package/externs/shaka/codecs.js +13 -0
  31. package/externs/shaka/manifest.js +69 -7
  32. package/externs/shaka/net.js +13 -1
  33. package/externs/shaka/offline.js +7 -1
  34. package/externs/shaka/player.js +184 -13
  35. package/externs/shaka/text.js +19 -0
  36. package/externs/tizen.js +41 -0
  37. package/externs/webos.js +19 -0
  38. package/externs/xbox.js +43 -0
  39. package/lib/abr/simple_abr_manager.js +9 -1
  40. package/lib/ads/ad_manager.js +19 -2
  41. package/lib/ads/client_side_ad_manager.js +5 -3
  42. package/lib/ads/media_tailor_ad_manager.js +9 -0
  43. package/lib/cast/cast_receiver.js +0 -14
  44. package/lib/cast/cast_utils.js +1 -0
  45. package/lib/cea/mp4_cea_parser.js +6 -1
  46. package/lib/dash/content_protection.js +97 -53
  47. package/lib/dash/dash_parser.js +240 -179
  48. package/lib/dash/mpd_utils.js +72 -65
  49. package/lib/dash/segment_base.js +35 -22
  50. package/lib/dash/segment_list.js +39 -31
  51. package/lib/dash/segment_template.js +47 -23
  52. package/lib/hls/hls_parser.js +376 -165
  53. package/lib/media/buffering_observer.js +25 -1
  54. package/lib/media/drm_engine.js +91 -71
  55. package/lib/media/manifest_filterer.js +220 -0
  56. package/lib/media/media_source_engine.js +108 -29
  57. package/lib/media/playhead.js +3 -3
  58. package/lib/media/preload_manager.js +827 -0
  59. package/lib/media/presentation_timeline.js +11 -0
  60. package/lib/media/segment_index.js +60 -49
  61. package/lib/media/segment_prefetch.js +140 -43
  62. package/lib/media/segment_reference.js +31 -8
  63. package/lib/media/segment_utils.js +208 -25
  64. package/lib/media/streaming_engine.js +321 -77
  65. package/lib/mss/content_protection.js +31 -25
  66. package/lib/mss/mss_parser.js +78 -55
  67. package/lib/net/networking_engine.js +49 -4
  68. package/lib/offline/indexeddb/v1_storage_cell.js +2 -0
  69. package/lib/offline/indexeddb/v2_storage_cell.js +2 -0
  70. package/lib/offline/manifest_converter.js +10 -3
  71. package/lib/offline/storage.js +5 -3
  72. package/lib/player.js +896 -674
  73. package/lib/polyfill/media_capabilities.js +16 -1
  74. package/lib/text/simple_text_displayer.js +9 -0
  75. package/lib/text/stub_text_displayer.js +7 -0
  76. package/lib/text/text_engine.js +16 -3
  77. package/lib/text/ttml_text_parser.js +116 -114
  78. package/lib/text/ui_text_displayer.js +28 -11
  79. package/lib/text/vtt_text_parser.js +54 -115
  80. package/lib/transmuxer/opus.js +91 -0
  81. package/lib/transmuxer/ts_transmuxer.js +115 -0
  82. package/lib/util/cmcd_manager.js +152 -3
  83. package/lib/util/dom_utils.js +11 -0
  84. package/lib/util/error.js +5 -0
  85. package/lib/util/fake_event.js +2 -0
  86. package/lib/util/manifest_parser_utils.js +97 -3
  87. package/lib/util/mime_utils.js +10 -5
  88. package/lib/util/mp4_box_parsers.js +346 -17
  89. package/lib/util/mp4_generator.js +23 -0
  90. package/lib/util/periods.js +24 -37
  91. package/lib/util/platform.js +161 -9
  92. package/lib/util/player_configuration.js +61 -3
  93. package/lib/util/stats.js +16 -0
  94. package/lib/util/stream_utils.js +350 -277
  95. package/lib/util/string_utils.js +36 -0
  96. package/lib/util/tXml.js +814 -0
  97. package/lib/util/ts_parser.js +50 -0
  98. package/lib/util/xml_utils.js +10 -338
  99. package/package.json +2 -2
  100. package/shaka-player.uncompiled.js +8 -0
  101. package/ui/controls.js +187 -5
  102. package/ui/enums.js +2 -0
  103. package/ui/externs/ui.js +30 -3
  104. package/ui/gl_matrix/matrix_4x4.js +568 -0
  105. package/ui/gl_matrix/matrix_quaternion.js +45 -0
  106. package/ui/less/containers.less +11 -0
  107. package/ui/locales/en-GB.json +2 -0
  108. package/ui/locales/en.json +2 -0
  109. package/ui/locales/es-419.json +2 -0
  110. package/ui/locales/es.json +2 -0
  111. package/ui/locales/source.json +8 -0
  112. package/ui/recenter_vr.js +126 -0
  113. package/ui/resolution_selection.js +25 -5
  114. package/ui/statistics_button.js +12 -0
  115. package/ui/text_selection.js +1 -1
  116. package/ui/toggle_stereoscopic.js +127 -0
  117. package/ui/ui.js +74 -38
  118. package/ui/vr_manager.js +557 -0
  119. package/ui/vr_utils.js +232 -0
  120. package/ui/vr_webgl.js +697 -0
package/lib/player.js CHANGED
@@ -14,6 +14,7 @@ goog.require('shaka.media.AdaptationSetCriteria');
14
14
  goog.require('shaka.media.BufferingObserver');
15
15
  goog.require('shaka.media.DrmEngine');
16
16
  goog.require('shaka.media.ExampleBasedCriteria');
17
+ goog.require('shaka.media.ManifestFilterer');
17
18
  goog.require('shaka.media.ManifestParser');
18
19
  goog.require('shaka.media.MediaSourceEngine');
19
20
  goog.require('shaka.media.MediaSourcePlayhead');
@@ -22,10 +23,12 @@ goog.require('shaka.media.PlayRateController');
22
23
  goog.require('shaka.media.Playhead');
23
24
  goog.require('shaka.media.PlayheadObserverManager');
24
25
  goog.require('shaka.media.PreferenceBasedCriteria');
26
+ goog.require('shaka.media.PreloadManager');
25
27
  goog.require('shaka.media.QualityObserver');
26
28
  goog.require('shaka.media.RegionObserver');
27
29
  goog.require('shaka.media.RegionTimeline');
28
30
  goog.require('shaka.media.SegmentIndex');
31
+ goog.require('shaka.media.SegmentPrefetch');
29
32
  goog.require('shaka.media.SegmentReference');
30
33
  goog.require('shaka.media.SrcEqualsPlayhead');
31
34
  goog.require('shaka.media.StreamingEngine');
@@ -494,6 +497,29 @@ goog.requireType('shaka.media.PresentationTimeline');
494
497
  */
495
498
 
496
499
 
500
+ /**
501
+ * @event shaka.Player.SpatialVideoInfoEvent
502
+ * @description Fired when the video has spatial video info. If a previous
503
+ * event was fired, this include the new info.
504
+ * @property {string} type
505
+ * 'spatialvideoinfo'
506
+ * @property {shaka.extern.SpatialVideoInfo} detail
507
+ * An object which contains the content of the emsg box.
508
+ * @exportDoc
509
+ */
510
+
511
+
512
+ /**
513
+ * @event shaka.Player.NoSpatialVideoInfoEvent
514
+ * @description Fired when the video no longer has spatial video information.
515
+ * For it to be fired, the shaka.Player.SpatialVideoInfoEvent event must
516
+ * have been previously fired.
517
+ * @property {string} type
518
+ * 'nospatialvideoinfo'
519
+ * @exportDoc
520
+ */
521
+
522
+
497
523
  /**
498
524
  * @summary The main player object for Shaka Player.
499
525
  *
@@ -635,6 +661,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
635
661
  /** @private {?string} */
636
662
  this.assetUri_ = null;
637
663
 
664
+ /** @private {?string} */
665
+ this.mimeType_ = null;
666
+
638
667
  /** @private {?number} */
639
668
  this.startTime_ = null;
640
669
 
@@ -676,6 +705,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
676
705
  /** @private {{width: number, height: number}} */
677
706
  this.maxHwRes_ = {width: Infinity, height: Infinity};
678
707
 
708
+ /** @private {!shaka.media.ManifestFilterer} */
709
+ this.manifestFilterer_ = new shaka.media.ManifestFilterer(
710
+ this.config_, this.maxHwRes_, null);
711
+
679
712
  /** @private {shaka.util.Stats} */
680
713
  this.stats_ = null;
681
714
 
@@ -814,7 +847,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
814
847
  * @return {!shaka.util.FakeEvent}
815
848
  * @private
816
849
  */
817
- makeEvent_(name, data) {
850
+ static makeEvent_(name, data) {
818
851
  return new shaka.util.FakeEvent(name, data);
819
852
  }
820
853
 
@@ -997,7 +1030,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
997
1030
  * @private
998
1031
  */
999
1032
  makeStateChangeEvent_(nodeName) {
1000
- this.dispatchEvent(this.makeEvent_(
1033
+ this.dispatchEvent(shaka.Player.makeEvent_(
1001
1034
  /* name= */ shaka.util.FakeEvent.EventName.OnStateChange,
1002
1035
  /* data= */ (new Map()).set('state', nodeName)));
1003
1036
  }
@@ -1018,6 +1051,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1018
1051
  throw this.createAbortLoadError_();
1019
1052
  }
1020
1053
 
1054
+ const noop = this.video_ && this.video_ == mediaElement;
1055
+
1021
1056
  if (this.video_ && this.video_ != mediaElement) {
1022
1057
  await this.detach();
1023
1058
  }
@@ -1027,11 +1062,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1027
1062
  }
1028
1063
 
1029
1064
  try {
1030
- this.makeStateChangeEvent_('attach');
1065
+ if (!noop) {
1066
+ this.makeStateChangeEvent_('attach');
1031
1067
 
1032
- const onError = (error) => this.onVideoError_(error);
1033
- this.attachEventManager_.listen(mediaElement, 'error', onError);
1034
- this.video_ = mediaElement;
1068
+ const onError = (error) => this.onVideoError_(error);
1069
+ this.attachEventManager_.listen(mediaElement, 'error', onError);
1070
+ this.video_ = mediaElement;
1071
+ }
1035
1072
 
1036
1073
  // Only initialize media source if the platform supports it.
1037
1074
  if (initializeMediaSource &&
@@ -1163,7 +1200,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1163
1200
 
1164
1201
  // Dispatch the unloading event.
1165
1202
  this.dispatchEvent(
1166
- this.makeEvent_(shaka.util.FakeEvent.EventName.Unloading));
1203
+ shaka.Player.makeEvent_(shaka.util.FakeEvent.EventName.Unloading));
1167
1204
 
1168
1205
  // Release the region timeline, which is created when parsing the
1169
1206
  // manifest.
@@ -1270,6 +1307,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1270
1307
  }
1271
1308
 
1272
1309
  this.assetUri_ = null;
1310
+ this.mimeType_ = null;
1273
1311
  this.bufferObserver_ = null;
1274
1312
 
1275
1313
  if (this.manifest_) {
@@ -1324,7 +1362,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1324
1362
  * Loads a new stream.
1325
1363
  * If another stream was already playing, first unloads that stream.
1326
1364
  *
1327
- * @param {string} assetUri
1365
+ * @param {string|shaka.media.PreloadManager} assetUriOrPreloader
1328
1366
  * @param {?number=} startTime
1329
1367
  * When <code>startTime</code> is <code>null</code> or
1330
1368
  * <code>undefined</code>, playback will start at the default start time (0
@@ -1333,12 +1371,22 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1333
1371
  * @return {!Promise}
1334
1372
  * @export
1335
1373
  */
1336
- async load(assetUri, startTime = null, mimeType) {
1374
+ async load(assetUriOrPreloader, startTime = null, mimeType) {
1337
1375
  // Do not allow the player to be used after |destroy| is called.
1338
1376
  if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
1339
1377
  throw this.createAbortLoadError_();
1340
1378
  }
1341
1379
 
1380
+ /** @type {?shaka.media.PreloadManager} */
1381
+ let preloadManager = null;
1382
+ let assetUri = '';
1383
+ if (assetUriOrPreloader instanceof shaka.media.PreloadManager) {
1384
+ preloadManager = assetUriOrPreloader;
1385
+ assetUri = preloadManager.getAssetUri() || '';
1386
+ } else {
1387
+ assetUri = assetUriOrPreloader || '';
1388
+ }
1389
+
1342
1390
  // Quickly acquire the mutex, so this will wait for other top-level
1343
1391
  // operations.
1344
1392
  await this.mutex_.acquire('load');
@@ -1358,8 +1406,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1358
1406
  // Add a mechanism to detect if the load process has been interrupted by a
1359
1407
  // call to another top-level operation (unload, load, etc).
1360
1408
  const operationId = ++this.operationId_;
1361
- const detectInterruption = () => {
1409
+ const detectInterruption = async () => {
1362
1410
  if (this.operationId_ != operationId) {
1411
+ if (preloadManager) {
1412
+ await preloadManager.destroy();
1413
+ }
1363
1414
  throw this.createAbortLoadError_();
1364
1415
  }
1365
1416
  };
@@ -1375,37 +1426,75 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1375
1426
  const mutexWrapOperation = async (operation, mutexIdentifier) => {
1376
1427
  try {
1377
1428
  await this.mutex_.acquire(mutexIdentifier);
1378
- detectInterruption();
1429
+ await detectInterruption();
1379
1430
  await operation();
1380
- detectInterruption();
1431
+ await detectInterruption();
1432
+ if (preloadManager && this.config_) {
1433
+ preloadManager.reconfigure(this.config_);
1434
+ }
1381
1435
  } finally {
1382
1436
  this.mutex_.release();
1383
1437
  }
1384
1438
  };
1385
1439
 
1386
1440
  try {
1441
+ if (startTime == null && preloadManager) {
1442
+ startTime = preloadManager.getStartTime();
1443
+ }
1387
1444
  this.startTime_ = startTime;
1388
1445
  this.fullyLoaded_ = false;
1389
1446
 
1390
1447
  // We dispatch the loading event when someone calls |load| because we want
1391
1448
  // to surface the user intent.
1392
- this.dispatchEvent(this.makeEvent_(
1449
+ this.dispatchEvent(shaka.Player.makeEvent_(
1393
1450
  shaka.util.FakeEvent.EventName.Loading));
1394
1451
 
1395
- const startTimeOfLoad = Date.now() / 1000;
1452
+ if (preloadManager) {
1453
+ mimeType = preloadManager.getMimeType();
1454
+ } else if (!mimeType) {
1455
+ await mutexWrapOperation(async () => {
1456
+ mimeType = await this.guessMimeType_(assetUri);
1457
+ }, 'guessMimeType_');
1458
+ }
1459
+
1460
+ const wasPreloaded = !!preloadManager;
1461
+ if (!preloadManager) {
1462
+ // For simplicity, if an asset is NOT preloaded, start an internal
1463
+ // "preload" here without prefetch.
1464
+ // That way, both a preload and normal load can follow the same code
1465
+ // paths.
1466
+ // NOTE: await preloadInner_ can be outside the mutex because it should
1467
+ // not mutate "this".
1468
+ preloadManager = await this.preloadInner_(
1469
+ assetUri, startTime, mimeType, /* standardLoad= */ true);
1470
+ if (preloadManager) {
1471
+ preloadManager.setEventHandoffTarget(this);
1472
+ this.stats_ = preloadManager.getStats();
1473
+ await mutexWrapOperation(() => preloadManager.start(), 'preload');
1474
+ } else {
1475
+ this.stats_ = new shaka.util.Stats();
1476
+ }
1477
+ } else {
1478
+ // Hook up events, so any events emitted by the preloadManager will
1479
+ // instead be emitted by the player.
1480
+ preloadManager.setEventHandoffTarget(this);
1481
+ this.stats_ = preloadManager.getStats();
1482
+ }
1483
+ // Now, if there is no preload manager, that means that this is a src=
1484
+ // asset.
1485
+ const shouldUseSrcEquals = !preloadManager;
1486
+
1487
+ const startTimeOfLoad = preloadManager ?
1488
+ preloadManager.getStartTimeOfLoad() : (Date.now() / 1000);
1396
1489
 
1397
1490
  // Stats are for a single playback/load session. Stats must be initialized
1398
1491
  // before we allow calls to |updateStateHistory|.
1399
- this.stats_ = new shaka.util.Stats();
1492
+ this.stats_ =
1493
+ preloadManager ? preloadManager.getStats() : new shaka.util.Stats();
1400
1494
 
1401
1495
  this.assetUri_ = assetUri;
1496
+ this.mimeType_ = mimeType || null;
1402
1497
 
1403
- if (!mimeType) {
1404
- await mutexWrapOperation(async () => {
1405
- mimeType = await this.guessMimeType_();
1406
- }, 'guessMimeType_');
1407
- }
1408
- const shouldUseSrcEquals = this.shouldUseSrcEquals_(assetUri, mimeType);
1409
1498
  if (shouldUseSrcEquals) {
1410
1499
  await mutexWrapOperation(async () => {
1411
1500
  goog.asserts.assert(mimeType, 'We should know the mimeType by now!');
@@ -1421,42 +1510,475 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1421
1510
  await this.initializeMediaSourceEngineInner_();
1422
1511
  }, 'initializeMediaSourceEngineInner_');
1423
1512
  }
1513
+
1514
+ // Get manifest and associated values from preloader.
1515
+ await mutexWrapOperation(
1516
+ () => preloadManager.waitForManifest(), 'waitForManifest');
1517
+ this.config_ = preloadManager.getConfiguration();
1518
+ this.manifestFilterer_ = preloadManager.getManifestFilterer();
1519
+ this.parserFactory_ = preloadManager.getParserFactory();
1520
+ this.parser_ = preloadManager.receiveParser();
1521
+ this.regionTimeline_ = preloadManager.receiveRegionTimeline();
1522
+ this.qualityObserver_ = preloadManager.getQualityObserver();
1523
+ this.manifest_ = preloadManager.getManifest();
1524
+ const currentAdaptationSetCriteria =
1525
+ preloadManager.getCurrentAdaptationSetCriteria();
1526
+ if (currentAdaptationSetCriteria) {
1527
+ this.currentAdaptationSetCriteria_ = currentAdaptationSetCriteria;
1528
+ }
1529
+ if (wasPreloaded && this.video_ && this.video_.nodeName === 'AUDIO') {
1530
+ // Filter the variants to be audio-only after the fact.
1531
+ // As, when preloading, we don't know if we are going to be attached
1532
+ // to a video or audio element when we load, we have to do the auto
1533
+ // audio-only filtering here, post-facto.
1534
+ this.makeManifestAudioOnly_();
1535
+ // And continue to do so in the future.
1536
+ this.configure('manifest.disableVideo', true);
1537
+ }
1538
+
1539
+ // Get drm engine from preloader, then finalize it.
1540
+ await mutexWrapOperation(
1541
+ () => preloadManager.waitForDrm(), 'waitForDrm');
1542
+ this.drmEngine_ = preloadManager.receiveDrmEngine();
1424
1543
  await mutexWrapOperation(async () => {
1425
- await this.parseManifestInner_(mimeType);
1426
- }, 'parseManifestInner_');
1427
- await mutexWrapOperation(async () => {
1428
- await this.initializeDrmInner_();
1429
- }, 'initializeDrmInner_');
1544
+ await this.drmEngine_.attach(this.video_);
1545
+ }, 'drmEngine_.attach');
1546
+
1547
+ // Choose an initial variant.
1548
+ await mutexWrapOperation(
1549
+ () => preloadManager.waitForChooseInitialVariant(),
1550
+ 'waitForChooseInitialVariant');
1551
+
1552
+ // Also get the ABR manager, which has special logic related to being
1553
+ // received.
1554
+ const abrManagerFactory = preloadManager.getAbrManagerFactory();
1555
+ if (abrManagerFactory) {
1556
+ if (!this.abrManagerFactory_ ||
1557
+ this.abrManagerFactory_ != abrManagerFactory) {
1558
+ this.abrManager_ = preloadManager.receiveAbrManager();
1559
+ this.abrManagerFactory_ = preloadManager.getAbrManagerFactory();
1560
+ if (typeof this.abrManager_.setMediaElement != 'function') {
1561
+ shaka.Deprecate.deprecateFeature(5,
1562
+ 'AbrManager w/o setMediaElement',
1563
+ 'Please use an AbrManager with setMediaElement function.');
1564
+ this.abrManager_.setMediaElement = () => {};
1565
+ }
1566
+ if (typeof this.abrManager_.setCmsdManager != 'function') {
1567
+ shaka.Deprecate.deprecateFeature(5,
1568
+ 'AbrManager w/o setCmsdManager',
1569
+ 'Please use an AbrManager with setCmsdManager function.');
1570
+ this.abrManager_.setCmsdManager = () => {};
1571
+ }
1572
+ if (typeof this.abrManager_.trySuggestStreams != 'function') {
1573
+ shaka.Deprecate.deprecateFeature(5,
1574
+ 'AbrManager w/o trySuggestStreams',
1575
+ 'Please use an AbrManager with trySuggestStreams function.');
1576
+ this.abrManager_.trySuggestStreams = () => {};
1577
+ }
1578
+ }
1579
+ }
1580
+
1581
+ // Load the asset.
1582
+ const segmentPrefetchById =
1583
+ preloadManager.receiveSegmentPrefetchesById();
1584
+ const prefetchedVariant = preloadManager.getPrefetchedVariant();
1430
1585
  await mutexWrapOperation(async () => {
1431
- await this.loadInner_(startTimeOfLoad);
1586
+ await this.loadInner_(
1587
+ startTimeOfLoad, prefetchedVariant, segmentPrefetchById);
1432
1588
  }, 'loadInner_');
1589
+ preloadManager.stopQueuingLatePhaseQueuedOperations();
1433
1590
  }
1434
- this.dispatchEvent(this.makeEvent_(
1591
+ this.dispatchEvent(shaka.Player.makeEvent_(
1435
1592
  shaka.util.FakeEvent.EventName.Loaded));
1436
1593
  } catch (error) {
1437
1594
  if (error.code != shaka.util.Error.Code.LOAD_INTERRUPTED) {
1438
1595
  await this.unload(/* initializeMediaSource= */ false);
1439
1596
  }
1440
1597
  throw error;
1598
+ } finally {
1599
+ if (preloadManager) {
1600
+ // This will cause any resources that were generated but not used to be
1601
+ // properly destroyed or released.
1602
+ await preloadManager.destroy();
1603
+ }
1441
1604
  }
1442
1605
  }
1443
1606
 
1607
+ /**
1608
+ * Modifies the current manifest so that it is audio-only.
1609
+ * @private
1610
+ */
1611
+ makeManifestAudioOnly_() {
1612
+ for (const variant of this.manifest_.variants) {
1613
+ if (variant.video) {
1614
+ variant.video.closeSegmentIndex();
1615
+ variant.video = null;
1616
+ }
1617
+ if (variant.audio && variant.audio.bandwidth) {
1618
+ variant.bandwidth = variant.audio.bandwidth;
1619
+ } else {
1620
+ variant.bandwidth = 0;
1621
+ }
1622
+ }
1623
+ this.manifest_.variants = this.manifest_.variants.filter((v) => {
1624
+ return v.audio;
1625
+ });
1626
+ }
1627
+
1628
+ /**
1629
+ * Unloads the currently playing stream, if any, and returns a PreloadManager
1630
+ * that contains the loaded manifest of that asset, if any.
1631
+ * Allows for the asset to be re-loaded by this player faster, in the future.
1632
+ * When in src= mode, this unloads but does not make a PreloadManager.
1633
+ *
1634
+ * @param {boolean=} initializeMediaSource
1635
+ * @param {boolean=} keepAdManager
1636
+ * @return {!Promise.<?shaka.media.PreloadManager>}
1637
+ * @export
1638
+ */
1639
+ async unloadAndSavePreload(
1640
+ initializeMediaSource = true, keepAdManager = false) {
1641
+ let preloadManager = null;
1642
+ if (this.manifest_ && this.parser_ && this.parserFactory_ &&
1643
+ this.assetUri_) {
1644
+ let startTime = this.video_.currentTime;
1645
+ if (this.isLive()) {
1646
+ startTime = null;
1647
+ }
1648
+ // We have enough information to make a PreloadManager!
1649
+ const startTimeOfLoad = Date.now() / 1000;
1650
+ preloadManager = await this.makePreloadManager_(
1651
+ this.assetUri_,
1652
+ startTime,
1653
+ this.mimeType_,
1654
+ startTimeOfLoad,
1655
+ /* allowPrefetch= */ true,
1656
+ /* disableVideo= */ false,
1657
+ /* allowMakeAbrManager= */ false);
1658
+ preloadManager.attachManifest(
1659
+ this.manifest_, this.parser_, this.parserFactory_);
1660
+ preloadManager.attachAbrManager(
1661
+ this.abrManager_, this.abrManagerFactory_);
1662
+ preloadManager.attachAdaptationSetCriteria(
1663
+ this.currentAdaptationSetCriteria_);
1664
+ preloadManager.start();
1665
+ // Null the manifest and manifestParser, so that they won't be shut down
1666
+ // during unload and will continue to live inside the preloadManager.
1667
+ this.manifest_ = null;
1668
+ this.parser_ = null;
1669
+ this.parserFactory_ = null;
1670
+ // Null the abrManager and abrManagerFactory, so that they won't be shut
1671
+ // down during unload and will continue to live inside the preloadManager.
1672
+ this.abrManager_ = null;
1673
+ this.abrManagerFactory_ = null;
1674
+ }
1675
+ await this.unload(initializeMediaSource, keepAdManager);
1676
+ return preloadManager;
1677
+ }
1678
+
1679
+ /**
1680
+ * Starts to preload a given asset, and returns a PreloadManager object that
1681
+ * represents that preloading process.
1682
+ * The PreloadManager will load the manifest for that asset, as well as the
1683
+ * initialization segment. It will not preload anything more than that;
1684
+ * this feature is intended for reducing start-time latency, not for fully
1685
+ * downloading assets before playing them (for that, use
1686
+ * |shaka.offline.Storage|).
1687
+ * You can pass that PreloadManager object in to the |load| method on this
1688
+ * Player instance to finish loading that particular asset, or you can call
1689
+ * the |destroy| method on the manager if the preload is no longer necessary.
1690
+ * If this returns null rather than a PreloadManager, that indicates that the
1691
+ * asset must be played with src=, which cannot be preloaded.
1692
+ *
1693
+ * @param {string} assetUri
1694
+ * @param {?number=} startTime
1695
+ * When <code>startTime</code> is <code>null</code> or
1696
+ * <code>undefined</code>, playback will start at the default start time (0
1697
+ * for VOD and liveEdge for LIVE).
1698
+ * @param {?string=} mimeType
1699
+ * @return {!Promise.<?shaka.media.PreloadManager>}
1700
+ * @export
1701
+ */
1702
+ async preload(assetUri, startTime = null, mimeType) {
1703
+ const preloadManager = await this.preloadInner_(
1704
+ assetUri, startTime, mimeType);
1705
+ if (!preloadManager) {
1706
+ this.onError_(new shaka.util.Error(
1707
+ shaka.util.Error.Severity.CRITICAL,
1708
+ shaka.util.Error.Category.PLAYER,
1709
+ shaka.util.Error.Code.SRC_EQUALS_PRELOAD_NOT_SUPPORTED));
1710
+ } else {
1711
+ preloadManager.start().catch((error) => {}); // Catch errors.
1712
+ }
1713
+ return preloadManager;
1714
+ }
1715
+
1716
+ /**
1717
+ * @param {string} assetUri
1718
+ * @param {?number} startTime
1719
+ * @param {?string=} mimeType
1720
+ * @param {boolean=} standardLoad
1721
+ * @return {!Promise.<?shaka.media.PreloadManager>}
1722
+ * @private
1723
+ */
1724
+ async preloadInner_(assetUri, startTime, mimeType, standardLoad = false) {
1725
+ goog.asserts.assert(this.networkingEngine_, 'Should have a net engine!');
1726
+ goog.asserts.assert(this.config_, 'Config must not be null!');
1727
+ const startTimeOfLoad = Date.now() / 1000;
1728
+ if (!mimeType) {
1729
+ mimeType = await this.guessMimeType_(assetUri);
1730
+ }
1731
+ const shouldUseSrcEquals = this.shouldUseSrcEquals_(assetUri, mimeType);
1732
+ if (shouldUseSrcEquals) {
1733
+ // We cannot preload src= content.
1734
+ return null;
1735
+ }
1736
+ let disableVideo = false;
1737
+ let allowMakeAbrManager = true;
1738
+ if (standardLoad) {
1739
+ if (this.abrManager_ &&
1740
+ this.abrManagerFactory_ == this.config_.abrFactory) {
1741
+ // If there's already an abr manager, don't make a new abr manager at
1742
+ // all.
1743
+ // In standardLoad mode, the abr manager isn't used for anything anyway,
1744
+ // so it should only be created to create an abr manager for the player
1745
+ // to use... which is unnecessary if we already have one of the right
1746
+ // type.
1747
+ allowMakeAbrManager = false;
1748
+ }
1749
+ if (this.video_ && this.video_.nodeName === 'AUDIO') {
1750
+ disableVideo = true;
1751
+ }
1752
+ }
1753
+ return this.makePreloadManager_(
1754
+ assetUri, startTime, mimeType || null, startTimeOfLoad,
1755
+ /* allowPrefetch= */ !standardLoad, disableVideo, allowMakeAbrManager);
1756
+ }
1757
+
1758
+ /**
1759
+ * @param {string} assetUri
1760
+ * @param {?number} startTime
1761
+ * @param {?string} mimeType
1762
+ * @param {number} startTimeOfLoad
1763
+ * @param {boolean=} allowPrefetch
1764
+ * @param {boolean=} disableVideo
1765
+ * @param {boolean=} allowMakeAbrManager
1766
+ * @return {!Promise.<!shaka.media.PreloadManager>}
1767
+ * @private
1768
+ */
1769
+ async makePreloadManager_(assetUri, startTime, mimeType, startTimeOfLoad,
1770
+ allowPrefetch = true, disableVideo = false, allowMakeAbrManager = true) {
1771
+ goog.asserts.assert(this.networkingEngine_, 'Must have net engine');
1772
+ /** @type {?shaka.media.PreloadManager} */
1773
+ let preloadManager = null;
1774
+
1775
+ const config = shaka.util.ObjectUtils.cloneObject(this.config_);
1776
+ if (disableVideo) {
1777
+ config.manifest.disableVideo = true;
1778
+ }
1779
+
1780
+ const getPreloadManager = () => {
1781
+ goog.asserts.assert(preloadManager, 'Must have preload manager');
1782
+ if (preloadManager.hasBeenAttached() && preloadManager.isDestroyed()) {
1783
+ return null;
1784
+ }
1785
+ return preloadManager;
1786
+ };
1787
+
1788
+ const getConfig = () => {
1789
+ if (getPreloadManager()) {
1790
+ return getPreloadManager().getConfiguration();
1791
+ } else {
1792
+ return this.config_;
1793
+ }
1794
+ };
1795
+
1796
+ const setConfig = (name, value) => {
1797
+ if (getPreloadManager()) {
1798
+ preloadManager.configure(name, value);
1799
+ } else {
1800
+ this.configure(name, value);
1801
+ }
1802
+ };
1803
+
1804
+ // Avoid having to detect the resolution again if it has already been
1805
+ // detected or set
1806
+ if (this.maxHwRes_.width == Infinity &&
1807
+ this.maxHwRes_.height == Infinity) {
1808
+ const maxResolution =
1809
+ await shaka.util.Platform.detectMaxHardwareResolution();
1810
+ this.maxHwRes_.width = maxResolution.width;
1811
+ this.maxHwRes_.height = maxResolution.height;
1812
+ }
1813
+ const manifestFilterer = new shaka.media.ManifestFilterer(
1814
+ config, this.maxHwRes_, null);
1815
+ const manifestPlayerInterface = {
1816
+ networkingEngine: this.networkingEngine_,
1817
+ filter: async (manifest) => {
1818
+ const tracksChanged = await manifestFilterer.filterManifest(manifest);
1819
+ if (tracksChanged) {
1820
+ // Delay the 'trackschanged' event so StreamingEngine has time to
1821
+ // absorb the changes before the user tries to query it.
1822
+ const event = shaka.Player.makeEvent_(
1823
+ shaka.util.FakeEvent.EventName.TracksChanged);
1824
+ await Promise.resolve();
1825
+ preloadManager.dispatchEvent(event);
1826
+ }
1827
+ },
1828
+ makeTextStreamsForClosedCaptions: (manifest) => {
1829
+ return this.makeTextStreamsForClosedCaptions_(manifest);
1830
+ },
1831
+
1832
+ // Called when the parser finds a timeline region. This can be called
1833
+ // before we start playback or during playback (live/in-progress
1834
+ // manifest).
1835
+ onTimelineRegionAdded: (region) => {
1836
+ preloadManager.getRegionTimeline().addRegion(region);
1837
+ },
1838
+
1839
+ onEvent: (event) => preloadManager.dispatchEvent(event),
1840
+ onError: (error) => preloadManager.onError(error),
1841
+ isLowLatencyMode: () => getConfig().streaming.lowLatencyMode,
1842
+ isAutoLowLatencyMode: () => getConfig().streaming.autoLowLatencyMode,
1843
+ enableLowLatencyMode: () => {
1844
+ setConfig('streaming.lowLatencyMode', true);
1845
+ },
1846
+ updateDuration: () => {
1847
+ if (this.streamingEngine_ && preloadManager.hasBeenAttached()) {
1848
+ this.streamingEngine_.updateDuration();
1849
+ }
1850
+ },
1851
+ newDrmInfo: (stream) => {
1852
+ // We may need to create new sessions for any new init data.
1853
+ const drmEngine = preloadManager.getDrmEngine();
1854
+ const currentDrmInfo = drmEngine ? drmEngine.getDrmInfo() : null;
1855
+ // DrmEngine.newInitData() requires mediaKeys to be available.
1856
+ if (currentDrmInfo && drmEngine.getMediaKeys()) {
1857
+ manifestFilterer.processDrmInfos(currentDrmInfo.keySystem, stream);
1858
+ }
1859
+ },
1860
+ onManifestUpdated: () => {
1861
+ const eventName = shaka.util.FakeEvent.EventName.ManifestUpdated;
1862
+ const data = (new Map()).set('isLive', this.isLive());
1863
+ preloadManager.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
1864
+
1865
+ preloadManager.addQueuedOperation(false, () => {
1866
+ if (this.adManager_) {
1867
+ this.adManager_.onManifestUpdated(this.isLive());
1868
+ }
1869
+ });
1870
+ },
1871
+ getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(),
1872
+ };
1873
+ const regionTimeline =
1874
+ new shaka.media.RegionTimeline(() => this.seekRange());
1875
+ regionTimeline.addEventListener('regionadd', (event) => {
1876
+ /** @type {shaka.extern.TimelineRegionInfo} */
1877
+ const region = event['region'];
1878
+ this.onRegionEvent_(
1879
+ shaka.util.FakeEvent.EventName.TimelineRegionAdded, region,
1880
+ preloadManager);
1881
+
1882
+ preloadManager.addQueuedOperation(false, () => {
1883
+ if (this.adManager_) {
1884
+ this.adManager_.onDashTimedMetadata(region);
1885
+ }
1886
+ });
1887
+ });
1888
+ let qualityObserver = null;
1889
+ if (config.streaming.observeQualityChanges) {
1890
+ qualityObserver = new shaka.media.QualityObserver(
1891
+ () => this.getBufferedInfo());
1892
+
1893
+ qualityObserver.addEventListener('qualitychange', (event) => {
1894
+ /** @type {shaka.extern.MediaQualityInfo} */
1895
+ const mediaQualityInfo = event['quality'];
1896
+ /** @type {number} */
1897
+ const position = event['position'];
1898
+ this.onMediaQualityChange_(mediaQualityInfo, position);
1899
+ });
1900
+ }
1901
+ let firstEvent = true;
1902
+ const drmPlayerInterface = {
1903
+ netEngine: this.networkingEngine_,
1904
+ onError: (e) => preloadManager.onError(e),
1905
+ onKeyStatus: (map) => {
1906
+ preloadManager.addQueuedOperation(true, () => {
1907
+ this.onKeyStatus_(map);
1908
+ });
1909
+ },
1910
+ onExpirationUpdated: (id, expiration) => {
1911
+ const event = shaka.Player.makeEvent_(
1912
+ shaka.util.FakeEvent.EventName.ExpirationUpdated);
1913
+ preloadManager.dispatchEvent(event);
1914
+ const parser = preloadManager.getParser();
1915
+ if (parser && parser.onExpirationUpdated) {
1916
+ parser.onExpirationUpdated(id, expiration);
1917
+ }
1918
+ },
1919
+ onEvent: (e) => {
1920
+ preloadManager.dispatchEvent(e);
1921
+ if (e.type == shaka.util.FakeEvent.EventName.DrmSessionUpdate &&
1922
+ firstEvent) {
1923
+ firstEvent = false;
1924
+ const now = Date.now() / 1000;
1925
+ const delta = now - preloadManager.getStartTimeOfDRM();
1926
+ const stats = this.stats_ || preloadManager.getStats();
1927
+ stats.setDrmTime(delta);
1928
+ // LCEVC data by itself is not encrypted in DRM protected streams
1929
+ // and can therefore be accessed and decoded as normal. However,
1930
+ // the LCEVC decoder needs access to the VideoElement output in
1931
+ // order to apply the enhancement. In DRM contexts where the
1932
+ // browser CDM restricts access from our decoder, the enhancement
1933
+ // cannot be applied and therefore the LCEVC output canvas is
1934
+ // hidden accordingly.
1935
+ if (this.lcevcDec_) {
1936
+ this.lcevcDec_.hideCanvas();
1937
+ }
1938
+ }
1939
+ },
1940
+ };
1941
+
1942
+ // Sadly, as the network engine creation code must be replaceable by tests,
1943
+ // it cannot be made and use the utilities defined in this function.
1944
+ const networkingEngine = this.createNetworkingEngine(getPreloadManager);
1945
+
1946
+ /** @return {!shaka.media.DrmEngine} */
1947
+ const createDrmEngine = () => {
1948
+ return this.createDrmEngine(drmPlayerInterface);
1949
+ };
1950
+ /** @type {!shaka.media.PreloadManager.PlayerInterface} */
1951
+ const playerInterface = {
1952
+ config,
1953
+ manifestPlayerInterface,
1954
+ regionTimeline,
1955
+ qualityObserver,
1956
+ createDrmEngine,
1957
+ manifestFilterer,
1958
+ networkingEngine,
1959
+ allowPrefetch,
1960
+ allowMakeAbrManager,
1961
+ };
1962
+ preloadManager = new shaka.media.PreloadManager(
1963
+ assetUri, mimeType, startTimeOfLoad, startTime, playerInterface);
1964
+ return preloadManager;
1965
+ }
1966
+
1444
1967
  /**
1445
1968
  * Determines the mimeType of the given asset, if we are not told that inside
1446
1969
  * the loading process.
1447
1970
  *
1971
+ * @param {string} assetUri
1448
1972
  * @return {!Promise.<?string>} mimeType
1449
1973
  * @private
1450
1974
  */
1451
- async guessMimeType_() {
1452
- goog.asserts.assert(this.assetUri_, 'should have a uri by now.');
1453
-
1975
+ async guessMimeType_(assetUri) {
1454
1976
  // If no MIME type is provided, and we can't base it on extension, make a
1455
1977
  // HEAD request to determine it.
1456
1978
  goog.asserts.assert(this.networkingEngine_, 'Should have a net engine!');
1457
1979
  const retryParams = this.config_.manifest.retryParameters;
1458
1980
  let mimeType = await shaka.net.NetworkingUtils.getMimeType(
1459
- this.assetUri_, this.networkingEngine_, retryParams);
1981
+ assetUri, this.networkingEngine_, retryParams);
1460
1982
  if (mimeType == 'application/x-mpegurl' && shaka.util.Platform.isApple()) {
1461
1983
  mimeType = 'application/vnd.apple.mpegurl';
1462
1984
  }
@@ -1516,6 +2038,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1516
2038
  return true;
1517
2039
  }
1518
2040
 
2041
+ // Native FairPlay HLS can be preferred on Apple platfforms.
2042
+ if (Platform.isApple() &&
2043
+ (this.config_.drm.servers['com.apple.fps'] ||
2044
+ this.config_.drm.servers['com.apple.fps.1_0'])) {
2045
+ return this.config_.streaming.useNativeHlsForFairPlay;
2046
+ }
2047
+
1519
2048
  // For Safari, we have an older flag which only applies to this one
1520
2049
  // browser:
1521
2050
  if (Platform.isApple()) {
@@ -1559,276 +2088,52 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1559
2088
  // until they are both initialized before setting the initial value.
1560
2089
  const textDisplayerFactory = this.config_.textDisplayFactory;
1561
2090
  const textDisplayer = textDisplayerFactory();
2091
+ if (textDisplayer.configure) {
2092
+ textDisplayer.configure(this.config_.textDisplayer);
2093
+ } else {
2094
+ shaka.Deprecate.deprecateFeature(5,
2095
+ 'Text displayer w/ configure',
2096
+ 'Text displayer should have a "configure" method!');
2097
+ }
1562
2098
  this.lastTextFactory_ = textDisplayerFactory;
1563
2099
 
1564
2100
  const mediaSourceEngine = this.createMediaSourceEngine(
1565
2101
  this.video_,
1566
- textDisplayer,
1567
- (metadata, offset, endTime) => {
1568
- this.processTimedMetadataMediaSrc_(metadata, offset, endTime);
1569
- },
1570
- this.lcevcDec_);
1571
- mediaSourceEngine.configure(this.config_.mediaSource);
1572
- const {segmentRelativeVttTiming} = this.config_.manifest;
1573
- mediaSourceEngine.setSegmentRelativeVttTiming(segmentRelativeVttTiming);
1574
-
1575
- // Wait for media source engine to finish opening. This promise should
1576
- // NEVER be rejected as per the media source engine implementation.
1577
- await mediaSourceEngine.open();
1578
-
1579
- // Wait until it is ready to actually store the reference.
1580
- this.mediaSourceEngine_ = mediaSourceEngine;
1581
- }
1582
-
1583
- /**
1584
- * Pick an initialize a manifest parser, then have it download and parse the
1585
- * manifest.
1586
- *
1587
- * @param {?string=} mimeType
1588
- * @return {!Promise}
1589
- * @private
1590
- */
1591
- async parseManifestInner_(mimeType) {
1592
- goog.asserts.assert(
1593
- this.assetUri_,
1594
- 'should have a uri when making the parser.');
1595
- goog.asserts.assert(
1596
- this.video_,
1597
- 'We should have a media element when initializing the parser.');
1598
- goog.asserts.assert(
1599
- this.networkingEngine_,
1600
- 'Need networking engine when initializing the parser.');
1601
- goog.asserts.assert(
1602
- this.cmcdManager_,
1603
- 'Need CMCD manager to populate manifest request data.');
1604
- goog.asserts.assert(
1605
- this.cmsdManager_,
1606
- 'Need CMSD manager to process response headers.');
1607
- goog.asserts.assert(
1608
- this.config_,
1609
- 'Need player config when initializing the parser.');
1610
-
1611
- // Store references to things we asserted so that we don't need to
1612
- // reassert them again later.
1613
- const networkingEngine = this.networkingEngine_;
1614
-
1615
- this.makeStateChangeEvent_('manifest-parser');
1616
-
1617
- // Create the parser that we will use to parse the manifest.
1618
- this.parserFactory_ = shaka.media.ManifestParser.getFactory(
1619
- this.assetUri_,
1620
- mimeType || null);
1621
- goog.asserts.assert(this.parserFactory_, 'Must have manifest parser');
1622
- this.parser_ = this.parserFactory_();
1623
-
1624
- const manifestConfig =
1625
- shaka.util.ObjectUtils.cloneObject(this.config_.manifest);
1626
- // Don't read video segments if the player is attached to an audio element
1627
- if (this.video_ && this.video_.nodeName === 'AUDIO') {
1628
- manifestConfig.disableVideo = true;
1629
- }
1630
-
1631
- this.parser_.configure(manifestConfig);
1632
-
1633
- // This will be needed by the parser once it starts parsing, so we will
1634
- // initialize it now even through it appears a little out-of-place.
1635
- this.regionTimeline_ =
1636
- new shaka.media.RegionTimeline(() => this.seekRange());
1637
- this.regionTimeline_.addEventListener('regionadd', (event) => {
1638
- /** @type {shaka.extern.TimelineRegionInfo} */
1639
- const region = event['region'];
1640
- this.onRegionEvent_(
1641
- shaka.util.FakeEvent.EventName.TimelineRegionAdded, region);
1642
-
1643
- if (this.adManager_) {
1644
- this.adManager_.onDashTimedMetadata(region);
1645
- }
1646
- });
1647
-
1648
- this.qualityObserver_ = null;
1649
- if (this.config_.streaming.observeQualityChanges) {
1650
- this.qualityObserver_ = new shaka.media.QualityObserver(
1651
- () => this.getBufferedInfo());
1652
-
1653
- this.qualityObserver_.addEventListener('qualitychange', (event) => {
1654
- /** @type {shaka.extern.MediaQualityInfo} */
1655
- const mediaQualityInfo = event['quality'];
1656
- /** @type {number} */
1657
- const position = event['position'];
1658
- this.onMediaQualityChange_(mediaQualityInfo, position);
1659
- });
1660
- }
1661
-
1662
- const playerInterface = {
1663
- networkingEngine: networkingEngine,
1664
- filter: (manifest) => this.filterManifest_(manifest),
1665
- makeTextStreamsForClosedCaptions: (manifest) => {
1666
- return this.makeTextStreamsForClosedCaptions_(manifest);
1667
- },
1668
-
1669
- // Called when the parser finds a timeline region. This can be called
1670
- // before we start playback or during playback (live/in-progress
1671
- // manifest).
1672
- onTimelineRegionAdded: (region) => {
1673
- this.regionTimeline_.addRegion(region);
1674
- },
1675
-
1676
- onEvent: (event) => this.dispatchEvent(event),
1677
- onError: (error) => this.onError_(error),
1678
- isLowLatencyMode: () => this.isLowLatencyMode_(),
1679
- isAutoLowLatencyMode: () => this.isAutoLowLatencyMode_(),
1680
- enableLowLatencyMode: () => {
1681
- this.configure('streaming.lowLatencyMode', true);
1682
- },
1683
- updateDuration: () => {
1684
- if (this.streamingEngine_) {
1685
- this.streamingEngine_.updateDuration();
1686
- }
1687
- },
1688
- newDrmInfo: (stream) => {
1689
- // We may need to create new sessions for any new init data.
1690
- const currentDrmInfo =
1691
- this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
1692
- // DrmEngine.newInitData() requires mediaKeys to be available.
1693
- if (currentDrmInfo && this.drmEngine_.getMediaKeys()) {
1694
- this.processDrmInfos_(currentDrmInfo.keySystem, stream);
1695
- }
1696
- },
1697
- onManifestUpdated: () => {
1698
- const eventName = shaka.util.FakeEvent.EventName.ManifestUpdated;
1699
- const data = (new Map()).set('isLive', this.isLive());
1700
- this.dispatchEvent(this.makeEvent_(eventName, data));
1701
- if (this.adManager_) {
1702
- this.adManager_.onManifestUpdated(this.isLive());
1703
- }
1704
- },
1705
- getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(),
1706
- };
1707
-
1708
- const startTime = Date.now() / 1000;
1709
-
1710
- this.makeStateChangeEvent_('manifest');
1711
-
1712
- this.manifest_ = await this.parser_.start(
1713
- this.assetUri_, playerInterface);
1714
-
1715
- // This event is fired after the manifest is parsed, but before any
1716
- // filtering takes place.
1717
- const event =
1718
- this.makeEvent_(shaka.util.FakeEvent.EventName.ManifestParsed);
1719
- this.dispatchEvent(event);
1720
-
1721
- // We require all manifests to have at least one variant.
1722
- if (this.manifest_.variants.length == 0) {
1723
- throw new shaka.util.Error(
1724
- shaka.util.Error.Severity.CRITICAL,
1725
- shaka.util.Error.Category.MANIFEST,
1726
- shaka.util.Error.Code.NO_VARIANTS);
1727
- }
1728
-
1729
- // Make sure that all variants are either: audio-only, video-only, or
1730
- // audio-video.
1731
- shaka.Player.filterForAVVariants_(this.manifest_);
1732
-
1733
- const now = Date.now() / 1000;
1734
- const delta = now - startTime;
1735
- this.stats_.setManifestTime(delta);
1736
- }
1737
-
1738
- /**
1739
- * Initializes the DRM engine.
1740
- *
1741
- * @return {!Promise}
1742
- * @private
1743
- */
1744
- async initializeDrmInner_() {
1745
- goog.asserts.assert(
1746
- this.networkingEngine_,
1747
- '|onInitializeDrm_| should never be called after |destroy|');
1748
- goog.asserts.assert(
1749
- this.config_,
1750
- '|onInitializeDrm_| should never be called after |destroy|');
1751
- goog.asserts.assert(
1752
- this.manifest_,
1753
- '|this.manifest_| should have been set in an earlier step.');
1754
- goog.asserts.assert(
1755
- this.video_,
1756
- 'We should have a media element when initializing the DRM Engine.');
1757
-
1758
- this.makeStateChangeEvent_('drm-engine');
1759
-
1760
- const startTime = Date.now() / 1000;
1761
- let firstEvent = true;
1762
-
1763
- this.drmEngine_ = this.createDrmEngine({
1764
- netEngine: this.networkingEngine_,
1765
- onError: (e) => {
1766
- this.onError_(e);
1767
- },
1768
- onKeyStatus: (map) => {
1769
- this.onKeyStatus_(map);
1770
- },
1771
- onExpirationUpdated: (id, expiration) => {
1772
- this.onExpirationUpdated_(id, expiration);
1773
- },
1774
- onEvent: (e) => {
1775
- this.dispatchEvent(e);
1776
- if (e.type == shaka.util.FakeEvent.EventName.DrmSessionUpdate &&
1777
- firstEvent) {
1778
- firstEvent = false;
1779
- const now = Date.now() / 1000;
1780
- const delta = now - startTime;
1781
- this.stats_.setDrmTime(delta);
1782
- // LCEVC data by itself is not encrypted in DRM protected streams
1783
- // and can therefore be accessed and decoded as normal. However,
1784
- // the LCEVC decoder needs access to the VideoElement output in
1785
- // order to apply the enhancement. In DRM contexts where the
1786
- // browser CDM restricts access from our decoder, the enhancement
1787
- // cannot be applied and therefore the LCEVC output canvas is
1788
- // hidden accordingly.
1789
- if (this.lcevcDec_) {
1790
- this.lcevcDec_.hideCanvas();
1791
- }
1792
- }
1793
- },
1794
- });
1795
-
1796
- this.drmEngine_.configure(this.config_.drm);
1797
-
1798
- const tracksChanged = shaka.util.StreamUtils.applyRestrictions(
1799
- this.manifest_.variants, this.config_.restrictions, this.maxHwRes_);
1800
- if (tracksChanged && this.streamingEngine_) {
1801
- this.onTracksChanged_();
1802
- }
1803
-
1804
- const playableVariants = shaka.util.StreamUtils.getPlayableVariants(
1805
- this.manifest_.variants);
1806
-
1807
- await this.drmEngine_.initForPlayback(
1808
- playableVariants,
1809
- this.manifest_.offlineSessionIds);
2102
+ textDisplayer,
2103
+ (metadata, offset, endTime) => {
2104
+ this.processTimedMetadataMediaSrc_(metadata, offset, endTime);
2105
+ },
2106
+ this.lcevcDec_);
2107
+ mediaSourceEngine.configure(this.config_.mediaSource);
2108
+ const {segmentRelativeVttTiming} = this.config_.manifest;
2109
+ mediaSourceEngine.setSegmentRelativeVttTiming(segmentRelativeVttTiming);
1810
2110
 
1811
- await this.drmEngine_.attach(this.video_);
2111
+ // Wait for media source engine to finish opening. This promise should
2112
+ // NEVER be rejected as per the media source engine implementation.
2113
+ await mediaSourceEngine.open();
1812
2114
 
1813
- // Now that we have drm information, filter the manifest (again) so that
1814
- // we can ensure we only use variants with the selected key system.
1815
- await this.filterManifest_(this.manifest_);
2115
+ // Wait until it is ready to actually store the reference.
2116
+ this.mediaSourceEngine_ = mediaSourceEngine;
1816
2117
  }
1817
2118
 
1818
2119
  /**
1819
2120
  * Starts loading the content described by the parsed manifest.
1820
2121
  *
1821
2122
  * @param {number} startTimeOfLoad
2123
+ * @param {?shaka.extern.Variant} prefetchedVariant
2124
+ * @param {!Map.<number, shaka.media.SegmentPrefetch>} segmentPrefetchById
1822
2125
  * @return {!Promise}
1823
2126
  * @private
1824
2127
  */
1825
- async loadInner_(startTimeOfLoad) {
2128
+ async loadInner_(startTimeOfLoad, prefetchedVariant, segmentPrefetchById) {
1826
2129
  goog.asserts.assert(
1827
2130
  this.video_, 'We should have a media element by now.');
1828
2131
  goog.asserts.assert(
1829
2132
  this.manifest_, 'The manifest should already be parsed.');
1830
2133
  goog.asserts.assert(
1831
2134
  this.assetUri_, 'We should have an asset uri by now.');
2135
+ goog.asserts.assert(
2136
+ this.abrManager_, 'We should have an abr manager by now.');
1832
2137
 
1833
2138
  this.makeStateChangeEvent_('load');
1834
2139
 
@@ -1852,46 +2157,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1852
2157
  // depending on the config.
1853
2158
  this.setupLcevc_(this.config_);
1854
2159
 
1855
- const abrFactory = this.config_.abrFactory;
1856
- if (!this.abrManager_ || this.abrManagerFactory_ != abrFactory) {
1857
- this.abrManagerFactory_ = abrFactory;
1858
- this.abrManager_ = abrFactory();
1859
- if (typeof this.abrManager_.setMediaElement != 'function') {
1860
- shaka.Deprecate.deprecateFeature(5,
1861
- 'AbrManager w/o setMediaElement',
1862
- 'Please use an AbrManager with setMediaElement function.');
1863
- this.abrManager_.setMediaElement = () => {};
1864
- }
1865
- if (typeof this.abrManager_.setCmsdManager != 'function') {
1866
- shaka.Deprecate.deprecateFeature(5,
1867
- 'AbrManager w/o setCmsdManager',
1868
- 'Please use an AbrManager with setCmsdManager function.');
1869
- this.abrManager_.setCmsdManager = () => {};
1870
- }
1871
- if (typeof this.abrManager_.trySuggestStreams != 'function') {
1872
- shaka.Deprecate.deprecateFeature(5,
1873
- 'AbrManager w/o trySuggestStreams',
1874
- 'Please use an AbrManager with trySuggestStreams function.');
1875
- this.abrManager_.trySuggestStreams = () => {};
1876
- }
1877
- this.abrManager_.configure(this.config_.abr);
1878
- }
1879
-
1880
- // Copy preferred languages from the config again, in case the config was
1881
- // changed between construction and playback.
1882
- this.currentAdaptationSetCriteria_ =
1883
- new shaka.media.PreferenceBasedCriteria(
1884
- this.config_.preferredAudioLanguage,
1885
- this.config_.preferredVariantRole,
1886
- this.config_.preferredAudioChannelCount,
1887
- this.config_.preferredVideoHdrLevel,
1888
- this.config_.preferSpatialAudio,
1889
- this.config_.preferredVideoLayout,
1890
- this.config_.preferredAudioLabel,
1891
- this.config_.preferredVideoLabel,
1892
- this.config_.mediaSource.codecSwitchingStrategy,
1893
- this.config_.manifest.dash.enableAudioGroups);
1894
-
1895
2160
  this.currentTextLanguage_ = this.config_.preferredTextLanguage;
1896
2161
  this.currentTextRole_ = this.config_.preferredTextRole;
1897
2162
  this.currentTextForced_ = this.config_.preferForcedSubs;
@@ -1906,14 +2171,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1906
2171
  this.abrManager_.setMediaElement(mediaElement);
1907
2172
  this.abrManager_.setCmsdManager(this.cmsdManager_);
1908
2173
 
1909
- // If the content is multi-codec and the browser can play more than one of
1910
- // them, choose codecs now before we initialize streaming.
1911
- shaka.util.StreamUtils.chooseCodecsAndFilterManifest(
1912
- this.manifest_,
1913
- this.config_.preferredVideoCodecs,
1914
- this.config_.preferredAudioCodecs,
1915
- this.config_.preferredDecodingAttributes);
1916
-
1917
2174
  this.streamingEngine_ = this.createStreamingEngine();
1918
2175
  this.streamingEngine_.configure(this.config_.streaming);
1919
2176
 
@@ -1944,15 +2201,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1944
2201
  // The event must be fired after we filter by restrictions but before the
1945
2202
  // active stream is picked to allow those listening for the "streaming"
1946
2203
  // event to make changes before streaming starts.
1947
- this.dispatchEvent(
1948
- this.makeEvent_(shaka.util.FakeEvent.EventName.Streaming));
2204
+ this.dispatchEvent(shaka.Player.makeEvent_(
2205
+ shaka.util.FakeEvent.EventName.Streaming));
1949
2206
 
1950
2207
  // Pick the initial streams to play.
1951
2208
  // Unless the user has already picked a variant, anyway, by calling
1952
2209
  // selectVariantTrack before this loading stage.
1953
- let initialVariant = null;
2210
+ let initialVariant = prefetchedVariant;
1954
2211
  const activeVariant = this.streamingEngine_.getCurrentVariant();
1955
- if (!activeVariant) {
2212
+ if (!activeVariant && !initialVariant) {
1956
2213
  initialVariant = this.chooseVariant_(/* initialSelection= */ true);
1957
2214
  goog.asserts.assert(initialVariant, 'Must choose an initial variant!');
1958
2215
  }
@@ -1977,31 +2234,49 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1977
2234
  this.config_.playRangeStart,
1978
2235
  this.config_.playRangeEnd);
1979
2236
 
1980
- this.playhead_ = this.createPlayhead(this.startTime_);
1981
- this.playheadObservers_ =
1982
- this.createPlayheadObserversForMSE_(startTimeOfLoad);
1983
2237
 
1984
- // We need to start the buffer management code near the end because it
1985
- // will set the initial buffering state and that depends on other
1986
- // components being initialized.
1987
- const rebufferThreshold = Math.max(
1988
- this.manifest_.minBufferTime, this.config_.streaming.rebufferingGoal);
1989
- this.startBufferManagement_(mediaElement, rebufferThreshold);
2238
+ const setupPlayhead = (startTime) => {
2239
+ this.playhead_ = this.createPlayhead(startTime);
2240
+ this.playheadObservers_ =
2241
+ this.createPlayheadObserversForMSE_(startTimeOfLoad);
2242
+
2243
+ // We need to start the buffer management code near the end because it
2244
+ // will set the initial buffering state and that depends on other
2245
+ // components being initialized.
2246
+ const rebufferThreshold = Math.max(
2247
+ this.manifest_.minBufferTime,
2248
+ this.config_.streaming.rebufferingGoal);
2249
+ this.startBufferManagement_(mediaElement, rebufferThreshold);
2250
+ };
2251
+
2252
+ if (!this.config_.streaming.startAtSegmentBoundary) {
2253
+ setupPlayhead(this.startTime_);
2254
+ }
1990
2255
 
1991
2256
  // Now we can switch to the initial variant.
1992
2257
  if (!activeVariant) {
1993
2258
  goog.asserts.assert(initialVariant,
1994
2259
  'Must have choosen an initial variant!');
1995
2260
 
1996
- this.switchVariant_(initialVariant, /* fromAdaptation= */ true,
1997
- /* clearBuffer= */ false, /* safeMargin= */ 0);
1998
-
1999
2261
  // Now that we have initial streams, we may adjust the start time to
2000
2262
  // align to a segment boundary.
2001
2263
  if (this.config_.streaming.startAtSegmentBoundary) {
2002
- this.playhead_.setStartTime(await this.adjustStartTime_(
2003
- initialVariant, this.playhead_.getTime()));
2264
+ const timeline = this.manifest_.presentationTimeline;
2265
+ let initialTime = this.startTime_ || this.video_.currentTime;
2266
+ const seekRangeStart = timeline.getSeekRangeStart();
2267
+ const seekRangeEnd = timeline.getSeekRangeEnd();
2268
+ if (initialTime < seekRangeStart) {
2269
+ initialTime = seekRangeStart;
2270
+ } else if (initialTime > seekRangeEnd) {
2271
+ initialTime = seekRangeEnd;
2272
+ }
2273
+ const startTime = await this.adjustStartTime_(
2274
+ initialVariant, initialTime);
2275
+ setupPlayhead(startTime);
2004
2276
  }
2277
+
2278
+ this.switchVariant_(initialVariant, /* fromAdaptation= */ true,
2279
+ /* clearBuffer= */ false, /* safeMargin= */ 0);
2005
2280
  }
2006
2281
 
2007
2282
  this.playhead_.ready();
@@ -2033,7 +2308,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2033
2308
 
2034
2309
  // Start streaming content. This will start the flow of content down to
2035
2310
  // media source.
2036
- await this.streamingEngine_.start();
2311
+ await this.streamingEngine_.start(segmentPrefetchById);
2037
2312
 
2038
2313
  if (this.config_.abr.enabled) {
2039
2314
  this.abrManager_.enable();
@@ -2061,8 +2336,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2061
2336
 
2062
2337
  const isLive = this.isLive();
2063
2338
 
2064
- if (isLive && (this.config_.streaming.liveSync ||
2065
- this.manifest_.serviceDescription)) {
2339
+ if ((isLive && (this.config_.streaming.liveSync ||
2340
+ this.manifest_.serviceDescription ||
2341
+ this.config_.streaming.liveSyncPanicMode)) ||
2342
+ this.config_.streaming.vodDynamicPlaybackRate) {
2066
2343
  const onTimeUpdate = () => this.onTimeUpdate_();
2067
2344
  this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
2068
2345
  } else if (!isLive) {
@@ -2112,10 +2389,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2112
2389
  this.onError_(e);
2113
2390
  },
2114
2391
  onKeyStatus: (map) => {
2115
- this.onKeyStatus_(map);
2392
+ // According to this.onKeyStatus_, we can't even use this information
2393
+ // in src= mode, so this is just a no-op.
2116
2394
  },
2117
2395
  onExpirationUpdated: (id, expiration) => {
2118
- this.onExpirationUpdated_(id, expiration);
2396
+ const event = shaka.Player.makeEvent_(
2397
+ shaka.util.FakeEvent.EventName.ExpirationUpdated);
2398
+ this.dispatchEvent(event);
2119
2399
  },
2120
2400
  onEvent: (e) => {
2121
2401
  this.dispatchEvent(e);
@@ -2174,12 +2454,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2174
2454
  accessibilityPurpose: null,
2175
2455
  external: false,
2176
2456
  fastSwitching: false,
2457
+ fullMimeTypes: new Set(),
2177
2458
  },
2178
2459
  bandwidth: 100,
2179
2460
  allowedByApplication: true,
2180
2461
  allowedByKeySystem: true,
2181
2462
  decodingInfos: [],
2182
2463
  };
2464
+ variant.video.fullMimeTypes.add(shaka.util.MimeUtils.getFullType(
2465
+ variant.video.mimeType, variant.video.codecs));
2183
2466
 
2184
2467
  this.drmEngine_.setSrcEquals(/* srcEquals= */ true);
2185
2468
  await this.drmEngine_.initForPlayback(
@@ -2313,6 +2596,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2313
2596
  mediaElement.load();
2314
2597
  }
2315
2598
 
2599
+ // In Safari using HLS won't load anything unless you call load()
2600
+ // explicitly, no matter the value of the preload attribute.
2601
+ // Note: this only happens when there are not autoplay.
2602
+ if (mediaElement.preload != 'none' && !mediaElement.autoplay &&
2603
+ shaka.util.MimeUtils.isHlsType(mimeType) &&
2604
+ shaka.util.Platform.safariVersion()) {
2605
+ mediaElement.load();
2606
+ }
2607
+
2316
2608
  // Set the load mode last so that we know that all our components are
2317
2609
  // initialized.
2318
2610
  this.loadMode_ = shaka.Player.LoadMode.SRC_EQUALS;
@@ -2320,8 +2612,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2320
2612
  // The event doesn't mean as much for src= playback, since we don't
2321
2613
  // control streaming. But we should fire it in this path anyway since
2322
2614
  // some applications may be expecting it as a life-cycle event.
2323
- this.dispatchEvent(
2324
- this.makeEvent_(shaka.util.FakeEvent.EventName.Streaming));
2615
+ this.dispatchEvent(shaka.Player.makeEvent_(
2616
+ shaka.util.FakeEvent.EventName.Streaming));
2325
2617
 
2326
2618
  // The "load" Promise is resolved when we have loaded the metadata. If we
2327
2619
  // wait for the full data, that won't happen on Safari until the play
@@ -2393,7 +2685,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2393
2685
 
2394
2686
  const isLive = this.isLive();
2395
2687
 
2396
- if (isLive && this.config_.streaming.liveSync) {
2688
+ if ((isLive && (this.config_.streaming.liveSync ||
2689
+ this.config_.streaming.liveSyncPanicMode)) ||
2690
+ this.config_.streaming.vodDynamicPlaybackRate) {
2397
2691
  const onTimeUpdate = () => this.onTimeUpdate_();
2398
2692
  this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
2399
2693
  } else if (!isLive) {
@@ -2553,7 +2847,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2553
2847
  .set('endTime', endTime)
2554
2848
  .set('metadataType', metadataType)
2555
2849
  .set('payload', payload);
2556
- this.dispatchEvent(this.makeEvent_(eventName, data));
2850
+ this.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
2557
2851
  }
2558
2852
 
2559
2853
  /**
@@ -2583,35 +2877,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2583
2877
  });
2584
2878
  }
2585
2879
 
2586
- /**
2587
- * Take a series of variants and ensure that they only contain one type of
2588
- * variant. The different options are:
2589
- * 1. Audio-Video
2590
- * 2. Audio-Only
2591
- * 3. Video-Only
2592
- *
2593
- * A manifest can only contain a single type because once we initialize media
2594
- * source to expect specific streams, it must always have content for those
2595
- * streams. If we were to start with audio+video and switch to an audio-only
2596
- * variant, media source would block waiting for video content.
2597
- *
2598
- * @param {shaka.extern.Manifest} manifest
2599
- * @private
2600
- */
2601
- static filterForAVVariants_(manifest) {
2602
- const isAVVariant = (variant) => {
2603
- // Audio-video variants may include both streams separately or may be
2604
- // single multiplexed streams with multiple codecs.
2605
- return (variant.video && variant.audio) ||
2606
- (variant.video && variant.video.codecs.includes(','));
2607
- };
2608
- if (manifest.variants.some(isAVVariant)) {
2609
- shaka.log.debug('Found variant with audio and video content, ' +
2610
- 'so filtering out audio-only content.');
2611
- manifest.variants = manifest.variants.filter(isAVVariant);
2612
- }
2613
- }
2614
-
2615
2880
  /**
2616
2881
  * Releases all of the mutexes of the player. Meant for use by the tests.
2617
2882
  * @export
@@ -2629,25 +2894,66 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2629
2894
  * @return {!shaka.media.DrmEngine}
2630
2895
  */
2631
2896
  createDrmEngine(playerInterface) {
2632
- const updateExpirationTime = this.config_.drm.updateExpirationTime;
2633
- return new shaka.media.DrmEngine(playerInterface, updateExpirationTime);
2897
+ return new shaka.media.DrmEngine(playerInterface);
2634
2898
  }
2635
2899
 
2636
2900
  /**
2637
2901
  * Creates a new instance of NetworkingEngine. This can be replaced by tests
2638
2902
  * to create fake instances instead.
2639
2903
  *
2904
+ * @param {(function():?shaka.media.PreloadManager)=} getPreloadManager
2640
2905
  * @return {!shaka.net.NetworkingEngine}
2641
2906
  */
2642
- createNetworkingEngine() {
2643
- /** @type {function(number, number, boolean)} */
2644
- const onProgressUpdated_ = (deltaTimeMs, bytesDownloaded, allowSwitch) => {
2907
+ createNetworkingEngine(getPreloadManager) {
2908
+ if (!getPreloadManager) {
2909
+ getPreloadManager = () => null;
2910
+ }
2911
+
2912
+ const getAbrManager = () => {
2913
+ if (getPreloadManager()) {
2914
+ return getPreloadManager().getAbrManager();
2915
+ } else {
2916
+ return this.abrManager_;
2917
+ }
2918
+ };
2919
+ const getParser = () => {
2920
+ if (getPreloadManager()) {
2921
+ return getPreloadManager().getParser();
2922
+ } else {
2923
+ return this.parser_;
2924
+ }
2925
+ };
2926
+ const lateQueue = (fn) => {
2927
+ if (getPreloadManager()) {
2928
+ getPreloadManager().addQueuedOperation(true, fn);
2929
+ } else {
2930
+ fn();
2931
+ }
2932
+ };
2933
+ const dispatchEvent = (event) => {
2934
+ if (getPreloadManager()) {
2935
+ getPreloadManager().dispatchEvent(event);
2936
+ } else {
2937
+ this.dispatchEvent(event);
2938
+ }
2939
+ };
2940
+ const getStats = () => {
2941
+ if (getPreloadManager()) {
2942
+ return getPreloadManager().getStats();
2943
+ } else {
2944
+ return this.stats_;
2945
+ }
2946
+ };
2947
+ /** @type {shaka.net.NetworkingEngine.onProgressUpdated} */
2948
+ const onProgressUpdated_ = (deltaTimeMs,
2949
+ bytesDownloaded, allowSwitch, request) => {
2645
2950
  // In some situations, such as during offline storage, the abr manager
2646
2951
  // might not yet exist. Therefore, we need to check if abr manager has
2647
2952
  // been initialized before using it.
2648
- if (this.abrManager_) {
2649
- this.abrManager_.segmentDownloaded(
2650
- deltaTimeMs, bytesDownloaded, allowSwitch);
2953
+ const abrManager = getAbrManager();
2954
+ if (abrManager) {
2955
+ abrManager.segmentDownloaded(deltaTimeMs, bytesDownloaded,
2956
+ allowSwitch, request);
2651
2957
  }
2652
2958
  };
2653
2959
  /** @type {shaka.net.NetworkingEngine.OnHeadersReceived} */
@@ -2658,10 +2964,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2658
2964
  .set('headers', headers)
2659
2965
  .set('request', request)
2660
2966
  .set('requestType', requestType);
2661
- this.dispatchEvent(this.makeEvent_(name, data));
2662
- if (this.cmsdManager_) {
2663
- this.cmsdManager_.processHeaders(headers);
2664
- }
2967
+ dispatchEvent(shaka.Player.makeEvent_(name, data));
2968
+ lateQueue(() => {
2969
+ if (this.cmsdManager_) {
2970
+ this.cmsdManager_.processHeaders(headers);
2971
+ }
2972
+ });
2665
2973
  };
2666
2974
  /** @type {shaka.net.NetworkingEngine.OnDownloadFailed} */
2667
2975
  const onDownloadFailed_ = (request, error, httpResponseCode, aborted) => {
@@ -2672,23 +2980,35 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2672
2980
  .set('error', error)
2673
2981
  .set('httpResponseCode', httpResponseCode)
2674
2982
  .set('aborted', aborted);
2675
- this.dispatchEvent(this.makeEvent_(name, data));
2983
+ dispatchEvent(shaka.Player.makeEvent_(name, data));
2676
2984
  };
2677
2985
  /** @type {shaka.net.NetworkingEngine.OnRequest} */
2678
2986
  const onRequest_ = (type, request, context) => {
2679
- this.cmcdManager_.applyData(type, request, context);
2987
+ lateQueue(() => {
2988
+ this.cmcdManager_.applyData(type, request, context);
2989
+ });
2680
2990
  };
2681
2991
 
2682
-
2992
+ /** @type {shaka.net.NetworkingEngine.OnRetry} */
2683
2993
  const onRetry_ = (type, context, newUrl, oldUrl) => {
2684
- if (this.parser_ && this.parser_.banLocation) {
2685
- this.parser_.banLocation(oldUrl);
2994
+ const parser = getParser();
2995
+ if (parser && parser.banLocation) {
2996
+ parser.banLocation(oldUrl);
2997
+ }
2998
+ };
2999
+
3000
+ /** @type {shaka.net.NetworkingEngine.OnResponse} */
3001
+ const onResponse_ = (type, response, context) => {
3002
+ const bytesDownloaded = response.data.byteLength;
3003
+ const stats = getStats();
3004
+ if (stats) {
3005
+ stats.addBytesDownloaded(bytesDownloaded);
2686
3006
  }
2687
3007
  };
2688
3008
 
2689
3009
  return new shaka.net.NetworkingEngine(
2690
3010
  onProgressUpdated_, onHeadersReceived_, onDownloadFailed_, onRequest_,
2691
- onRetry_);
3011
+ onRetry_, onResponse_);
2692
3012
  }
2693
3013
 
2694
3014
  /**
@@ -3015,11 +3335,33 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3015
3335
  delete config['streaming']['forceTransmux'];
3016
3336
  }
3017
3337
 
3338
+ // Deprecate 'streaming.useNativeHlsOnSafari' configuration.
3339
+ if (config['streaming'] && 'useNativeHlsOnSafari' in config['streaming']) {
3340
+ shaka.Deprecate.deprecateFeature(5,
3341
+ 'streaming.useNativeHlsOnSafari configuration',
3342
+ 'Please Use streaming.useNativeHlsForFairPlay or ' +
3343
+ 'streaming.preferNativeHls instead.');
3344
+ }
3345
+
3346
+ // Deprecate 'mediaSource.sourceBufferExtraFeatures' configuration.
3347
+ if (config['mediaSource'] &&
3348
+ 'sourceBufferExtraFeatures' in config['mediaSource']) {
3349
+ shaka.Deprecate.deprecateFeature(5,
3350
+ 'mediaSource.sourceBufferExtraFeatures configuration',
3351
+ 'Please Use mediaSource.addExtraFeaturesToSourceBuffer() instead.');
3352
+ const sourceBufferExtraFeatures =
3353
+ config['mediaSource']['sourceBufferExtraFeatures'];
3354
+ config['mediaSource']['addExtraFeaturesToSourceBuffer'] = () => {
3355
+ return sourceBufferExtraFeatures;
3356
+ };
3357
+ delete config['mediaSource']['sourceBufferExtraFeatures'];
3358
+ }
3359
+
3018
3360
  // If lowLatencyMode is enabled, and inaccurateManifestTolerance and
3019
3361
  // rebufferingGoal and segmentPrefetchLimit and baseDelay are not
3020
3362
  // specified, set inaccurateManifestTolerance to 0 and rebufferingGoal
3021
- // to 0.01 and segmentPrefetchLimit to 2 and baseDelay to 100 by default
3022
- // for low latency streaming.
3363
+ // to 0.01 and segmentPrefetchLimit to 2 and updateIntervalSeconds to
3364
+ // 0.1 and baseDelay to 100 by default for low latency streaming.
3023
3365
  if (config['streaming'] && config['streaming']['lowLatencyMode']) {
3024
3366
  if (config['streaming']['inaccurateManifestTolerance'] == undefined) {
3025
3367
  config['streaming']['inaccurateManifestTolerance'] = 0;
@@ -3030,6 +3372,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3030
3372
  if (config['streaming']['segmentPrefetchLimit'] == undefined) {
3031
3373
  config['streaming']['segmentPrefetchLimit'] = 2;
3032
3374
  }
3375
+ if (config['streaming']['updateIntervalSeconds'] == undefined) {
3376
+ config['streaming']['updateIntervalSeconds'] = 0.1;
3377
+ }
3033
3378
  if (config['streaming']['retryParameters'] == undefined) {
3034
3379
  config['streaming']['retryParameters'] = {};
3035
3380
  }
@@ -3067,6 +3412,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3067
3412
  * @private
3068
3413
  */
3069
3414
  applyConfig_() {
3415
+ this.manifestFilterer_ = new shaka.media.ManifestFilterer(
3416
+ this.config_, this.maxHwRes_, this.drmEngine_);
3070
3417
  if (this.parser_) {
3071
3418
  const manifestConfig =
3072
3419
  shaka.util.ObjectUtils.cloneObject(this.config_.manifest);
@@ -3083,9 +3430,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3083
3430
  this.streamingEngine_.configure(this.config_.streaming);
3084
3431
 
3085
3432
  // Need to apply the restrictions.
3433
+ // this.filterManifestWithRestrictions_() may throw.
3086
3434
  try {
3087
- // this.filterManifestWithRestrictions_() may throw.
3088
- this.filterManifestWithRestrictions_(this.manifest_);
3435
+ if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) {
3436
+ if (this.manifestFilterer_.filterManifestWithRestrictions(
3437
+ this.manifest_)) {
3438
+ this.onTracksChanged_();
3439
+ }
3440
+ }
3089
3441
  } catch (error) {
3090
3442
  this.onError_(error);
3091
3443
  }
@@ -3118,6 +3470,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3118
3470
  const textDisplayerFactory = this.config_.textDisplayFactory;
3119
3471
  if (this.lastTextFactory_ != textDisplayerFactory) {
3120
3472
  const displayer = textDisplayerFactory();
3473
+ if (displayer.configure) {
3474
+ displayer.configure(this.config_.textDisplayer);
3475
+ } else {
3476
+ shaka.Deprecate.deprecateFeature(5,
3477
+ 'Text displayer w/ configure',
3478
+ 'Text displayer should have a "configure" method!');
3479
+ }
3121
3480
  this.mediaSourceEngine_.setTextDisplayer(displayer);
3122
3481
  this.lastTextFactory_ = textDisplayerFactory;
3123
3482
 
@@ -3125,6 +3484,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3125
3484
  // Reload the text stream, so the cues will load again.
3126
3485
  this.streamingEngine_.reloadTextStream();
3127
3486
  }
3487
+ } else {
3488
+ const displayer = this.mediaSourceEngine_.getTextDisplayer();
3489
+ if (displayer.configure) {
3490
+ displayer.configure(this.config_.textDisplayer);
3491
+ }
3128
3492
  }
3129
3493
  }
3130
3494
  if (this.abrManager_) {
@@ -3394,24 +3758,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3394
3758
  }
3395
3759
  }
3396
3760
 
3397
- /**
3398
- * Return the value of lowLatencyMode configuration.
3399
- * @return {boolean}
3400
- * @private
3401
- */
3402
- isLowLatencyMode_() {
3403
- return this.config_.streaming.lowLatencyMode;
3404
- }
3405
-
3406
- /**
3407
- * Return the value of autoLowLatencyMode configuration.
3408
- * @return {boolean}
3409
- * @private
3410
- */
3411
- isAutoLowLatencyMode_() {
3412
- return this.config_.streaming.autoLowLatencyMode;
3413
- }
3414
-
3415
3761
  /**
3416
3762
  * Get the range of time (in seconds) that seeking is allowed. If the player
3417
3763
  * has not loaded content and the manifest is HLS, this will return a range
@@ -3620,12 +3966,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
3620
3966
  const currentTime = this.video_.currentTime;
3621
3967
  const seekRange = this.seekRange();
3622
3968
  const safeSeekOffset = this.config_.streaming.safeSeekOffset;
3969
+
3970
+ // Cancel trick play if we hit the beginning or end of the seekable
3971
+ // (Sub-second accuracy not required here)
3623
3972
  if (rate > 0) {
3624
- if (currentTime >= seekRange.end) {
3973
+ if (Math.floor(currentTime) >= Math.floor(seekRange.end)) {
3625
3974
  this.cancelTrickPlay();
3626
3975
  }
3627
3976
  } else {
3628
- if (currentTime <= (seekRange.start + safeSeekOffset)) {
3977
+ if (Math.floor(currentTime) <=
3978
+ Math.floor(seekRange.start + safeSeekOffset)) {
3629
3979
  this.cancelTrickPlay();
3630
3980
  }
3631
3981
  }
@@ -4465,19 +4815,12 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4465
4815
  }
4466
4816
 
4467
4817
  /**
4468
- * Get the current playhead position as a date. This should only be called
4469
- * when the player has loaded a live stream. If the player has not loaded a
4470
- * live stream, this will return <code>null</code>.
4818
+ * Get the current playhead position as a date.
4471
4819
  *
4472
4820
  * @return {Date}
4473
4821
  * @export
4474
4822
  */
4475
4823
  getPlayheadTimeAsDate() {
4476
- if (!this.isLive()) {
4477
- shaka.log.warning('getPlayheadTimeAsDate is for live streams!');
4478
- return null;
4479
- }
4480
-
4481
4824
  let presentationTime = 0;
4482
4825
  if (this.playhead_) {
4483
4826
  presentationTime = this.playhead_.getTime();
@@ -4514,22 +4857,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4514
4857
  }
4515
4858
 
4516
4859
  /**
4517
- * Get the presentation start time as a date. This should only be called when
4518
- * the player has loaded a live stream. If the player has not loaded a live
4519
- * stream, this will return <code>null</code>.
4860
+ * Get the presentation start time as a date.
4520
4861
  *
4521
4862
  * @return {Date}
4522
4863
  * @export
4523
4864
  */
4524
4865
  getPresentationStartTimeAsDate() {
4525
- if (!this.isLive()) {
4526
- shaka.log.warning('getPresentationStartTimeAsDate is for live streams!');
4527
- return null;
4528
- }
4529
-
4530
4866
  if (this.manifest_) {
4531
4867
  const timeline = this.manifest_.presentationTimeline;
4532
- const startTime = timeline.getPresentationStartTime();
4868
+ const startTime = timeline.getInitialProgramDateTime() ||
4869
+ timeline.getPresentationStartTime();
4533
4870
  goog.asserts.assert(startTime != null,
4534
4871
  'Presentation start time should not be null!');
4535
4872
  return new Date(/* ms= */ startTime * 1000);
@@ -4550,6 +4887,29 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4550
4887
  }
4551
4888
  }
4552
4889
 
4890
+ /**
4891
+ * Get the presentation segment availability duration. This should only be
4892
+ * called when the player has loaded a live stream. If the player has not
4893
+ * loaded a live stream, this will return <code>null</code>.
4894
+ *
4895
+ * @return {?number}
4896
+ * @export
4897
+ */
4898
+ getSegmentAvailabilityDuration() {
4899
+ if (!this.isLive()) {
4900
+ shaka.log.warning('getSegmentAvailabilityDuration is for live streams!');
4901
+ return null;
4902
+ }
4903
+
4904
+ if (this.manifest_) {
4905
+ const timeline = this.manifest_.presentationTimeline;
4906
+ return timeline.getSegmentAvailabilityDuration();
4907
+ } else {
4908
+ shaka.log.warning('No way to get segment segment availability duration!');
4909
+ return null;
4910
+ }
4911
+ }
4912
+
4553
4913
  /**
4554
4914
  * Get information about what the player has buffered. If the player has not
4555
4915
  * loaded content or is currently loading content, the buffered content will
@@ -4804,6 +5164,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4804
5164
  accessibilityPurpose: null,
4805
5165
  external: true,
4806
5166
  fastSwitching: false,
5167
+ fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
5168
+ mimeType || '', codec || '')]),
4807
5169
  };
4808
5170
 
4809
5171
  const fullMimeType = shaka.util.MimeUtils.getFullType(
@@ -4960,6 +5322,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
4960
5322
  accessibilityPurpose: null,
4961
5323
  external: true,
4962
5324
  fastSwitching: false,
5325
+ fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
5326
+ mimeType || '', '')]),
4963
5327
  };
4964
5328
 
4965
5329
  if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
@@ -5170,8 +5534,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5170
5534
 
5171
5535
  /**
5172
5536
  * Set the maximum resolution that the platform's hardware can handle.
5173
- * This will be called automatically by <code>shaka.cast.CastReceiver</code>
5174
- * to enforce limitations of the Chromecast hardware.
5175
5537
  *
5176
5538
  * @param {number} width
5177
5539
  * @param {number} height
@@ -5274,8 +5636,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5274
5636
  // TextDisplay factory must capture a reference to "this".
5275
5637
  config.textDisplayFactory = () => {
5276
5638
  if (this.videoContainer_) {
5639
+ const latestConfig = this.getConfiguration();
5277
5640
  return new shaka.text.UITextDisplayer(
5278
- this.video_, this.videoContainer_);
5641
+ this.video_, this.videoContainer_, latestConfig.textDisplayer);
5279
5642
  } else {
5280
5643
  // eslint-disable-next-line no-restricted-syntax
5281
5644
  if (HTMLMediaElement.prototype.addTextTrack) {
@@ -5390,6 +5753,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5390
5753
  accessibilityPurpose: null,
5391
5754
  external: false,
5392
5755
  fastSwitching: false,
5756
+ fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
5757
+ mimeType, '')]),
5393
5758
  };
5394
5759
  manifest.textStreams.push(textStream);
5395
5760
  closedCaptionsSet.add(id);
@@ -5399,82 +5764,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5399
5764
  }
5400
5765
  }
5401
5766
 
5402
- /**
5403
- * Filters a manifest, removing unplayable streams/variants.
5404
- *
5405
- * @param {?shaka.extern.Manifest} manifest
5406
- * @private
5407
- */
5408
- async filterManifest_(manifest) {
5409
- await this.filterManifestWithStreamUtils_(manifest);
5410
- this.filterManifestWithRestrictions_(manifest);
5411
- }
5412
-
5413
- /**
5414
- * Filters a manifest, removing unplayable streams/variants.
5415
- *
5416
- * @param {?shaka.extern.Manifest} manifest
5417
- * @private
5418
- */
5419
- async filterManifestWithStreamUtils_(manifest) {
5420
- goog.asserts.assert(manifest, 'Manifest should exist!');
5421
- await shaka.util.StreamUtils.filterManifest(this.drmEngine_, manifest);
5422
- this.checkPlayableVariants_(manifest);
5423
- }
5424
-
5425
-
5426
- /**
5427
- * Apply the restrictions configuration to the manifest, and check if there's
5428
- * a variant that meets the restrictions.
5429
- *
5430
- * @param {?shaka.extern.Manifest} manifest
5431
- * @private
5432
- */
5433
- filterManifestWithRestrictions_(manifest) {
5434
- // Return if |destroy| is called.
5435
- if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
5436
- return;
5437
- }
5438
- const tracksChanged = shaka.util.StreamUtils.applyRestrictions(
5439
- manifest.variants, this.config_.restrictions, this.maxHwRes_);
5440
- if (tracksChanged && this.streamingEngine_) {
5441
- this.onTracksChanged_();
5442
- }
5443
-
5444
- // We may need to create new sessions for any new init data.
5445
- const currentDrmInfo =
5446
- this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
5447
- // DrmEngine.newInitData() requires mediaKeys to be available.
5448
- if (currentDrmInfo && this.drmEngine_.getMediaKeys()) {
5449
- for (const variant of manifest.variants) {
5450
- this.processDrmInfos_(currentDrmInfo.keySystem, variant.video);
5451
- this.processDrmInfos_(currentDrmInfo.keySystem, variant.audio);
5452
- }
5453
- }
5454
- this.checkRestrictedVariants_(manifest);
5455
- }
5456
-
5457
- /**
5458
- * @param {string} keySystem
5459
- * @param {?shaka.extern.Stream} stream
5460
- * @private
5461
- */
5462
- processDrmInfos_(keySystem, stream) {
5463
- if (!stream) {
5464
- return;
5465
- }
5466
-
5467
- for (const drmInfo of stream.drmInfos) {
5468
- // Ignore any data for different key systems.
5469
- if (drmInfo.keySystem == keySystem) {
5470
- for (const initData of (drmInfo.initData || [])) {
5471
- this.drmEngine_.newInitData(
5472
- initData.initDataType, initData.initData);
5473
- }
5474
- }
5475
- }
5476
- }
5477
-
5478
5767
  /**
5479
5768
  * @param {shaka.extern.Variant} initialVariant
5480
5769
  * @param {number} time
@@ -5553,7 +5842,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5553
5842
  // buffering.
5554
5843
  const eventName = shaka.util.FakeEvent.EventName.Buffering;
5555
5844
  const data = (new Map()).set('buffering', isBuffering);
5556
- this.dispatchEvent(this.makeEvent_(eventName, data));
5845
+ this.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
5557
5846
  }
5558
5847
 
5559
5848
  /**
@@ -5586,7 +5875,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5586
5875
  this.playRateController_.set(newRate);
5587
5876
  }
5588
5877
 
5589
- const event = this.makeEvent_(shaka.util.FakeEvent.EventName.RateChange);
5878
+ const event = shaka.Player.makeEvent_(
5879
+ shaka.util.FakeEvent.EventName.RateChange);
5590
5880
  this.dispatchEvent(event);
5591
5881
  }
5592
5882
 
@@ -5622,20 +5912,46 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5622
5912
  if (stateChanged) {
5623
5913
  const eventName = shaka.util.FakeEvent.EventName.StateChanged;
5624
5914
  const data = (new Map()).set('newstate', updateState);
5625
- this.dispatchEvent(this.makeEvent_(eventName, data));
5915
+ this.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
5626
5916
  }
5627
5917
  }
5628
5918
 
5629
5919
  /**
5630
- * Callback for liveSync
5920
+ * Callback for liveSync and vodDynamicPlaybackRate
5631
5921
  *
5632
5922
  * @private
5633
5923
  */
5634
5924
  onTimeUpdate_() {
5925
+ const playbackRate = this.video_.playbackRate;
5926
+ const isLive = this.isLive();
5927
+
5928
+ if (this.config_.streaming.vodDynamicPlaybackRate && !isLive) {
5929
+ const minPlaybackRate =
5930
+ this.config_.streaming.vodDynamicPlaybackRateLowBufferRate;
5931
+ const bufferFullness = this.getBufferFullness();
5932
+ const bufferThreshold =
5933
+ this.config_.streaming.vodDynamicPlaybackRateBufferRatio;
5934
+
5935
+ if (bufferFullness <= bufferThreshold) {
5936
+ if (playbackRate != minPlaybackRate) {
5937
+ shaka.log.debug('Buffer fullness ratio (' + bufferFullness + ') ' +
5938
+ 'is less than the vodDynamicPlaybackRateBufferRatio (' +
5939
+ bufferThreshold + '). Updating playbackRate to ' + minPlaybackRate);
5940
+ this.trickPlay(minPlaybackRate);
5941
+ }
5942
+ } else if (bufferFullness == 1) {
5943
+ if (playbackRate !== this.playRateController_.getDefaultRate()) {
5944
+ shaka.log.debug('Buffer is full. Cancel trick play.');
5945
+ this.cancelTrickPlay();
5946
+ }
5947
+ }
5948
+ }
5949
+
5635
5950
  // If the live stream has reached its end, do not sync.
5636
- if (!this.isLive()) {
5951
+ if (!isLive) {
5637
5952
  return;
5638
5953
  }
5954
+
5639
5955
  const seekRange = this.seekRange();
5640
5956
  if (!Number.isFinite(seekRange.end)) {
5641
5957
  return;
@@ -5680,7 +5996,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5680
5996
  }
5681
5997
  }
5682
5998
 
5683
- const playbackRate = this.video_.playbackRate;
5684
5999
  const latency = seekRange.end - this.video_.currentTime;
5685
6000
  let offset = 0;
5686
6001
  // In src= mode, the seek range isn't updated frequently enough, so we need
@@ -5695,7 +6010,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5695
6010
  }
5696
6011
  }
5697
6012
 
5698
- if (liveSyncMaxLatency && liveSyncPlaybackRate &&
6013
+ const panicMode = this.config_.streaming.liveSyncPanicMode;
6014
+ const panicThreshold = this.config_.streaming.liveSyncPanicThreshold * 1000;
6015
+ const timeSinceLastRebuffer =
6016
+ Date.now() - this.bufferObserver_.getLastRebufferTime();
6017
+ if (panicMode && !liveSyncMinPlaybackRate) {
6018
+ liveSyncMinPlaybackRate = this.config_.streaming.liveSyncMinPlaybackRate;
6019
+ }
6020
+
6021
+ if (panicMode && liveSyncMinPlaybackRate &&
6022
+ timeSinceLastRebuffer <= panicThreshold) {
6023
+ if (playbackRate != liveSyncMinPlaybackRate) {
6024
+ shaka.log.debug('Time since last rebuffer (' +
6025
+ timeSinceLastRebuffer + 's) ' +
6026
+ 'is less than the liveSyncPanicThreshold (' + panicThreshold +
6027
+ 's). Updating playbackRate to ' + liveSyncMinPlaybackRate);
6028
+ this.trickPlay(liveSyncMinPlaybackRate);
6029
+ }
6030
+ } else if (liveSyncMaxLatency && liveSyncPlaybackRate &&
5699
6031
  (latency - offset) > liveSyncMaxLatency) {
5700
6032
  if (playbackRate != liveSyncPlaybackRate) {
5701
6033
  shaka.log.debug('Latency (' + latency + 's) ' +
@@ -5743,15 +6075,19 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5743
6075
  if (hasNewCompletionPercent) {
5744
6076
  let event;
5745
6077
  if (this.completionPercent_ == 0) {
5746
- event = this.makeEvent_(shaka.util.FakeEvent.EventName.Started);
6078
+ event = shaka.Player.makeEvent_(shaka.util.FakeEvent.EventName.Started);
5747
6079
  } else if (this.completionPercent_ == 25) {
5748
- event = this.makeEvent_(shaka.util.FakeEvent.EventName.FirstQuartile);
6080
+ event = shaka.Player.makeEvent_(
6081
+ shaka.util.FakeEvent.EventName.FirstQuartile);
5749
6082
  } else if (this.completionPercent_ == 50) {
5750
- event = this.makeEvent_(shaka.util.FakeEvent.EventName.Midpoint);
6083
+ event = shaka.Player.makeEvent_(
6084
+ shaka.util.FakeEvent.EventName.Midpoint);
5751
6085
  } else if (this.completionPercent_ == 75) {
5752
- event = this.makeEvent_(shaka.util.FakeEvent.EventName.ThirdQuartile);
6086
+ event = shaka.Player.makeEvent_(
6087
+ shaka.util.FakeEvent.EventName.ThirdQuartile);
5753
6088
  } else if (this.completionPercent_ == 100) {
5754
- event = this.makeEvent_(shaka.util.FakeEvent.EventName.Complete);
6089
+ event = shaka.Player.makeEvent_(
6090
+ shaka.util.FakeEvent.EventName.Complete);
5755
6091
  }
5756
6092
  if (event) {
5757
6093
  this.dispatchEvent(event);
@@ -5791,7 +6127,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5791
6127
  updateAbrManagerVariants_() {
5792
6128
  try {
5793
6129
  goog.asserts.assert(this.manifest_, 'Manifest should exist by now!');
5794
- this.checkRestrictedVariants_(this.manifest_);
6130
+ this.manifestFilterer_.checkRestrictedVariants(this.manifest_);
5795
6131
  } catch (e) {
5796
6132
  this.onError_(e);
5797
6133
  return false;
@@ -6120,7 +6456,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6120
6456
  .set('start', start)
6121
6457
  .set('end', end)
6122
6458
  .set('contentType', contentType);
6123
- this.dispatchEvent(this.makeEvent_(
6459
+ this.dispatchEvent(shaka.Player.makeEvent_(
6124
6460
  shaka.util.FakeEvent.EventName.SegmentAppended, data));
6125
6461
  }
6126
6462
 
@@ -6170,8 +6506,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6170
6506
  if (this.lcevcDec_) {
6171
6507
  this.lcevcDec_.updateVariant(to, this.getManifestType());
6172
6508
  }
6173
- const event =
6174
- this.makeEvent_(shaka.util.FakeEvent.EventName.Adaptation, data);
6509
+ const event = shaka.Player.makeEvent_(
6510
+ shaka.util.FakeEvent.EventName.Adaptation, data);
6175
6511
  this.delayDispatchEvent_(event);
6176
6512
  }
6177
6513
 
@@ -6182,7 +6518,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6182
6518
  onTracksChanged_() {
6183
6519
  // Delay the 'trackschanged' event so StreamingEngine has time to absorb the
6184
6520
  // changes before the user tries to query it.
6185
- const event = this.makeEvent_(shaka.util.FakeEvent.EventName.TracksChanged);
6521
+ const event = shaka.Player.makeEvent_(
6522
+ shaka.util.FakeEvent.EventName.TracksChanged);
6186
6523
  this.delayDispatchEvent_(event);
6187
6524
  }
6188
6525
 
@@ -6202,8 +6539,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6202
6539
  this.lcevcDec_.updateVariant(to, this.getManifestType());
6203
6540
  }
6204
6541
 
6205
- const event =
6206
- this.makeEvent_(shaka.util.FakeEvent.EventName.VariantChanged, data);
6542
+ const event = shaka.Player.makeEvent_(
6543
+ shaka.util.FakeEvent.EventName.VariantChanged, data);
6207
6544
  this.delayDispatchEvent_(event);
6208
6545
  }
6209
6546
 
@@ -6214,14 +6551,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6214
6551
  onTextChanged_() {
6215
6552
  // Delay the 'textchanged' event so StreamingEngine time to absorb the
6216
6553
  // changes before the user tries to query it.
6217
- const event = this.makeEvent_(shaka.util.FakeEvent.EventName.TextChanged);
6554
+ const event = shaka.Player.makeEvent_(
6555
+ shaka.util.FakeEvent.EventName.TextChanged);
6218
6556
  this.delayDispatchEvent_(event);
6219
6557
  }
6220
6558
 
6221
6559
  /** @private */
6222
6560
  onTextTrackVisibility_() {
6223
- const event =
6224
- this.makeEvent_(shaka.util.FakeEvent.EventName.TextTrackVisibility);
6561
+ const event = shaka.Player.makeEvent_(
6562
+ shaka.util.FakeEvent.EventName.TextTrackVisibility);
6225
6563
  this.delayDispatchEvent_(event);
6226
6564
  }
6227
6565
 
@@ -6233,7 +6571,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6233
6571
  }
6234
6572
 
6235
6573
  const data = (new Map()).set('newStatus', this.config_.abr.enabled);
6236
- this.delayDispatchEvent_(this.makeEvent_(
6574
+ this.delayDispatchEvent_(shaka.Player.makeEvent_(
6237
6575
  shaka.util.FakeEvent.EventName.AbrStatusChanged, data));
6238
6576
  }
6239
6577
 
@@ -6367,7 +6705,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6367
6705
  }
6368
6706
 
6369
6707
  const eventName = shaka.util.FakeEvent.EventName.Error;
6370
- const event = this.makeEvent_(eventName, (new Map()).set('detail', error));
6708
+ const event = shaka.Player.makeEvent_(
6709
+ eventName, (new Map()).set('detail', error));
6371
6710
  this.dispatchEvent(event);
6372
6711
  if (event.defaultPrevented) {
6373
6712
  error.handled = true;
@@ -6382,10 +6721,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6382
6721
  *
6383
6722
  * @param {!shaka.util.FakeEvent.EventName} eventName
6384
6723
  * @param {shaka.extern.TimelineRegionInfo} region
6724
+ * @param {shaka.util.FakeEventTarget=} eventTarget
6385
6725
  *
6386
6726
  * @private
6387
6727
  */
6388
- onRegionEvent_(eventName, region) {
6728
+ onRegionEvent_(eventName, region, eventTarget = this) {
6389
6729
  // Always make a copy to avoid exposing our internal data to the app.
6390
6730
  const clone = {
6391
6731
  schemeIdUri: region.schemeIdUri,
@@ -6394,10 +6734,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6394
6734
  endTime: region.endTime,
6395
6735
  id: region.id,
6396
6736
  eventElement: region.eventElement,
6737
+ eventNode: region.eventNode,
6397
6738
  };
6398
6739
 
6399
6740
  const data = (new Map()).set('detail', clone);
6400
- this.dispatchEvent(this.makeEvent_(eventName, data));
6741
+ eventTarget.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
6401
6742
  }
6402
6743
 
6403
6744
  /**
@@ -6428,7 +6769,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6428
6769
  .set('mediaQuality', clone)
6429
6770
  .set('position', position);
6430
6771
 
6431
- this.dispatchEvent(this.makeEvent_(
6772
+ this.dispatchEvent(shaka.Player.makeEvent_(
6432
6773
  shaka.util.FakeEvent.EventName.MediaQualityChanged, data));
6433
6774
  }
6434
6775
 
@@ -6491,14 +6832,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6491
6832
  * @private
6492
6833
  */
6493
6834
  onKeyStatus_(keyStatusMap) {
6494
- if (!this.streamingEngine_) {
6495
- // We can't use this info to manage restrictions in src= mode, so ignore
6496
- // it.
6497
- return;
6498
- }
6835
+ goog.asserts.assert(this.streamingEngine_, 'Cannot be called in src= mode');
6499
6836
 
6500
- const event =
6501
- this.makeEvent_(shaka.util.FakeEvent.EventName.KeyStatusChanged);
6837
+ const event = shaka.Player.makeEvent_(
6838
+ shaka.util.FakeEvent.EventName.KeyStatusChanged);
6502
6839
  this.dispatchEvent(event);
6503
6840
 
6504
6841
  const keyIds = Object.keys(keyStatusMap);
@@ -6520,7 +6857,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6520
6857
  'restrictions so we don\'t select those tracks.');
6521
6858
  }
6522
6859
 
6523
- const restrictedStatuses = shaka.Player.restrictedStatuses_;
6860
+ const restrictedStatuses = shaka.media.ManifestFilterer.restrictedStatuses;
6524
6861
  let tracksChanged = false;
6525
6862
 
6526
6863
  goog.asserts.assert(this.drmEngine_, 'drmEngine should be non-null here.');
@@ -6569,22 +6906,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6569
6906
  }
6570
6907
  }
6571
6908
 
6572
- /**
6573
- * Callback from DrmEngine
6574
- * @param {string} keyId
6575
- * @param {number} expiration
6576
- * @private
6577
- */
6578
- onExpirationUpdated_(keyId, expiration) {
6579
- if (this.parser_ && this.parser_.onExpirationUpdated) {
6580
- this.parser_.onExpirationUpdated(keyId, expiration);
6581
- }
6582
-
6583
- const event =
6584
- this.makeEvent_(shaka.util.FakeEvent.EventName.ExpirationUpdated);
6585
- this.dispatchEvent(event);
6586
- }
6587
-
6588
6909
  /**
6589
6910
  * @return {boolean} true if we should stream text right now.
6590
6911
  * @private
@@ -6628,93 +6949,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
6628
6949
  }
6629
6950
  }
6630
6951
 
6631
- /**
6632
- * Checks if the variants are all restricted, and throw an appropriate
6633
- * exception if so.
6634
- *
6635
- * @param {shaka.extern.Manifest} manifest
6636
- *
6637
- * @private
6638
- */
6639
- checkRestrictedVariants_(manifest) {
6640
- const restrictedStatuses = shaka.Player.restrictedStatuses_;
6641
- const keyStatusMap =
6642
- this.drmEngine_ ? this.drmEngine_.getKeyStatuses() : {};
6643
- const keyIds = Object.keys(keyStatusMap);
6644
- const isGlobalStatus = keyIds.length && keyIds[0] == '00';
6645
-
6646
- let hasPlayable = false;
6647
- let hasAppRestrictions = false;
6648
-
6649
- /** @type {!Set.<string>} */
6650
- const missingKeys = new Set();
6651
-
6652
- /** @type {!Set.<string>} */
6653
- const badKeyStatuses = new Set();
6654
-
6655
- for (const variant of manifest.variants) {
6656
- // TODO: Combine with onKeyStatus_.
6657
- const streams = [];
6658
- if (variant.audio) {
6659
- streams.push(variant.audio);
6660
- }
6661
- if (variant.video) {
6662
- streams.push(variant.video);
6663
- }
6664
-
6665
- for (const stream of streams) {
6666
- if (stream.keyIds.size) {
6667
- for (const keyId of stream.keyIds) {
6668
- const keyStatus = keyStatusMap[isGlobalStatus ? '00' : keyId];
6669
- if (!keyStatus) {
6670
- missingKeys.add(keyId);
6671
- } else if (restrictedStatuses.includes(keyStatus)) {
6672
- badKeyStatuses.add(keyStatus);
6673
- }
6674
- }
6675
- } // if (stream.keyIds.size)
6676
- }
6677
-
6678
- if (!variant.allowedByApplication) {
6679
- hasAppRestrictions = true;
6680
- } else if (variant.allowedByKeySystem) {
6681
- hasPlayable = true;
6682
- }
6683
- }
6684
-
6685
- if (!hasPlayable) {
6686
- /** @type {shaka.extern.RestrictionInfo} */
6687
- const data = {
6688
- hasAppRestrictions,
6689
- missingKeys: Array.from(missingKeys),
6690
- restrictedKeyStatuses: Array.from(badKeyStatuses),
6691
- };
6692
- throw new shaka.util.Error(
6693
- shaka.util.Error.Severity.CRITICAL,
6694
- shaka.util.Error.Category.MANIFEST,
6695
- shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET,
6696
- data);
6697
- }
6698
- }
6699
-
6700
- /**
6701
- * Confirm some variants are playable. Otherwise, throw an exception.
6702
- * @param {!shaka.extern.Manifest} manifest
6703
- * @private
6704
- */
6705
- checkPlayableVariants_(manifest) {
6706
- const valid = manifest.variants.some(shaka.util.StreamUtils.isPlayable);
6707
-
6708
- // If none of the variants are playable, throw
6709
- // CONTENT_UNSUPPORTED_BY_BROWSER.
6710
- if (!valid) {
6711
- throw new shaka.util.Error(
6712
- shaka.util.Error.Severity.CRITICAL,
6713
- shaka.util.Error.Category.MANIFEST,
6714
- shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER);
6715
- }
6716
- }
6717
-
6718
6952
  /**
6719
6953
  * Fire an event, but wait a little bit so that the immediate execution can
6720
6954
  * complete before the event is handled.
@@ -6947,25 +7181,13 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6947
7181
  * @export
6948
7182
  */
6949
7183
  // eslint-disable-next-line no-useless-concat
6950
- shaka.Player.version = 'v4.7.12' + '-uncompiled'; // x-release-please-version
7184
+ shaka.Player.version = 'v4.8.0' + '-uncompiled'; // x-release-please-version
6951
7185
 
6952
7186
  // Initialize the deprecation system using the version string we just set
6953
7187
  // on the player.
6954
7188
  shaka.Deprecate.init(shaka.Player.version);
6955
7189
 
6956
7190
 
6957
- /**
6958
- * These are the EME key statuses that represent restricted playback.
6959
- * 'usable', 'released', 'output-downscaled', 'status-pending' are statuses
6960
- * of the usable keys. 'expired' status is being handled separately in
6961
- * DrmEngine.
6962
- *
6963
- * @const {!Array.<string>}
6964
- * @private
6965
- */
6966
- shaka.Player.restrictedStatuses_ = ['output-restricted', 'internal-error'];
6967
-
6968
-
6969
7191
  /** @private {!Object.<string, function():*>} */
6970
7192
  shaka.Player.supportPlugins_ = {};
6971
7193