Source: lib/player.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.Player');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.config.AutoShowText');
  9. goog.require('shaka.Deprecate');
  10. goog.require('shaka.log');
  11. goog.require('shaka.media.AdaptationSetCriteria');
  12. goog.require('shaka.media.BufferingObserver');
  13. goog.require('shaka.media.DrmEngine');
  14. goog.require('shaka.media.ExampleBasedCriteria');
  15. goog.require('shaka.media.ManifestFilterer');
  16. goog.require('shaka.media.ManifestParser');
  17. goog.require('shaka.media.MediaSourceEngine');
  18. goog.require('shaka.media.MediaSourcePlayhead');
  19. goog.require('shaka.media.MetaSegmentIndex');
  20. goog.require('shaka.media.PlayRateController');
  21. goog.require('shaka.media.Playhead');
  22. goog.require('shaka.media.PlayheadObserverManager');
  23. goog.require('shaka.media.PreferenceBasedCriteria');
  24. goog.require('shaka.media.PreloadManager');
  25. goog.require('shaka.media.QualityObserver');
  26. goog.require('shaka.media.RegionObserver');
  27. goog.require('shaka.media.RegionTimeline');
  28. goog.require('shaka.media.SegmentIndex');
  29. goog.require('shaka.media.SegmentPrefetch');
  30. goog.require('shaka.media.SegmentReference');
  31. goog.require('shaka.media.SrcEqualsPlayhead');
  32. goog.require('shaka.media.StreamingEngine');
  33. goog.require('shaka.media.TimeRangesUtils');
  34. goog.require('shaka.net.NetworkingEngine');
  35. goog.require('shaka.net.NetworkingUtils');
  36. goog.require('shaka.text.SimpleTextDisplayer');
  37. goog.require('shaka.text.StubTextDisplayer');
  38. goog.require('shaka.text.TextEngine');
  39. goog.require('shaka.text.UITextDisplayer');
  40. goog.require('shaka.text.WebVttGenerator');
  41. goog.require('shaka.util.BufferUtils');
  42. goog.require('shaka.util.CmcdManager');
  43. goog.require('shaka.util.CmsdManager');
  44. goog.require('shaka.util.ConfigUtils');
  45. goog.require('shaka.util.Dom');
  46. goog.require('shaka.util.Error');
  47. goog.require('shaka.util.EventManager');
  48. goog.require('shaka.util.FakeEvent');
  49. goog.require('shaka.util.FakeEventTarget');
  50. goog.require('shaka.util.IDestroyable');
  51. goog.require('shaka.util.LanguageUtils');
  52. goog.require('shaka.util.ManifestParserUtils');
  53. goog.require('shaka.util.MediaReadyState');
  54. goog.require('shaka.util.MimeUtils');
  55. goog.require('shaka.util.Mutex');
  56. goog.require('shaka.util.ObjectUtils');
  57. goog.require('shaka.util.Platform');
  58. goog.require('shaka.util.PlayerConfiguration');
  59. goog.require('shaka.util.PublicPromise');
  60. goog.require('shaka.util.Stats');
  61. goog.require('shaka.util.StreamUtils');
  62. goog.require('shaka.util.Timer');
  63. goog.require('shaka.lcevc.Dec');
  64. goog.requireType('shaka.media.PresentationTimeline');
  65. /**
  66. * @event shaka.Player.ErrorEvent
  67. * @description Fired when a playback error occurs.
  68. * @property {string} type
  69. * 'error'
  70. * @property {!shaka.util.Error} detail
  71. * An object which contains details on the error. The error's
  72. * <code>category</code> and <code>code</code> properties will identify the
  73. * specific error that occurred. In an uncompiled build, you can also use the
  74. * <code>message</code> and <code>stack</code> properties to debug.
  75. * @exportDoc
  76. */
  77. /**
  78. * @event shaka.Player.StateChangeEvent
  79. * @description Fired when the player changes load states.
  80. * @property {string} type
  81. * 'onstatechange'
  82. * @property {string} state
  83. * The name of the state that the player just entered.
  84. * @exportDoc
  85. */
  86. /**
  87. * @event shaka.Player.EmsgEvent
  88. * @description Fired when an emsg box is found in a segment.
  89. * If the application calls preventDefault() on this event, further parsing
  90. * will not happen, and no 'metadata' event will be raised for ID3 payloads.
  91. * @property {string} type
  92. * 'emsg'
  93. * @property {shaka.extern.EmsgInfo} detail
  94. * An object which contains the content of the emsg box.
  95. * @exportDoc
  96. */
  97. /**
  98. * @event shaka.Player.DownloadFailed
  99. * @description Fired when a download has failed, for any reason.
  100. * 'downloadfailed'
  101. * @property {!shaka.extern.Request} request
  102. * @property {?shaka.util.Error} error
  103. * @param {number} httpResponseCode
  104. * @param {boolean} aborted
  105. * @exportDoc
  106. */
  107. /**
  108. * @event shaka.Player.DownloadHeadersReceived
  109. * @description Fired when the networking engine has received the headers for
  110. * a download, but before the body has been downloaded.
  111. * If the HTTP plugin being used does not track this information, this event
  112. * will default to being fired when the body is received, instead.
  113. * @property {!Object.<string, string>} headers
  114. * @property {!shaka.extern.Request} request
  115. * @property {!shaka.net.NetworkingEngine.RequestType} type
  116. * 'downloadheadersreceived'
  117. * @exportDoc
  118. */
  119. /**
  120. * @event shaka.Player.DrmSessionUpdateEvent
  121. * @description Fired when the CDM has accepted the license response.
  122. * @property {string} type
  123. * 'drmsessionupdate'
  124. * @exportDoc
  125. */
  126. /**
  127. * @event shaka.Player.TimelineRegionAddedEvent
  128. * @description Fired when a media timeline region is added.
  129. * @property {string} type
  130. * 'timelineregionadded'
  131. * @property {shaka.extern.TimelineRegionInfo} detail
  132. * An object which contains a description of the region.
  133. * @exportDoc
  134. */
  135. /**
  136. * @event shaka.Player.TimelineRegionEnterEvent
  137. * @description Fired when the playhead enters a timeline region.
  138. * @property {string} type
  139. * 'timelineregionenter'
  140. * @property {shaka.extern.TimelineRegionInfo} detail
  141. * An object which contains a description of the region.
  142. * @exportDoc
  143. */
  144. /**
  145. * @event shaka.Player.TimelineRegionExitEvent
  146. * @description Fired when the playhead exits a timeline region.
  147. * @property {string} type
  148. * 'timelineregionexit'
  149. * @property {shaka.extern.TimelineRegionInfo} detail
  150. * An object which contains a description of the region.
  151. * @exportDoc
  152. */
  153. /**
  154. * @event shaka.Player.MediaQualityChangedEvent
  155. * @description Fired when the media quality changes at the playhead.
  156. * That may be caused by an adaptation change or a DASH period transition.
  157. * Separate events are emitted for audio and video contentTypes.
  158. * @property {string} type
  159. * 'mediaqualitychanged'
  160. * @property {shaka.extern.MediaQualityInfo} mediaQuality
  161. * Information about media quality at the playhead position.
  162. * @property {number} position
  163. * The playhead position.
  164. * @exportDoc
  165. */
  166. /**
  167. * @event shaka.Player.AudioTrackChangedEvent
  168. * @description Fired when the audio track changes at the playhead.
  169. * That may be caused by a user requesting to chang audio tracks.
  170. * @property {string} type
  171. * 'audiotrackchanged'
  172. * @property {shaka.extern.MediaQualityInfo} mediaQuality
  173. * Information about media quality at the playhead position.
  174. * @property {number} position
  175. * The playhead position.
  176. * @exportDoc
  177. */
  178. /**
  179. * @event shaka.Player.BufferingEvent
  180. * @description Fired when the player's buffering state changes.
  181. * @property {string} type
  182. * 'buffering'
  183. * @property {boolean} buffering
  184. * True when the Player enters the buffering state.
  185. * False when the Player leaves the buffering state.
  186. * @exportDoc
  187. */
  188. /**
  189. * @event shaka.Player.LoadingEvent
  190. * @description Fired when the player begins loading. The start of loading is
  191. * defined as when the user has communicated intent to load content (i.e.
  192. * <code>Player.load</code> has been called).
  193. * @property {string} type
  194. * 'loading'
  195. * @exportDoc
  196. */
  197. /**
  198. * @event shaka.Player.LoadedEvent
  199. * @description Fired when the player ends the load.
  200. * @property {string} type
  201. * 'loaded'
  202. * @exportDoc
  203. */
  204. /**
  205. * @event shaka.Player.UnloadingEvent
  206. * @description Fired when the player unloads or fails to load.
  207. * Used by the Cast receiver to determine idle state.
  208. * @property {string} type
  209. * 'unloading'
  210. * @exportDoc
  211. */
  212. /**
  213. * @event shaka.Player.TextTrackVisibilityEvent
  214. * @description Fired when text track visibility changes.
  215. * @property {string} type
  216. * 'texttrackvisibility'
  217. * @exportDoc
  218. */
  219. /**
  220. * @event shaka.Player.TracksChangedEvent
  221. * @description Fired when the list of tracks changes. For example, this will
  222. * happen when new tracks are added/removed or when track restrictions change.
  223. * @property {string} type
  224. * 'trackschanged'
  225. * @exportDoc
  226. */
  227. /**
  228. * @event shaka.Player.AdaptationEvent
  229. * @description Fired when an automatic adaptation causes the active tracks
  230. * to change. Does not fire when the application calls
  231. * <code>selectVariantTrack()</code>, <code>selectTextTrack()</code>,
  232. * <code>selectAudioLanguage()</code>, or <code>selectTextLanguage()</code>.
  233. * @property {string} type
  234. * 'adaptation'
  235. * @property {shaka.extern.Track} oldTrack
  236. * @property {shaka.extern.Track} newTrack
  237. * @exportDoc
  238. */
  239. /**
  240. * @event shaka.Player.VariantChangedEvent
  241. * @description Fired when a call from the application caused a variant change.
  242. * Can be triggered by calls to <code>selectVariantTrack()</code> or
  243. * <code>selectAudioLanguage()</code>. Does not fire when an automatic
  244. * adaptation causes a variant change.
  245. * @property {string} type
  246. * 'variantchanged'
  247. * @property {shaka.extern.Track} oldTrack
  248. * @property {shaka.extern.Track} newTrack
  249. * @exportDoc
  250. */
  251. /**
  252. * @event shaka.Player.TextChangedEvent
  253. * @description Fired when a call from the application caused a text stream
  254. * change. Can be triggered by calls to <code>selectTextTrack()</code> or
  255. * <code>selectTextLanguage()</code>.
  256. * @property {string} type
  257. * 'textchanged'
  258. * @exportDoc
  259. */
  260. /**
  261. * @event shaka.Player.ExpirationUpdatedEvent
  262. * @description Fired when there is a change in the expiration times of an
  263. * EME session.
  264. * @property {string} type
  265. * 'expirationupdated'
  266. * @exportDoc
  267. */
  268. /**
  269. * @event shaka.Player.ManifestParsedEvent
  270. * @description Fired after the manifest has been parsed, but before anything
  271. * else happens. The manifest may contain streams that will be filtered out,
  272. * at this stage of the loading process.
  273. * @property {string} type
  274. * 'manifestparsed'
  275. * @exportDoc
  276. */
  277. /**
  278. * @event shaka.Player.ManifestUpdatedEvent
  279. * @description Fired after the manifest has been updated (live streams).
  280. * @property {string} type
  281. * 'manifestupdated'
  282. * @property {boolean} isLive
  283. * True when the playlist is live. Useful to detect transition from live
  284. * to static playlist..
  285. * @exportDoc
  286. */
  287. /**
  288. * @event shaka.Player.MetadataEvent
  289. * @description Triggers after metadata associated with the stream is found.
  290. * Usually they are metadata of type ID3.
  291. * @property {string} type
  292. * 'metadata'
  293. * @property {number} startTime
  294. * The time that describes the beginning of the range of the metadata to
  295. * which the cue applies.
  296. * @property {?number} endTime
  297. * The time that describes the end of the range of the metadata to which
  298. * the cue applies.
  299. * @property {string} metadataType
  300. * Type of metadata. Eg: 'org.id3' or 'com.apple.quicktime.HLS'
  301. * @property {shaka.extern.MetadataFrame} payload
  302. * The metadata itself
  303. * @exportDoc
  304. */
  305. /**
  306. * @event shaka.Player.StreamingEvent
  307. * @description Fired after the manifest has been parsed and track information
  308. * is available, but before streams have been chosen and before any segments
  309. * have been fetched. You may use this event to configure the player based on
  310. * information found in the manifest.
  311. * @property {string} type
  312. * 'streaming'
  313. * @exportDoc
  314. */
  315. /**
  316. * @event shaka.Player.AbrStatusChangedEvent
  317. * @description Fired when the state of abr has been changed.
  318. * (Enabled or disabled).
  319. * @property {string} type
  320. * 'abrstatuschanged'
  321. * @property {boolean} newStatus
  322. * The new status of the application. True for 'is enabled' and
  323. * false otherwise.
  324. * @exportDoc
  325. */
  326. /**
  327. * @event shaka.Player.RateChangeEvent
  328. * @description Fired when the video's playback rate changes.
  329. * This allows the PlayRateController to update it's internal rate field,
  330. * before the UI updates playback button with the newest playback rate.
  331. * @property {string} type
  332. * 'ratechange'
  333. * @exportDoc
  334. */
  335. /**
  336. * @event shaka.Player.SegmentAppended
  337. * @description Fired when a segment is appended to the media element.
  338. * @property {string} type
  339. * 'segmentappended'
  340. * @property {number} start
  341. * The start time of the segment.
  342. * @property {number} end
  343. * The end time of the segment.
  344. * @property {string} contentType
  345. * The content type of the segment. E.g. 'video', 'audio', or 'text'.
  346. * @property {boolean} isMuxed
  347. * Indicates if the segment is muxed (audio + video).
  348. * @exportDoc
  349. */
  350. /**
  351. * @event shaka.Player.SessionDataEvent
  352. * @description Fired when the manifest parser find info about session data.
  353. * Specification: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
  354. * @property {string} type
  355. * 'sessiondata'
  356. * @property {string} id
  357. * The id of the session data.
  358. * @property {string} uri
  359. * The uri with the session data info.
  360. * @property {string} language
  361. * The language of the session data.
  362. * @property {string} value
  363. * The value of the session data.
  364. * @exportDoc
  365. */
  366. /**
  367. * @event shaka.Player.StallDetectedEvent
  368. * @description Fired when a stall in playback is detected by the StallDetector.
  369. * Not all stalls are caused by gaps in the buffered ranges.
  370. * @property {string} type
  371. * 'stalldetected'
  372. * @exportDoc
  373. */
  374. /**
  375. * @event shaka.Player.GapJumpedEvent
  376. * @description Fired when the GapJumpingController jumps over a gap in the
  377. * buffered ranges.
  378. * @property {string} type
  379. * 'gapjumped'
  380. * @exportDoc
  381. */
  382. /**
  383. * @event shaka.Player.KeyStatusChanged
  384. * @description Fired when the key status changed.
  385. * @property {string} type
  386. * 'keystatuschanged'
  387. * @exportDoc
  388. */
  389. /**
  390. * @event shaka.Player.StateChanged
  391. * @description Fired when player state is changed.
  392. * @property {string} type
  393. * 'statechanged'
  394. * @property {string} newstate
  395. * The new state.
  396. * @exportDoc
  397. */
  398. /**
  399. * @event shaka.Player.Started
  400. * @description Fires when the content starts playing.
  401. * Only for VoD.
  402. * @property {string} type
  403. * 'started'
  404. * @exportDoc
  405. */
  406. /**
  407. * @event shaka.Player.FirstQuartile
  408. * @description Fires when the content playhead crosses first quartile.
  409. * Only for VoD.
  410. * @property {string} type
  411. * 'firstquartile'
  412. * @exportDoc
  413. */
  414. /**
  415. * @event shaka.Player.Midpoint
  416. * @description Fires when the content playhead crosses midpoint.
  417. * Only for VoD.
  418. * @property {string} type
  419. * 'midpoint'
  420. * @exportDoc
  421. */
  422. /**
  423. * @event shaka.Player.ThirdQuartile
  424. * @description Fires when the content playhead crosses third quartile.
  425. * Only for VoD.
  426. * @property {string} type
  427. * 'thirdquartile'
  428. * @exportDoc
  429. */
  430. /**
  431. * @event shaka.Player.Complete
  432. * @description Fires when the content completes playing.
  433. * Only for VoD.
  434. * @property {string} type
  435. * 'complete'
  436. * @exportDoc
  437. */
  438. /**
  439. * @event shaka.Player.SpatialVideoInfoEvent
  440. * @description Fired when the video has spatial video info. If a previous
  441. * event was fired, this include the new info.
  442. * @property {string} type
  443. * 'spatialvideoinfo'
  444. * @property {shaka.extern.SpatialVideoInfo} detail
  445. * An object which contains the content of the emsg box.
  446. * @exportDoc
  447. */
  448. /**
  449. * @event shaka.Player.NoSpatialVideoInfoEvent
  450. * @description Fired when the video no longer has spatial video information.
  451. * For it to be fired, the shaka.Player.SpatialVideoInfoEvent event must
  452. * have been previously fired.
  453. * @property {string} type
  454. * 'nospatialvideoinfo'
  455. * @exportDoc
  456. */
  457. /**
  458. * @summary The main player object for Shaka Player.
  459. *
  460. * @implements {shaka.util.IDestroyable}
  461. * @export
  462. */
  463. shaka.Player = class extends shaka.util.FakeEventTarget {
  464. /**
  465. * @param {HTMLMediaElement=} mediaElement
  466. * When provided, the player will attach to <code>mediaElement</code>,
  467. * similar to calling <code>attach</code>. When not provided, the player
  468. * will remain detached.
  469. * @param {function(shaka.Player)=} dependencyInjector Optional callback
  470. * which is called to inject mocks into the Player. Used for testing.
  471. */
  472. constructor(mediaElement, dependencyInjector) {
  473. super();
  474. /** @private {shaka.Player.LoadMode} */
  475. this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED;
  476. /** @private {HTMLMediaElement} */
  477. this.video_ = null;
  478. /** @private {HTMLElement} */
  479. this.videoContainer_ = null;
  480. /**
  481. * Since we may not always have a text displayer created (e.g. before |load|
  482. * is called), we need to track what text visibility SHOULD be so that we
  483. * can ensure that when we create the text displayer. When we create our
  484. * text displayer, we will use this to show (or not show) text as per the
  485. * user's requests.
  486. *
  487. * @private {boolean}
  488. */
  489. this.isTextVisible_ = false;
  490. /**
  491. * For listeners scoped to the lifetime of the Player instance.
  492. * @private {shaka.util.EventManager}
  493. */
  494. this.globalEventManager_ = new shaka.util.EventManager();
  495. /**
  496. * For listeners scoped to the lifetime of the media element attachment.
  497. * @private {shaka.util.EventManager}
  498. */
  499. this.attachEventManager_ = new shaka.util.EventManager();
  500. /**
  501. * For listeners scoped to the lifetime of the loaded content.
  502. * @private {shaka.util.EventManager}
  503. */
  504. this.loadEventManager_ = new shaka.util.EventManager();
  505. /**
  506. * For listeners scoped to the lifetime of the loaded content.
  507. * @private {shaka.util.EventManager}
  508. */
  509. this.trickPlayEventManager_ = new shaka.util.EventManager();
  510. /**
  511. * For listeners scoped to the lifetime of the ad manager.
  512. * @private {shaka.util.EventManager}
  513. */
  514. this.adManagerEventManager_ = new shaka.util.EventManager();
  515. /** @private {shaka.net.NetworkingEngine} */
  516. this.networkingEngine_ = null;
  517. /** @private {shaka.media.DrmEngine} */
  518. this.drmEngine_ = null;
  519. /** @private {shaka.media.MediaSourceEngine} */
  520. this.mediaSourceEngine_ = null;
  521. /** @private {shaka.media.Playhead} */
  522. this.playhead_ = null;
  523. /**
  524. * Incremented whenever a top-level operation (load, attach, etc) is
  525. * performed.
  526. * Used to determine if a load operation has been interrupted.
  527. * @private {number}
  528. */
  529. this.operationId_ = 0;
  530. /** @private {!shaka.util.Mutex} */
  531. this.mutex_ = new shaka.util.Mutex();
  532. /**
  533. * The playhead observers are used to monitor the position of the playhead
  534. * and some other source of data (e.g. buffered content), and raise events.
  535. *
  536. * @private {shaka.media.PlayheadObserverManager}
  537. */
  538. this.playheadObservers_ = null;
  539. /**
  540. * This is our control over the playback rate of the media element. This
  541. * provides the missing functionality that we need to provide trick play,
  542. * for example a negative playback rate.
  543. *
  544. * @private {shaka.media.PlayRateController}
  545. */
  546. this.playRateController_ = null;
  547. // We use the buffering observer and timer to track when we move from having
  548. // enough buffered content to not enough. They only exist when content has
  549. // been loaded and are not re-used between loads.
  550. /** @private {shaka.util.Timer} */
  551. this.bufferPoller_ = null;
  552. /** @private {shaka.media.BufferingObserver} */
  553. this.bufferObserver_ = null;
  554. /** @private {shaka.media.RegionTimeline} */
  555. this.regionTimeline_ = null;
  556. /** @private {shaka.util.CmcdManager} */
  557. this.cmcdManager_ = null;
  558. /** @private {shaka.util.CmsdManager} */
  559. this.cmsdManager_ = null;
  560. // This is the canvas element that will be used for rendering LCEVC
  561. // enhanced frames.
  562. /** @private {?HTMLCanvasElement} */
  563. this.lcevcCanvas_ = null;
  564. // This is the LCEVC Decoder object to decode LCEVC.
  565. /** @private {?shaka.lcevc.Dec} */
  566. this.lcevcDec_ = null;
  567. /** @private {shaka.media.QualityObserver} */
  568. this.qualityObserver_ = null;
  569. /** @private {shaka.media.StreamingEngine} */
  570. this.streamingEngine_ = null;
  571. /** @private {shaka.extern.ManifestParser} */
  572. this.parser_ = null;
  573. /** @private {?shaka.extern.ManifestParser.Factory} */
  574. this.parserFactory_ = null;
  575. /** @private {?shaka.extern.Manifest} */
  576. this.manifest_ = null;
  577. /** @private {?string} */
  578. this.assetUri_ = null;
  579. /** @private {?string} */
  580. this.mimeType_ = null;
  581. /** @private {?number} */
  582. this.startTime_ = null;
  583. /** @private {boolean} */
  584. this.fullyLoaded_ = false;
  585. /** @private {shaka.extern.AbrManager} */
  586. this.abrManager_ = null;
  587. /**
  588. * The factory that was used to create the abrManager_ instance.
  589. * @private {?shaka.extern.AbrManager.Factory}
  590. */
  591. this.abrManagerFactory_ = null;
  592. /**
  593. * Contains an ID for use with creating streams. The manifest parser should
  594. * start with small IDs, so this starts with a large one.
  595. * @private {number}
  596. */
  597. this.nextExternalStreamId_ = 1e9;
  598. /** @private {!Array.<shaka.extern.Stream>} */
  599. this.externalSrcEqualsThumbnailsStreams_ = [];
  600. /** @private {number} */
  601. this.completionPercent_ = NaN;
  602. /** @private {?shaka.extern.PlayerConfiguration} */
  603. this.config_ = this.defaultConfig_();
  604. /** @private {?number} */
  605. this.currentTargetLatency_ = null;
  606. /** @private {number} */
  607. this.rebufferingCount_ = -1;
  608. /** @private {?number} */
  609. this.targetLatencyReached_ = null;
  610. /**
  611. * The TextDisplayerFactory that was last used to make a text displayer.
  612. * Stored so that we can tell if a new type of text displayer is desired.
  613. * @private {?shaka.extern.TextDisplayer.Factory}
  614. */
  615. this.lastTextFactory_;
  616. /** @private {shaka.extern.Resolution} */
  617. this.maxHwRes_ = {width: Infinity, height: Infinity};
  618. /** @private {!shaka.media.ManifestFilterer} */
  619. this.manifestFilterer_ = new shaka.media.ManifestFilterer(
  620. this.config_, this.maxHwRes_, null);
  621. /** @private {!Array.<shaka.media.PreloadManager>} */
  622. this.createdPreloadManagers_ = [];
  623. /** @private {shaka.util.Stats} */
  624. this.stats_ = null;
  625. /** @private {!shaka.media.AdaptationSetCriteria} */
  626. this.currentAdaptationSetCriteria_ =
  627. new shaka.media.PreferenceBasedCriteria(
  628. this.config_.preferredAudioLanguage,
  629. this.config_.preferredVariantRole,
  630. this.config_.preferredAudioChannelCount,
  631. this.config_.preferredVideoHdrLevel,
  632. this.config_.preferSpatialAudio,
  633. this.config_.preferredVideoLayout,
  634. this.config_.preferredAudioLabel,
  635. this.config_.preferredVideoLabel,
  636. this.config_.mediaSource.codecSwitchingStrategy,
  637. this.config_.manifest.dash.enableAudioGroups);
  638. /** @private {string} */
  639. this.currentTextLanguage_ = this.config_.preferredTextLanguage;
  640. /** @private {string} */
  641. this.currentTextRole_ = this.config_.preferredTextRole;
  642. /** @private {boolean} */
  643. this.currentTextForced_ = this.config_.preferForcedSubs;
  644. /** @private {!Array.<function():(!Promise|undefined)>} */
  645. this.cleanupOnUnload_ = [];
  646. if (dependencyInjector) {
  647. dependencyInjector(this);
  648. }
  649. // Create the CMCD manager so client data can be attached to all requests
  650. this.cmcdManager_ = this.createCmcd_();
  651. this.cmsdManager_ = this.createCmsd_();
  652. this.networkingEngine_ = this.createNetworkingEngine();
  653. this.networkingEngine_.setForceHTTP(this.config_.streaming.forceHTTP);
  654. this.networkingEngine_.setForceHTTPS(this.config_.streaming.forceHTTPS);
  655. /** @private {shaka.extern.IAdManager} */
  656. this.adManager_ = null;
  657. /** @private {?shaka.media.PreloadManager} */
  658. this.preloadDueAdManager_ = null;
  659. /** @private {HTMLMediaElement} */
  660. this.preloadDueAdManagerVideo_ = null;
  661. /** @private {boolean} */
  662. this.preloadDueAdManagerVideoEnded_ = false;
  663. /** @private {shaka.util.Timer} */
  664. this.preloadDueAdManagerTimer_ = new shaka.util.Timer(async () => {
  665. if (this.preloadDueAdManager_) {
  666. goog.asserts.assert(this.preloadDueAdManagerVideo_, 'Must have video');
  667. await this.attach(
  668. this.preloadDueAdManagerVideo_, /* initializeMediaSource= */ true);
  669. await this.load(this.preloadDueAdManager_);
  670. if (!this.preloadDueAdManagerVideoEnded_) {
  671. this.preloadDueAdManagerVideo_.play();
  672. } else {
  673. this.preloadDueAdManagerVideo_.pause();
  674. }
  675. this.preloadDueAdManager_ = null;
  676. this.preloadDueAdManagerVideoEnded_ = false;
  677. }
  678. });
  679. if (shaka.Player.adManagerFactory_) {
  680. this.adManager_ = shaka.Player.adManagerFactory_();
  681. this.adManager_.configure(this.config_.ads);
  682. // Note: we don't use shaka.ads.Utils.AD_CONTENT_PAUSE_REQUESTED to
  683. // avoid add a optional module in the player.
  684. this.adManagerEventManager_.listen(
  685. this.adManager_, 'ad-content-pause-requested', async (e) => {
  686. this.preloadDueAdManagerTimer_.stop();
  687. if (!this.preloadDueAdManager_) {
  688. this.preloadDueAdManagerVideo_ = this.video_;
  689. this.preloadDueAdManagerVideoEnded_ = this.video_.ended;
  690. const saveLivePosition = /** @type {boolean} */(
  691. e['saveLivePosition']) || false;
  692. this.preloadDueAdManager_ = await this.detachAndSavePreload(
  693. /* keepAdManager= */ true, saveLivePosition);
  694. }
  695. });
  696. // Note: we don't use shaka.ads.Utils.AD_CONTENT_RESUME_REQUESTED to
  697. // avoid add a optional module in the player.
  698. this.adManagerEventManager_.listen(
  699. this.adManager_, 'ad-content-resume-requested', (e) => {
  700. const offset = /** @type {number} */(e['offset']) || 0;
  701. if (this.preloadDueAdManager_) {
  702. this.preloadDueAdManager_.setOffsetToStartTime(offset);
  703. }
  704. this.preloadDueAdManagerTimer_.tickAfter(0.1);
  705. });
  706. // Note: we don't use shaka.ads.Utils.AD_CONTENT_ATTACH_REQUESTED to
  707. // avoid add a optional module in the player.
  708. this.adManagerEventManager_.listen(
  709. this.adManager_, 'ad-content-attach-requested', async (e) => {
  710. if (!this.video_ && this.preloadDueAdManagerVideo_) {
  711. goog.asserts.assert(this.preloadDueAdManagerVideo_,
  712. 'Must have video');
  713. await this.attach(this.preloadDueAdManagerVideo_,
  714. /* initializeMediaSource= */ true);
  715. }
  716. });
  717. }
  718. // If the browser comes back online after being offline, then try to play
  719. // again.
  720. this.globalEventManager_.listen(window, 'online', () => {
  721. this.restoreDisabledVariants_();
  722. this.retryStreaming();
  723. });
  724. /** @private {shaka.util.Timer} */
  725. this.checkVariantsTimer_ =
  726. new shaka.util.Timer(() => this.checkVariants_());
  727. /** @private {?shaka.media.PreloadManager} */
  728. this.preloadNextUrl_ = null;
  729. // Even though |attach| will start in later interpreter cycles, it should be
  730. // the LAST thing we do in the constructor because conceptually it relies on
  731. // player having been initialized.
  732. if (mediaElement) {
  733. shaka.Deprecate.deprecateFeature(5,
  734. 'Player w/ mediaElement',
  735. 'Please migrate from initializing Player with a mediaElement; ' +
  736. 'use the attach method instead.');
  737. this.attach(mediaElement, /* initializeMediaSource= */ true);
  738. }
  739. }
  740. /**
  741. * Create a shaka.lcevc.Dec object
  742. * @param {shaka.extern.LcevcConfiguration} config
  743. * @private
  744. */
  745. createLcevcDec_(config) {
  746. if (this.lcevcDec_ == null) {
  747. this.lcevcDec_ = new shaka.lcevc.Dec(
  748. /** @type {HTMLVideoElement} */ (this.video_),
  749. this.lcevcCanvas_,
  750. config,
  751. );
  752. if (this.mediaSourceEngine_) {
  753. this.mediaSourceEngine_.updateLcevcDec(this.lcevcDec_);
  754. }
  755. }
  756. }
  757. /**
  758. * Close a shaka.lcevc.Dec object if present and hide the canvas.
  759. * @private
  760. */
  761. closeLcevcDec_() {
  762. if (this.lcevcDec_ != null) {
  763. this.lcevcDec_.hideCanvas();
  764. this.lcevcDec_.release();
  765. this.lcevcDec_ = null;
  766. }
  767. }
  768. /**
  769. * Setup shaka.lcevc.Dec object
  770. * @param {?shaka.extern.PlayerConfiguration} config
  771. * @private
  772. */
  773. setupLcevc_(config) {
  774. if (config.lcevc.enabled) {
  775. this.closeLcevcDec_();
  776. this.createLcevcDec_(config.lcevc);
  777. } else {
  778. this.closeLcevcDec_();
  779. }
  780. }
  781. /**
  782. * @param {!shaka.util.FakeEvent.EventName} name
  783. * @param {Map.<string, Object>=} data
  784. * @return {!shaka.util.FakeEvent}
  785. * @private
  786. */
  787. static makeEvent_(name, data) {
  788. return new shaka.util.FakeEvent(name, data);
  789. }
  790. /**
  791. * After destruction, a Player object cannot be used again.
  792. *
  793. * @override
  794. * @export
  795. */
  796. async destroy() {
  797. // Make sure we only execute the destroy logic once.
  798. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
  799. return;
  800. }
  801. // If LCEVC Decoder exists close it.
  802. this.closeLcevcDec_();
  803. const detachPromise = this.detach();
  804. // Mark as "dead". This should stop external-facing calls from changing our
  805. // internal state any more. This will stop calls to |attach|, |detach|, etc.
  806. // from interrupting our final move to the detached state.
  807. this.loadMode_ = shaka.Player.LoadMode.DESTROYED;
  808. await detachPromise;
  809. // A PreloadManager can only be used with the Player instance that created
  810. // it, so all PreloadManagers this Player has created are now useless.
  811. // Destroy any remaining managers now, to help prevent memory leaks.
  812. await this.destroyAllPreloads();
  813. // Tear-down the event managers to ensure handlers stop firing.
  814. if (this.globalEventManager_) {
  815. this.globalEventManager_.release();
  816. this.globalEventManager_ = null;
  817. }
  818. if (this.attachEventManager_) {
  819. this.attachEventManager_.release();
  820. this.attachEventManager_ = null;
  821. }
  822. if (this.loadEventManager_) {
  823. this.loadEventManager_.release();
  824. this.loadEventManager_ = null;
  825. }
  826. if (this.trickPlayEventManager_) {
  827. this.trickPlayEventManager_.release();
  828. this.trickPlayEventManager_ = null;
  829. }
  830. if (this.adManagerEventManager_) {
  831. this.adManagerEventManager_.release();
  832. this.adManagerEventManager_ = null;
  833. }
  834. this.abrManagerFactory_ = null;
  835. this.config_ = null;
  836. this.stats_ = null;
  837. this.videoContainer_ = null;
  838. this.cmcdManager_ = null;
  839. this.cmsdManager_ = null;
  840. if (this.networkingEngine_) {
  841. await this.networkingEngine_.destroy();
  842. this.networkingEngine_ = null;
  843. }
  844. if (this.abrManager_) {
  845. this.abrManager_.release();
  846. this.abrManager_ = null;
  847. }
  848. // FakeEventTarget implements IReleasable
  849. super.release();
  850. }
  851. /**
  852. * Registers a plugin callback that will be called with
  853. * <code>support()</code>. The callback will return the value that will be
  854. * stored in the return value from <code>support()</code>.
  855. *
  856. * @param {string} name
  857. * @param {function():*} callback
  858. * @export
  859. */
  860. static registerSupportPlugin(name, callback) {
  861. shaka.Player.supportPlugins_[name] = callback;
  862. }
  863. /**
  864. * Set a factory to create an ad manager during player construction time.
  865. * This method needs to be called bafore instantiating the Player class.
  866. *
  867. * @param {!shaka.extern.IAdManager.Factory} factory
  868. * @export
  869. */
  870. static setAdManagerFactory(factory) {
  871. shaka.Player.adManagerFactory_ = factory;
  872. }
  873. /**
  874. * Return whether the browser provides basic support. If this returns false,
  875. * Shaka Player cannot be used at all. In this case, do not construct a
  876. * Player instance and do not use the library.
  877. *
  878. * @return {boolean}
  879. * @export
  880. */
  881. static isBrowserSupported() {
  882. if (!window.Promise) {
  883. shaka.log.alwaysWarn('A Promise implementation or polyfill is required');
  884. }
  885. // Basic features needed for the library to be usable.
  886. const basicSupport = !!window.Promise && !!window.Uint8Array &&
  887. // eslint-disable-next-line no-restricted-syntax
  888. !!Array.prototype.forEach;
  889. if (!basicSupport) {
  890. return false;
  891. }
  892. // We do not support IE
  893. if (shaka.util.Platform.isIE()) {
  894. return false;
  895. }
  896. const safariVersion = shaka.util.Platform.safariVersion();
  897. if (safariVersion && safariVersion < 9) {
  898. return false;
  899. }
  900. // DRM support is not strictly necessary, but the APIs at least need to be
  901. // there. Our no-op DRM polyfill should handle that.
  902. // TODO(#1017): Consider making even DrmEngine optional.
  903. const drmSupport = shaka.media.DrmEngine.isBrowserSupported();
  904. if (!drmSupport) {
  905. return false;
  906. }
  907. // If we have MediaSource (MSE) support, we should be able to use Shaka.
  908. if (shaka.util.Platform.supportsMediaSource()) {
  909. return true;
  910. }
  911. // If we don't have MSE, we _may_ be able to use Shaka. Look for native HLS
  912. // support, and call this platform usable if we have it.
  913. return shaka.util.Platform.supportsMediaType('application/x-mpegurl');
  914. }
  915. /**
  916. * Probes the browser to determine what features are supported. This makes a
  917. * number of requests to EME/MSE/etc which may result in user prompts. This
  918. * should only be used for diagnostics.
  919. *
  920. * <p>
  921. * NOTE: This may show a request to the user for permission.
  922. *
  923. * @see https://bit.ly/2ywccmH
  924. * @param {boolean=} promptsOkay
  925. * @return {!Promise.<shaka.extern.SupportType>}
  926. * @export
  927. */
  928. static async probeSupport(promptsOkay=true) {
  929. goog.asserts.assert(shaka.Player.isBrowserSupported(),
  930. 'Must have basic support');
  931. let drm = {};
  932. if (promptsOkay) {
  933. drm = await shaka.media.DrmEngine.probeSupport();
  934. }
  935. const manifest = shaka.media.ManifestParser.probeSupport();
  936. const media = shaka.media.MediaSourceEngine.probeSupport();
  937. const hardwareResolution =
  938. await shaka.util.Platform.detectMaxHardwareResolution();
  939. /** @type {shaka.extern.SupportType} */
  940. const ret = {
  941. manifest,
  942. media,
  943. drm,
  944. hardwareResolution,
  945. };
  946. const plugins = shaka.Player.supportPlugins_;
  947. for (const name in plugins) {
  948. ret[name] = plugins[name]();
  949. }
  950. return ret;
  951. }
  952. /**
  953. * Makes a fires an event corresponding to entering a state of the loading
  954. * process.
  955. * @param {string} nodeName
  956. * @private
  957. */
  958. makeStateChangeEvent_(nodeName) {
  959. this.dispatchEvent(shaka.Player.makeEvent_(
  960. /* name= */ shaka.util.FakeEvent.EventName.OnStateChange,
  961. /* data= */ (new Map()).set('state', nodeName)));
  962. }
  963. /**
  964. * Attaches the player to a media element.
  965. * If the player was already attached to a media element, first detaches from
  966. * that media element.
  967. *
  968. * @param {!HTMLMediaElement} mediaElement
  969. * @param {boolean=} initializeMediaSource
  970. * @return {!Promise}
  971. * @export
  972. */
  973. async attach(mediaElement, initializeMediaSource = true) {
  974. // Do not allow the player to be used after |destroy| is called.
  975. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
  976. throw this.createAbortLoadError_();
  977. }
  978. const noop = this.video_ && this.video_ == mediaElement;
  979. if (this.video_ && this.video_ != mediaElement) {
  980. await this.detach();
  981. }
  982. if (await this.atomicOperationAcquireMutex_('attach')) {
  983. return;
  984. }
  985. try {
  986. if (!noop) {
  987. this.makeStateChangeEvent_('attach');
  988. const onError = (error) => this.onVideoError_(error);
  989. this.attachEventManager_.listen(mediaElement, 'error', onError);
  990. this.video_ = mediaElement;
  991. }
  992. // Only initialize media source if the platform supports it.
  993. if (initializeMediaSource &&
  994. shaka.util.Platform.supportsMediaSource() &&
  995. !this.mediaSourceEngine_) {
  996. await this.initializeMediaSourceEngineInner_();
  997. }
  998. } catch (error) {
  999. await this.detach();
  1000. throw error;
  1001. } finally {
  1002. this.mutex_.release();
  1003. }
  1004. }
  1005. /**
  1006. * Calling <code>attachCanvas</code> will tell the player to set canvas
  1007. * element for LCEVC decoding.
  1008. *
  1009. * @param {HTMLCanvasElement} canvas
  1010. * @export
  1011. */
  1012. attachCanvas(canvas) {
  1013. this.lcevcCanvas_ = canvas;
  1014. }
  1015. /**
  1016. * Detach the player from the current media element. Leaves the player in a
  1017. * state where it cannot play media, until it has been attached to something
  1018. * else.
  1019. *
  1020. * @param {boolean=} keepAdManager
  1021. *
  1022. * @return {!Promise}
  1023. * @export
  1024. */
  1025. async detach(keepAdManager = false) {
  1026. // Do not allow the player to be used after |destroy| is called.
  1027. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
  1028. throw this.createAbortLoadError_();
  1029. }
  1030. await this.unload(/* initializeMediaSource= */ false, keepAdManager);
  1031. if (await this.atomicOperationAcquireMutex_('detach')) {
  1032. return;
  1033. }
  1034. try {
  1035. // If we were going from "detached" to "detached" we wouldn't have
  1036. // a media element to detach from.
  1037. if (this.video_) {
  1038. this.attachEventManager_.removeAll();
  1039. this.video_ = null;
  1040. }
  1041. this.makeStateChangeEvent_('detach');
  1042. if (this.adManager_ && !keepAdManager) {
  1043. // The ad manager is specific to the video, so detach it too.
  1044. this.adManager_.release();
  1045. }
  1046. } finally {
  1047. this.mutex_.release();
  1048. }
  1049. }
  1050. /**
  1051. * Tries to acquire the mutex, and then returns if the operation should end
  1052. * early due to someone else starting a mutex-acquiring operation.
  1053. * Meant for operations that can't be interrupted midway through (e.g.
  1054. * everything but load).
  1055. * @param {string} mutexIdentifier
  1056. * @return {!Promise.<boolean>} endEarly If false, the calling context will
  1057. * need to release the mutex.
  1058. * @private
  1059. */
  1060. async atomicOperationAcquireMutex_(mutexIdentifier) {
  1061. const operationId = ++this.operationId_;
  1062. await this.mutex_.acquire(mutexIdentifier);
  1063. if (operationId != this.operationId_) {
  1064. this.mutex_.release();
  1065. return true;
  1066. }
  1067. return false;
  1068. }
  1069. /**
  1070. * Unloads the currently playing stream, if any.
  1071. *
  1072. * @param {boolean=} initializeMediaSource
  1073. * @param {boolean=} keepAdManager
  1074. * @return {!Promise}
  1075. * @export
  1076. */
  1077. async unload(initializeMediaSource = true, keepAdManager = false) {
  1078. // Set the load mode to unload right away so that all the public methods
  1079. // will stop using the internal components. We need to make sure that we
  1080. // are not overriding the destroyed state because we will unload when we are
  1081. // destroying the player.
  1082. if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) {
  1083. this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED;
  1084. }
  1085. if (await this.atomicOperationAcquireMutex_('unload')) {
  1086. return;
  1087. }
  1088. try {
  1089. this.fullyLoaded_ = false;
  1090. this.makeStateChangeEvent_('unload');
  1091. // If the platform does not support media source, we will never want to
  1092. // initialize media source.
  1093. if (initializeMediaSource && !shaka.util.Platform.supportsMediaSource()) {
  1094. initializeMediaSource = false;
  1095. }
  1096. // If LCEVC Decoder exists close it.
  1097. this.closeLcevcDec_();
  1098. // Run any general cleanup tasks now. This should be here at the top,
  1099. // right after setting loadMode_, so that internal components still exist
  1100. // as they did when the cleanup tasks were registered in the array.
  1101. const cleanupTasks = this.cleanupOnUnload_.map((cb) => cb());
  1102. this.cleanupOnUnload_ = [];
  1103. await Promise.all(cleanupTasks);
  1104. // Dispatch the unloading event.
  1105. this.dispatchEvent(
  1106. shaka.Player.makeEvent_(shaka.util.FakeEvent.EventName.Unloading));
  1107. // Release the region timeline, which is created when parsing the
  1108. // manifest.
  1109. if (this.regionTimeline_) {
  1110. this.regionTimeline_.release();
  1111. this.regionTimeline_ = null;
  1112. }
  1113. // In most cases we should have a media element. The one exception would
  1114. // be if there was an error and we, by chance, did not have a media
  1115. // element.
  1116. if (this.video_) {
  1117. this.loadEventManager_.removeAll();
  1118. this.trickPlayEventManager_.removeAll();
  1119. }
  1120. // Stop the variant checker timer
  1121. this.checkVariantsTimer_.stop();
  1122. // Some observers use some playback components, shutting down the
  1123. // observers first ensures that they don't try to use the playback
  1124. // components mid-destroy.
  1125. if (this.playheadObservers_) {
  1126. this.playheadObservers_.release();
  1127. this.playheadObservers_ = null;
  1128. }
  1129. if (this.bufferPoller_) {
  1130. this.bufferPoller_.stop();
  1131. this.bufferPoller_ = null;
  1132. }
  1133. // Stop the parser early. Since it is at the start of the pipeline, it
  1134. // should be start early to avoid is pushing new data downstream.
  1135. if (this.parser_) {
  1136. await this.parser_.stop();
  1137. this.parser_ = null;
  1138. this.parserFactory_ = null;
  1139. }
  1140. // Abr Manager will tell streaming engine what to do, so we need to stop
  1141. // it before we destroy streaming engine. Unlike with the other
  1142. // components, we do not release the instance, we will reuse it in later
  1143. // loads.
  1144. if (this.abrManager_) {
  1145. await this.abrManager_.stop();
  1146. }
  1147. // Streaming engine will push new data to media source engine, so we need
  1148. // to shut it down before destroy media source engine.
  1149. if (this.streamingEngine_) {
  1150. await this.streamingEngine_.destroy();
  1151. this.streamingEngine_ = null;
  1152. }
  1153. if (this.playRateController_) {
  1154. this.playRateController_.release();
  1155. this.playRateController_ = null;
  1156. }
  1157. // Playhead is used by StreamingEngine, so we can't destroy this until
  1158. // after StreamingEngine has stopped.
  1159. if (this.playhead_) {
  1160. this.playhead_.release();
  1161. this.playhead_ = null;
  1162. }
  1163. // EME v0.1b requires the media element to clear the MediaKeys
  1164. if (shaka.util.Platform.isMediaKeysPolyfilled('webkit') &&
  1165. this.drmEngine_) {
  1166. await this.drmEngine_.destroy();
  1167. this.drmEngine_ = null;
  1168. }
  1169. // Media source engine holds onto the media element, and in order to
  1170. // detach the media keys (with drm engine), we need to break the
  1171. // connection between media source engine and the media element.
  1172. if (this.mediaSourceEngine_) {
  1173. await this.mediaSourceEngine_.destroy();
  1174. this.mediaSourceEngine_ = null;
  1175. }
  1176. if (this.adManager_ && !keepAdManager) {
  1177. this.adManager_.onAssetUnload();
  1178. }
  1179. if (this.preloadDueAdManager_ && !keepAdManager) {
  1180. this.preloadDueAdManager_.destroy();
  1181. this.preloadDueAdManager_ = null;
  1182. }
  1183. if (!keepAdManager) {
  1184. this.preloadDueAdManagerTimer_.stop();
  1185. }
  1186. if (this.cmcdManager_) {
  1187. this.cmcdManager_.reset();
  1188. }
  1189. if (this.cmsdManager_) {
  1190. this.cmsdManager_.reset();
  1191. }
  1192. if (this.video_) {
  1193. // Remove all track nodes
  1194. shaka.util.Dom.removeAllChildren(this.video_);
  1195. }
  1196. // In order to unload a media element, we need to remove the src attribute
  1197. // and then load again. When we destroy media source engine, this will be
  1198. // done for us, but for src=, we need to do it here.
  1199. //
  1200. // DrmEngine requires this to be done before we destroy DrmEngine itself.
  1201. if (this.video_ && this.video_.src) {
  1202. // TODO: Investigate this more. Only reproduces on Firefox 69.
  1203. // Introduce a delay before detaching the video source. We are seeing
  1204. // spurious Promise rejections involving an AbortError in our tests
  1205. // otherwise.
  1206. await new Promise(
  1207. (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1));
  1208. this.video_.removeAttribute('src');
  1209. this.video_.load();
  1210. }
  1211. if (this.drmEngine_) {
  1212. await this.drmEngine_.destroy();
  1213. this.drmEngine_ = null;
  1214. }
  1215. if (this.preloadNextUrl_ &&
  1216. this.assetUri_ != this.preloadNextUrl_.getAssetUri()) {
  1217. if (!this.preloadNextUrl_.isDestroyed()) {
  1218. this.preloadNextUrl_.destroy();
  1219. }
  1220. this.preloadNextUrl_ = null;
  1221. }
  1222. this.assetUri_ = null;
  1223. this.mimeType_ = null;
  1224. this.bufferObserver_ = null;
  1225. if (this.manifest_) {
  1226. for (const variant of this.manifest_.variants) {
  1227. for (const stream of [variant.audio, variant.video]) {
  1228. if (stream && stream.segmentIndex) {
  1229. stream.segmentIndex.release();
  1230. }
  1231. }
  1232. }
  1233. for (const stream of this.manifest_.textStreams) {
  1234. if (stream.segmentIndex) {
  1235. stream.segmentIndex.release();
  1236. }
  1237. }
  1238. }
  1239. // On some devices, cached MediaKeySystemAccess objects may corrupt
  1240. // after several playbacks, and they are not able anymore to properly
  1241. // create MediaKeys objects. To prevent it, clear the cache after
  1242. // each playback.
  1243. if (this.config_.streaming.clearDecodingCache) {
  1244. shaka.util.StreamUtils.clearDecodingConfigCache();
  1245. shaka.media.DrmEngine.clearMediaKeySystemAccessMap();
  1246. }
  1247. this.manifest_ = null;
  1248. this.stats_ = new shaka.util.Stats(); // Replace with a clean object.
  1249. this.lastTextFactory_ = null;
  1250. this.targetLatencyReached_ = null;
  1251. this.currentTargetLatency_ = null;
  1252. this.rebufferingCount_ = -1;
  1253. this.externalSrcEqualsThumbnailsStreams_ = [];
  1254. this.completionPercent_ = NaN;
  1255. // Make sure that the app knows of the new buffering state.
  1256. this.updateBufferState_();
  1257. } finally {
  1258. this.mutex_.release();
  1259. }
  1260. if (initializeMediaSource && shaka.util.Platform.supportsMediaSource() &&
  1261. !this.mediaSourceEngine_ && this.video_) {
  1262. await this.initializeMediaSourceEngineInner_();
  1263. }
  1264. }
  1265. /**
  1266. * Provides a way to update the stream start position during the media loading
  1267. * process. Can for example be called from the <code>manifestparsed</code>
  1268. * event handler to update the start position based on information in the
  1269. * manifest.
  1270. *
  1271. * @param {number} startTime
  1272. * @export
  1273. */
  1274. updateStartTime(startTime) {
  1275. this.startTime_ = startTime;
  1276. }
  1277. /**
  1278. * Loads a new stream.
  1279. * If another stream was already playing, first unloads that stream.
  1280. *
  1281. * @param {string|shaka.media.PreloadManager} assetUriOrPreloader
  1282. * @param {?number=} startTime
  1283. * When <code>startTime</code> is <code>null</code> or
  1284. * <code>undefined</code>, playback will start at the default start time (0
  1285. * for VOD and liveEdge for LIVE).
  1286. * @param {?string=} mimeType
  1287. * @return {!Promise}
  1288. * @export
  1289. */
  1290. async load(assetUriOrPreloader, startTime = null, mimeType) {
  1291. // Do not allow the player to be used after |destroy| is called.
  1292. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
  1293. throw this.createAbortLoadError_();
  1294. }
  1295. /** @type {?shaka.media.PreloadManager} */
  1296. let preloadManager = null;
  1297. let assetUri = '';
  1298. if (assetUriOrPreloader instanceof shaka.media.PreloadManager) {
  1299. preloadManager = assetUriOrPreloader;
  1300. assetUri = preloadManager.getAssetUri() || '';
  1301. } else {
  1302. assetUri = assetUriOrPreloader || '';
  1303. }
  1304. // Quickly acquire the mutex, so this will wait for other top-level
  1305. // operations.
  1306. await this.mutex_.acquire('load');
  1307. this.mutex_.release();
  1308. if (!this.video_) {
  1309. throw new shaka.util.Error(
  1310. shaka.util.Error.Severity.CRITICAL,
  1311. shaka.util.Error.Category.PLAYER,
  1312. shaka.util.Error.Code.NO_VIDEO_ELEMENT);
  1313. }
  1314. if (this.assetUri_) {
  1315. // Note: This is used to avoid the destruction of the nextUrl
  1316. // preloadManager that can be the current one.
  1317. this.assetUri_ = assetUri;
  1318. await this.unload(/* initializeMediaSource= */ false);
  1319. }
  1320. // Add a mechanism to detect if the load process has been interrupted by a
  1321. // call to another top-level operation (unload, load, etc).
  1322. const operationId = ++this.operationId_;
  1323. const detectInterruption = async () => {
  1324. if (this.operationId_ != operationId) {
  1325. if (preloadManager) {
  1326. await preloadManager.destroy();
  1327. }
  1328. throw this.createAbortLoadError_();
  1329. }
  1330. };
  1331. /**
  1332. * Wraps a given operation with mutex.acquire and mutex.release, along with
  1333. * calls to detectInterruption, to catch any other top-level calls happening
  1334. * while waiting for the mutex.
  1335. * @param {function():!Promise} operation
  1336. * @param {string} mutexIdentifier
  1337. * @return {!Promise}
  1338. */
  1339. const mutexWrapOperation = async (operation, mutexIdentifier) => {
  1340. try {
  1341. await this.mutex_.acquire(mutexIdentifier);
  1342. await detectInterruption();
  1343. await operation();
  1344. await detectInterruption();
  1345. if (preloadManager && this.config_) {
  1346. preloadManager.reconfigure(this.config_);
  1347. }
  1348. } finally {
  1349. this.mutex_.release();
  1350. }
  1351. };
  1352. try {
  1353. if (startTime == null && preloadManager) {
  1354. startTime = preloadManager.getStartTime();
  1355. }
  1356. this.startTime_ = startTime;
  1357. this.fullyLoaded_ = false;
  1358. // We dispatch the loading event when someone calls |load| because we want
  1359. // to surface the user intent.
  1360. this.dispatchEvent(shaka.Player.makeEvent_(
  1361. shaka.util.FakeEvent.EventName.Loading));
  1362. if (preloadManager) {
  1363. mimeType = preloadManager.getMimeType();
  1364. } else if (!mimeType) {
  1365. await mutexWrapOperation(async () => {
  1366. mimeType = await this.guessMimeType_(assetUri);
  1367. }, 'guessMimeType_');
  1368. }
  1369. const wasPreloaded = !!preloadManager;
  1370. if (!preloadManager) {
  1371. // For simplicity, if an asset is NOT preloaded, start an internal
  1372. // "preload" here without prefetch.
  1373. // That way, both a preload and normal load can follow the same code
  1374. // paths.
  1375. // NOTE: await preloadInner_ can be outside the mutex because it should
  1376. // not mutate "this".
  1377. preloadManager = await this.preloadInner_(
  1378. assetUri, startTime, mimeType, /* standardLoad= */ true);
  1379. if (preloadManager) {
  1380. preloadManager.setEventHandoffTarget(this);
  1381. this.stats_ = preloadManager.getStats();
  1382. preloadManager.start();
  1383. // Silence "uncaught error" warnings from this. Unless we are
  1384. // interrupted, we will check the result of this process and respond
  1385. // appropriately. If we are interrupted, we can ignore any error
  1386. // there.
  1387. preloadManager.waitForFinish().catch(() => {});
  1388. } else {
  1389. this.stats_ = new shaka.util.Stats();
  1390. }
  1391. } else {
  1392. // Hook up events, so any events emitted by the preloadManager will
  1393. // instead be emitted by the player.
  1394. preloadManager.setEventHandoffTarget(this);
  1395. this.stats_ = preloadManager.getStats();
  1396. }
  1397. // Now, if there is no preload manager, that means that this is a src=
  1398. // asset.
  1399. const shouldUseSrcEquals = !preloadManager;
  1400. const startTimeOfLoad = Date.now() / 1000;
  1401. // Stats are for a single playback/load session. Stats must be initialized
  1402. // before we allow calls to |updateStateHistory|.
  1403. this.stats_ =
  1404. preloadManager ? preloadManager.getStats() : new shaka.util.Stats();
  1405. this.assetUri_ = assetUri;
  1406. this.mimeType_ = mimeType || null;
  1407. if (shouldUseSrcEquals) {
  1408. await mutexWrapOperation(async () => {
  1409. goog.asserts.assert(mimeType, 'We should know the mimeType by now!');
  1410. await this.initializeSrcEqualsDrmInner_(mimeType);
  1411. }, 'initializeSrcEqualsDrmInner_');
  1412. await mutexWrapOperation(async () => {
  1413. goog.asserts.assert(mimeType, 'We should know the mimeType by now!');
  1414. await this.srcEqualsInner_(startTimeOfLoad, mimeType);
  1415. }, 'srcEqualsInner_');
  1416. } else {
  1417. if (!this.mediaSourceEngine_) {
  1418. await mutexWrapOperation(async () => {
  1419. await this.initializeMediaSourceEngineInner_();
  1420. }, 'initializeMediaSourceEngineInner_');
  1421. }
  1422. // Wait for the preload manager to do all of the loading it can do.
  1423. await mutexWrapOperation(async () => {
  1424. await preloadManager.waitForFinish();
  1425. }, 'waitForFinish');
  1426. // Get manifest and associated values from preloader.
  1427. this.config_ = preloadManager.getConfiguration();
  1428. this.manifestFilterer_ = preloadManager.getManifestFilterer();
  1429. this.parserFactory_ = preloadManager.getParserFactory();
  1430. this.parser_ = preloadManager.receiveParser();
  1431. if (this.parser_ && this.parser_.setMediaElement && this.video_) {
  1432. this.parser_.setMediaElement(this.video_);
  1433. }
  1434. this.regionTimeline_ = preloadManager.receiveRegionTimeline();
  1435. this.qualityObserver_ = preloadManager.getQualityObserver();
  1436. this.manifest_ = preloadManager.getManifest();
  1437. const currentAdaptationSetCriteria =
  1438. preloadManager.getCurrentAdaptationSetCriteria();
  1439. if (currentAdaptationSetCriteria) {
  1440. this.currentAdaptationSetCriteria_ = currentAdaptationSetCriteria;
  1441. }
  1442. if (wasPreloaded && this.video_ && this.video_.nodeName === 'AUDIO') {
  1443. // Filter the variants to be audio-only after the fact.
  1444. // As, when preloading, we don't know if we are going to be attached
  1445. // to a video or audio element when we load, we have to do the auto
  1446. // audio-only filtering here, post-facto.
  1447. this.makeManifestAudioOnly_();
  1448. // And continue to do so in the future.
  1449. this.configure('manifest.disableVideo', true);
  1450. }
  1451. // Get drm engine from preloader, then finalize it.
  1452. this.drmEngine_ = preloadManager.receiveDrmEngine();
  1453. await mutexWrapOperation(async () => {
  1454. await this.drmEngine_.attach(this.video_);
  1455. }, 'drmEngine_.attach');
  1456. // Also get the ABR manager, which has special logic related to being
  1457. // received.
  1458. const abrManagerFactory = preloadManager.getAbrManagerFactory();
  1459. if (abrManagerFactory) {
  1460. if (!this.abrManagerFactory_ ||
  1461. this.abrManagerFactory_ != abrManagerFactory) {
  1462. this.abrManager_ = preloadManager.receiveAbrManager();
  1463. this.abrManagerFactory_ = preloadManager.getAbrManagerFactory();
  1464. if (typeof this.abrManager_.setMediaElement != 'function') {
  1465. shaka.Deprecate.deprecateFeature(5,
  1466. 'AbrManager w/o setMediaElement',
  1467. 'Please use an AbrManager with setMediaElement function.');
  1468. this.abrManager_.setMediaElement = () => {};
  1469. }
  1470. if (typeof this.abrManager_.setCmsdManager != 'function') {
  1471. shaka.Deprecate.deprecateFeature(5,
  1472. 'AbrManager w/o setCmsdManager',
  1473. 'Please use an AbrManager with setCmsdManager function.');
  1474. this.abrManager_.setCmsdManager = () => {};
  1475. }
  1476. if (typeof this.abrManager_.trySuggestStreams != 'function') {
  1477. shaka.Deprecate.deprecateFeature(5,
  1478. 'AbrManager w/o trySuggestStreams',
  1479. 'Please use an AbrManager with trySuggestStreams function.');
  1480. this.abrManager_.trySuggestStreams = () => {};
  1481. }
  1482. }
  1483. }
  1484. // Load the asset.
  1485. const segmentPrefetchById =
  1486. preloadManager.receiveSegmentPrefetchesById();
  1487. const prefetchedVariant = preloadManager.getPrefetchedVariant();
  1488. await mutexWrapOperation(async () => {
  1489. await this.loadInner_(
  1490. startTimeOfLoad, prefetchedVariant, segmentPrefetchById);
  1491. }, 'loadInner_');
  1492. preloadManager.stopQueuingLatePhaseQueuedOperations();
  1493. }
  1494. this.dispatchEvent(shaka.Player.makeEvent_(
  1495. shaka.util.FakeEvent.EventName.Loaded));
  1496. } catch (error) {
  1497. if (error.code != shaka.util.Error.Code.LOAD_INTERRUPTED) {
  1498. await this.unload(/* initializeMediaSource= */ false);
  1499. }
  1500. throw error;
  1501. } finally {
  1502. if (preloadManager) {
  1503. // This will cause any resources that were generated but not used to be
  1504. // properly destroyed or released.
  1505. await preloadManager.destroy();
  1506. }
  1507. this.preloadNextUrl_ = null;
  1508. }
  1509. }
  1510. /**
  1511. * Modifies the current manifest so that it is audio-only.
  1512. * @private
  1513. */
  1514. makeManifestAudioOnly_() {
  1515. for (const variant of this.manifest_.variants) {
  1516. if (variant.video) {
  1517. variant.video.closeSegmentIndex();
  1518. variant.video = null;
  1519. }
  1520. if (variant.audio && variant.audio.bandwidth) {
  1521. variant.bandwidth = variant.audio.bandwidth;
  1522. } else {
  1523. variant.bandwidth = 0;
  1524. }
  1525. }
  1526. this.manifest_.variants = this.manifest_.variants.filter((v) => {
  1527. return v.audio;
  1528. });
  1529. }
  1530. /**
  1531. * Unloads the currently playing stream, if any, and returns a PreloadManager
  1532. * that contains the loaded manifest of that asset, if any.
  1533. * Allows for the asset to be re-loaded by this player faster, in the future.
  1534. * When in src= mode, this unloads but does not make a PreloadManager.
  1535. *
  1536. * @param {boolean=} initializeMediaSource
  1537. * @param {boolean=} keepAdManager
  1538. * @return {!Promise.<?shaka.media.PreloadManager>}
  1539. * @export
  1540. */
  1541. async unloadAndSavePreload(
  1542. initializeMediaSource = true, keepAdManager = false) {
  1543. const preloadManager = await this.savePreload_();
  1544. await this.unload(initializeMediaSource, keepAdManager);
  1545. return preloadManager;
  1546. }
  1547. /**
  1548. * Detach the player from the current media element, if any, and returns a
  1549. * PreloadManager that contains the loaded manifest of that asset, if any.
  1550. * Allows for the asset to be re-loaded by this player faster, in the future.
  1551. * When in src= mode, this detach but does not make a PreloadManager.
  1552. * Leaves the player in a state where it cannot play media, until it has been
  1553. * attached to something else.
  1554. *
  1555. * @param {boolean=} keepAdManager
  1556. * @param {boolean=} saveLivePosition
  1557. * @return {!Promise.<?shaka.media.PreloadManager>}
  1558. * @export
  1559. */
  1560. async detachAndSavePreload(keepAdManager = false, saveLivePosition = false) {
  1561. const preloadManager = await this.savePreload_(saveLivePosition);
  1562. await this.detach(keepAdManager);
  1563. return preloadManager;
  1564. }
  1565. /**
  1566. * @param {boolean=} saveLivePosition
  1567. * @return {!Promise.<?shaka.media.PreloadManager>}
  1568. * @private
  1569. */
  1570. async savePreload_(saveLivePosition = false) {
  1571. let preloadManager = null;
  1572. if (this.manifest_ && this.parser_ && this.parserFactory_ &&
  1573. this.assetUri_) {
  1574. let startTime = this.video_.currentTime;
  1575. if (this.isLive() && !saveLivePosition) {
  1576. startTime = null;
  1577. }
  1578. // We have enough information to make a PreloadManager!
  1579. preloadManager = await this.makePreloadManager_(
  1580. this.assetUri_,
  1581. startTime,
  1582. this.mimeType_,
  1583. /* allowPrefetch= */ true,
  1584. /* disableVideo= */ false,
  1585. /* allowMakeAbrManager= */ false);
  1586. this.createdPreloadManagers_.push(preloadManager);
  1587. if (this.parser_ && this.parser_.setMediaElement) {
  1588. this.parser_.setMediaElement(/* mediaElement= */ null);
  1589. }
  1590. preloadManager.attachManifest(
  1591. this.manifest_, this.parser_, this.parserFactory_);
  1592. preloadManager.attachAbrManager(
  1593. this.abrManager_, this.abrManagerFactory_);
  1594. preloadManager.attachAdaptationSetCriteria(
  1595. this.currentAdaptationSetCriteria_);
  1596. preloadManager.start();
  1597. // Null the manifest and manifestParser, so that they won't be shut down
  1598. // during unload and will continue to live inside the preloadManager.
  1599. this.manifest_ = null;
  1600. this.parser_ = null;
  1601. this.parserFactory_ = null;
  1602. // Null the abrManager and abrManagerFactory, so that they won't be shut
  1603. // down during unload and will continue to live inside the preloadManager.
  1604. this.abrManager_ = null;
  1605. this.abrManagerFactory_ = null;
  1606. }
  1607. return preloadManager;
  1608. }
  1609. /**
  1610. * Starts to preload a given asset, and returns a PreloadManager object that
  1611. * represents that preloading process.
  1612. * The PreloadManager will load the manifest for that asset, as well as the
  1613. * initialization segment. It will not preload anything more than that;
  1614. * this feature is intended for reducing start-time latency, not for fully
  1615. * downloading assets before playing them (for that, use
  1616. * |shaka.offline.Storage|).
  1617. * You can pass that PreloadManager object in to the |load| method on this
  1618. * Player instance to finish loading that particular asset, or you can call
  1619. * the |destroy| method on the manager if the preload is no longer necessary.
  1620. * If this returns null rather than a PreloadManager, that indicates that the
  1621. * asset must be played with src=, which cannot be preloaded.
  1622. *
  1623. * @param {string} assetUri
  1624. * @param {?number=} startTime
  1625. * When <code>startTime</code> is <code>null</code> or
  1626. * <code>undefined</code>, playback will start at the default start time (0
  1627. * for VOD and liveEdge for LIVE).
  1628. * @param {?string=} mimeType
  1629. * @return {!Promise.<?shaka.media.PreloadManager>}
  1630. * @export
  1631. */
  1632. async preload(assetUri, startTime = null, mimeType) {
  1633. const preloadManager = await this.preloadInner_(
  1634. assetUri, startTime, mimeType);
  1635. if (!preloadManager) {
  1636. this.onError_(new shaka.util.Error(
  1637. shaka.util.Error.Severity.CRITICAL,
  1638. shaka.util.Error.Category.PLAYER,
  1639. shaka.util.Error.Code.SRC_EQUALS_PRELOAD_NOT_SUPPORTED));
  1640. } else {
  1641. preloadManager.start();
  1642. }
  1643. return preloadManager;
  1644. }
  1645. /**
  1646. * Calls |destroy| on each PreloadManager object this player has created.
  1647. * @export
  1648. */
  1649. async destroyAllPreloads() {
  1650. const preloadManagerDestroys = [];
  1651. for (const preloadManager of this.createdPreloadManagers_) {
  1652. if (!preloadManager.isDestroyed()) {
  1653. preloadManagerDestroys.push(preloadManager.destroy());
  1654. }
  1655. }
  1656. this.createdPreloadManagers_ = [];
  1657. await Promise.all(preloadManagerDestroys);
  1658. }
  1659. /**
  1660. * @param {string} assetUri
  1661. * @param {?number} startTime
  1662. * @param {?string=} mimeType
  1663. * @param {boolean=} standardLoad
  1664. * @return {!Promise.<?shaka.media.PreloadManager>}
  1665. * @private
  1666. */
  1667. async preloadInner_(assetUri, startTime, mimeType, standardLoad = false) {
  1668. goog.asserts.assert(this.networkingEngine_, 'Should have a net engine!');
  1669. goog.asserts.assert(this.config_, 'Config must not be null!');
  1670. if (!mimeType) {
  1671. mimeType = await this.guessMimeType_(assetUri);
  1672. }
  1673. const shouldUseSrcEquals = this.shouldUseSrcEquals_(assetUri, mimeType);
  1674. if (shouldUseSrcEquals) {
  1675. // We cannot preload src= content.
  1676. return null;
  1677. }
  1678. let disableVideo = false;
  1679. let allowMakeAbrManager = true;
  1680. if (standardLoad) {
  1681. if (this.abrManager_ &&
  1682. this.abrManagerFactory_ == this.config_.abrFactory) {
  1683. // If there's already an abr manager, don't make a new abr manager at
  1684. // all.
  1685. // In standardLoad mode, the abr manager isn't used for anything anyway,
  1686. // so it should only be created to create an abr manager for the player
  1687. // to use... which is unnecessary if we already have one of the right
  1688. // type.
  1689. allowMakeAbrManager = false;
  1690. }
  1691. if (this.video_ && this.video_.nodeName === 'AUDIO') {
  1692. disableVideo = true;
  1693. }
  1694. }
  1695. let preloadManagerPromise = this.makePreloadManager_(
  1696. assetUri, startTime, mimeType || null,
  1697. /* allowPrefetch= */ !standardLoad, disableVideo, allowMakeAbrManager);
  1698. if (!standardLoad) {
  1699. // We only need to track the PreloadManager if it is not part of a
  1700. // standard load. If it is, the load() method will handle destroying it.
  1701. // Adding a standard load PreloadManager to the createdPreloadManagers_
  1702. // array runs the risk that the user will call destroyAllPreloads and
  1703. // destroy that PreloadManager mid-load.
  1704. preloadManagerPromise = preloadManagerPromise.then((preloadManager) => {
  1705. this.createdPreloadManagers_.push(preloadManager);
  1706. return preloadManager;
  1707. });
  1708. }
  1709. return preloadManagerPromise;
  1710. }
  1711. /**
  1712. * @param {string} assetUri
  1713. * @param {?number} startTime
  1714. * @param {?string} mimeType
  1715. * @param {boolean=} allowPrefetch
  1716. * @param {boolean=} disableVideo
  1717. * @param {boolean=} allowMakeAbrManager
  1718. * @return {!Promise.<!shaka.media.PreloadManager>}
  1719. * @private
  1720. */
  1721. async makePreloadManager_(assetUri, startTime, mimeType,
  1722. allowPrefetch = true, disableVideo = false, allowMakeAbrManager = true) {
  1723. goog.asserts.assert(this.networkingEngine_, 'Must have net engine');
  1724. /** @type {?shaka.media.PreloadManager} */
  1725. let preloadManager = null;
  1726. const config = shaka.util.ObjectUtils.cloneObject(this.config_);
  1727. if (disableVideo) {
  1728. config.manifest.disableVideo = true;
  1729. }
  1730. const getPreloadManager = () => {
  1731. goog.asserts.assert(preloadManager, 'Must have preload manager');
  1732. if (preloadManager.hasBeenAttached() && preloadManager.isDestroyed()) {
  1733. return null;
  1734. }
  1735. return preloadManager;
  1736. };
  1737. const getConfig = () => {
  1738. if (getPreloadManager()) {
  1739. return getPreloadManager().getConfiguration();
  1740. } else {
  1741. return this.config_;
  1742. }
  1743. };
  1744. const setConfig = (name, value) => {
  1745. if (getPreloadManager()) {
  1746. preloadManager.configure(name, value);
  1747. } else {
  1748. this.configure(name, value);
  1749. }
  1750. };
  1751. // Avoid having to detect the resolution again if it has already been
  1752. // detected or set
  1753. if (this.maxHwRes_.width == Infinity &&
  1754. this.maxHwRes_.height == Infinity) {
  1755. const maxResolution =
  1756. await shaka.util.Platform.detectMaxHardwareResolution();
  1757. this.maxHwRes_.width = maxResolution.width;
  1758. this.maxHwRes_.height = maxResolution.height;
  1759. }
  1760. const manifestFilterer = new shaka.media.ManifestFilterer(
  1761. config, this.maxHwRes_, null);
  1762. const manifestPlayerInterface = {
  1763. networkingEngine: this.networkingEngine_,
  1764. filter: async (manifest) => {
  1765. const tracksChanged = await manifestFilterer.filterManifest(manifest);
  1766. if (tracksChanged) {
  1767. // Delay the 'trackschanged' event so StreamingEngine has time to
  1768. // absorb the changes before the user tries to query it.
  1769. const event = shaka.Player.makeEvent_(
  1770. shaka.util.FakeEvent.EventName.TracksChanged);
  1771. await Promise.resolve();
  1772. preloadManager.dispatchEvent(event);
  1773. }
  1774. },
  1775. makeTextStreamsForClosedCaptions: (manifest) => {
  1776. return this.makeTextStreamsForClosedCaptions_(manifest);
  1777. },
  1778. // Called when the parser finds a timeline region. This can be called
  1779. // before we start playback or during playback (live/in-progress
  1780. // manifest).
  1781. onTimelineRegionAdded: (region) => {
  1782. preloadManager.getRegionTimeline().addRegion(region);
  1783. },
  1784. onEvent: (event) => preloadManager.dispatchEvent(event),
  1785. onError: (error) => preloadManager.onError(error),
  1786. isLowLatencyMode: () => getConfig().streaming.lowLatencyMode,
  1787. isAutoLowLatencyMode: () => getConfig().streaming.autoLowLatencyMode,
  1788. enableLowLatencyMode: () => {
  1789. setConfig('streaming.lowLatencyMode', true);
  1790. },
  1791. updateDuration: () => {
  1792. if (this.streamingEngine_ && preloadManager.hasBeenAttached()) {
  1793. this.streamingEngine_.updateDuration();
  1794. }
  1795. },
  1796. newDrmInfo: (stream) => {
  1797. // We may need to create new sessions for any new init data.
  1798. const drmEngine = preloadManager.getDrmEngine();
  1799. const currentDrmInfo = drmEngine ? drmEngine.getDrmInfo() : null;
  1800. // DrmEngine.newInitData() requires mediaKeys to be available.
  1801. if (currentDrmInfo && drmEngine.getMediaKeys()) {
  1802. manifestFilterer.processDrmInfos(currentDrmInfo.keySystem, stream);
  1803. }
  1804. },
  1805. onManifestUpdated: () => {
  1806. const eventName = shaka.util.FakeEvent.EventName.ManifestUpdated;
  1807. const data = (new Map()).set('isLive', this.isLive());
  1808. preloadManager.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
  1809. preloadManager.addQueuedOperation(false, () => {
  1810. if (this.adManager_) {
  1811. this.adManager_.onManifestUpdated(this.isLive());
  1812. }
  1813. });
  1814. },
  1815. getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(),
  1816. onMetadata: (type, startTime, endTime, values) => {
  1817. let metadataType = type;
  1818. if (type == 'com.apple.hls.interstitial') {
  1819. metadataType = 'com.apple.quicktime.HLS';
  1820. /** @type {shaka.extern.Interstitial} */
  1821. const interstitial = {
  1822. startTime,
  1823. endTime,
  1824. values,
  1825. };
  1826. if (this.adManager_) {
  1827. goog.asserts.assert(this.video_, 'Must have video');
  1828. this.adManager_.onInterstitialMetadata(
  1829. this, this.video_, interstitial);
  1830. }
  1831. }
  1832. for (const payload of values) {
  1833. preloadManager.addQueuedOperation(false, () => {
  1834. this.dispatchMetadataEvent_(
  1835. startTime, endTime, metadataType, payload);
  1836. });
  1837. }
  1838. },
  1839. disableStream: (stream) => this.disableStream(
  1840. stream, this.config_.streaming.maxDisabledTime),
  1841. };
  1842. const regionTimeline =
  1843. new shaka.media.RegionTimeline(() => this.seekRange());
  1844. regionTimeline.addEventListener('regionadd', (event) => {
  1845. /** @type {shaka.extern.TimelineRegionInfo} */
  1846. const region = event['region'];
  1847. this.onRegionEvent_(
  1848. shaka.util.FakeEvent.EventName.TimelineRegionAdded, region,
  1849. preloadManager);
  1850. preloadManager.addQueuedOperation(false, () => {
  1851. if (this.adManager_) {
  1852. this.adManager_.onDashTimedMetadata(region);
  1853. }
  1854. });
  1855. });
  1856. let qualityObserver = null;
  1857. if (config.streaming.observeQualityChanges) {
  1858. qualityObserver = new shaka.media.QualityObserver(
  1859. () => this.getBufferedInfo());
  1860. qualityObserver.addEventListener('qualitychange', (event) => {
  1861. /** @type {shaka.extern.MediaQualityInfo} */
  1862. const mediaQualityInfo = event['quality'];
  1863. /** @type {number} */
  1864. const position = event['position'];
  1865. this.onMediaQualityChange_(mediaQualityInfo, position);
  1866. });
  1867. qualityObserver.addEventListener('audiotrackchange', (event) => {
  1868. /** @type {shaka.extern.MediaQualityInfo} */
  1869. const mediaQualityInfo = event['quality'];
  1870. /** @type {number} */
  1871. const position = event['position'];
  1872. this.onMediaQualityChange_(mediaQualityInfo, position,
  1873. /* audioTrackChanged= */ true);
  1874. });
  1875. }
  1876. let firstEvent = true;
  1877. const drmPlayerInterface = {
  1878. netEngine: this.networkingEngine_,
  1879. onError: (e) => preloadManager.onError(e),
  1880. onKeyStatus: (map) => {
  1881. preloadManager.addQueuedOperation(true, () => {
  1882. this.onKeyStatus_(map);
  1883. });
  1884. },
  1885. onExpirationUpdated: (id, expiration) => {
  1886. const event = shaka.Player.makeEvent_(
  1887. shaka.util.FakeEvent.EventName.ExpirationUpdated);
  1888. preloadManager.dispatchEvent(event);
  1889. const parser = preloadManager.getParser();
  1890. if (parser && parser.onExpirationUpdated) {
  1891. parser.onExpirationUpdated(id, expiration);
  1892. }
  1893. },
  1894. onEvent: (e) => {
  1895. preloadManager.dispatchEvent(e);
  1896. if (e.type == shaka.util.FakeEvent.EventName.DrmSessionUpdate &&
  1897. firstEvent) {
  1898. firstEvent = false;
  1899. const now = Date.now() / 1000;
  1900. const delta = now - preloadManager.getStartTimeOfDRM();
  1901. const stats = this.stats_ || preloadManager.getStats();
  1902. stats.setDrmTime(delta);
  1903. // LCEVC data by itself is not encrypted in DRM protected streams
  1904. // and can therefore be accessed and decoded as normal. However,
  1905. // the LCEVC decoder needs access to the VideoElement output in
  1906. // order to apply the enhancement. In DRM contexts where the
  1907. // browser CDM restricts access from our decoder, the enhancement
  1908. // cannot be applied and therefore the LCEVC output canvas is
  1909. // hidden accordingly.
  1910. if (this.lcevcDec_) {
  1911. this.lcevcDec_.hideCanvas();
  1912. }
  1913. }
  1914. },
  1915. };
  1916. // Sadly, as the network engine creation code must be replaceable by tests,
  1917. // it cannot be made and use the utilities defined in this function.
  1918. const networkingEngine = this.createNetworkingEngine(getPreloadManager);
  1919. this.networkingEngine_.copyFiltersInto(networkingEngine);
  1920. /** @return {!shaka.media.DrmEngine} */
  1921. const createDrmEngine = () => {
  1922. return this.createDrmEngine(drmPlayerInterface);
  1923. };
  1924. /** @type {!shaka.media.PreloadManager.PlayerInterface} */
  1925. const playerInterface = {
  1926. config,
  1927. manifestPlayerInterface,
  1928. regionTimeline,
  1929. qualityObserver,
  1930. createDrmEngine,
  1931. manifestFilterer,
  1932. networkingEngine,
  1933. allowPrefetch,
  1934. allowMakeAbrManager,
  1935. };
  1936. preloadManager = new shaka.media.PreloadManager(
  1937. assetUri, mimeType, startTime, playerInterface);
  1938. return preloadManager;
  1939. }
  1940. /**
  1941. * Determines the mimeType of the given asset, if we are not told that inside
  1942. * the loading process.
  1943. *
  1944. * @param {string} assetUri
  1945. * @return {!Promise.<?string>} mimeType
  1946. * @private
  1947. */
  1948. async guessMimeType_(assetUri) {
  1949. // If no MIME type is provided, and we can't base it on extension, make a
  1950. // HEAD request to determine it.
  1951. goog.asserts.assert(this.networkingEngine_, 'Should have a net engine!');
  1952. const retryParams = this.config_.manifest.retryParameters;
  1953. let mimeType = await shaka.net.NetworkingUtils.getMimeType(
  1954. assetUri, this.networkingEngine_, retryParams);
  1955. if (mimeType == 'application/x-mpegurl' && shaka.util.Platform.isApple()) {
  1956. mimeType = 'application/vnd.apple.mpegurl';
  1957. }
  1958. return mimeType;
  1959. }
  1960. /**
  1961. * Determines if we should use src equals, based on the the mimeType (if
  1962. * known), the URI, and platform information.
  1963. *
  1964. * @param {string} assetUri
  1965. * @param {?string=} mimeType
  1966. * @return {boolean}
  1967. * |true| if the content should be loaded with src=, |false| if the content
  1968. * should be loaded with MediaSource.
  1969. * @private
  1970. */
  1971. shouldUseSrcEquals_(assetUri, mimeType) {
  1972. const Platform = shaka.util.Platform;
  1973. const MimeUtils = shaka.util.MimeUtils;
  1974. // If we are using a platform that does not support media source, we will
  1975. // fall back to src= to handle all playback.
  1976. if (!Platform.supportsMediaSource()) {
  1977. return true;
  1978. }
  1979. if (mimeType) {
  1980. // If we have a MIME type, check if the browser can play it natively.
  1981. // This will cover both single files and native HLS.
  1982. const mediaElement = this.video_ || Platform.anyMediaElement();
  1983. const canPlayNatively = mediaElement.canPlayType(mimeType) != '';
  1984. // If we can't play natively, then src= isn't an option.
  1985. if (!canPlayNatively) {
  1986. return false;
  1987. }
  1988. const canPlayMediaSource =
  1989. shaka.media.ManifestParser.isSupported(mimeType);
  1990. // If MediaSource isn't an option, the native option is our only chance.
  1991. if (!canPlayMediaSource) {
  1992. return true;
  1993. }
  1994. // If we land here, both are feasible.
  1995. goog.asserts.assert(canPlayNatively && canPlayMediaSource,
  1996. 'Both native and MSE playback should be possible!');
  1997. // We would prefer MediaSource in some cases, and src= in others. For
  1998. // example, Android has native HLS, but we'd prefer our own MediaSource
  1999. // version there.
  2000. if (MimeUtils.isHlsType(mimeType)) {
  2001. // Native FairPlay HLS can be preferred on Apple platfforms.
  2002. if (Platform.isApple() &&
  2003. (this.config_.drm.servers['com.apple.fps'] ||
  2004. this.config_.drm.servers['com.apple.fps.1_0'])) {
  2005. return this.config_.streaming.useNativeHlsForFairPlay;
  2006. }
  2007. // Native HLS can be preferred on any platform via this flag:
  2008. return this.config_.streaming.preferNativeHls;
  2009. }
  2010. // In all other cases, we prefer MediaSource.
  2011. return false;
  2012. }
  2013. // Unless there are good reasons to use src= (single-file playback or native
  2014. // HLS), we prefer MediaSource. So the final return value for choosing src=
  2015. // is false.
  2016. return false;
  2017. }
  2018. /**
  2019. * Initializes the media source engine.
  2020. *
  2021. * @return {!Promise}
  2022. * @private
  2023. */
  2024. async initializeMediaSourceEngineInner_() {
  2025. goog.asserts.assert(
  2026. shaka.util.Platform.supportsMediaSource(),
  2027. 'We should not be initializing media source on a platform that ' +
  2028. 'does not support media source.');
  2029. goog.asserts.assert(
  2030. this.video_,
  2031. 'We should have a media element when initializing media source.');
  2032. goog.asserts.assert(
  2033. this.mediaSourceEngine_ == null,
  2034. 'We should not have a media source engine yet.');
  2035. this.makeStateChangeEvent_('media-source');
  2036. // When changing text visibility we need to update both the text displayer
  2037. // and streaming engine because we don't always stream text. To ensure
  2038. // that the text displayer and streaming engine are always in sync, wait
  2039. // until they are both initialized before setting the initial value.
  2040. const textDisplayerFactory = this.config_.textDisplayFactory;
  2041. const textDisplayer = textDisplayerFactory();
  2042. if (textDisplayer.configure) {
  2043. textDisplayer.configure(this.config_.textDisplayer);
  2044. } else {
  2045. shaka.Deprecate.deprecateFeature(5,
  2046. 'Text displayer w/ configure',
  2047. 'Text displayer should have a "configure" method!');
  2048. }
  2049. this.lastTextFactory_ = textDisplayerFactory;
  2050. const mediaSourceEngine = this.createMediaSourceEngine(
  2051. this.video_,
  2052. textDisplayer,
  2053. {
  2054. getKeySystem: () => this.keySystem(),
  2055. onMetadata: (metadata, offset, endTime) => {
  2056. this.processTimedMetadataMediaSrc_(metadata, offset, endTime);
  2057. },
  2058. },
  2059. this.lcevcDec_);
  2060. mediaSourceEngine.configure(this.config_.mediaSource);
  2061. const {segmentRelativeVttTiming} = this.config_.manifest;
  2062. mediaSourceEngine.setSegmentRelativeVttTiming(segmentRelativeVttTiming);
  2063. // Wait for media source engine to finish opening. This promise should
  2064. // NEVER be rejected as per the media source engine implementation.
  2065. await mediaSourceEngine.open();
  2066. // Wait until it is ready to actually store the reference.
  2067. this.mediaSourceEngine_ = mediaSourceEngine;
  2068. }
  2069. /**
  2070. * Starts loading the content described by the parsed manifest.
  2071. *
  2072. * @param {number} startTimeOfLoad
  2073. * @param {?shaka.extern.Variant} prefetchedVariant
  2074. * @param {!Map.<number, shaka.media.SegmentPrefetch>} segmentPrefetchById
  2075. * @return {!Promise}
  2076. * @private
  2077. */
  2078. async loadInner_(startTimeOfLoad, prefetchedVariant, segmentPrefetchById) {
  2079. goog.asserts.assert(
  2080. this.video_, 'We should have a media element by now.');
  2081. goog.asserts.assert(
  2082. this.manifest_, 'The manifest should already be parsed.');
  2083. goog.asserts.assert(
  2084. this.assetUri_, 'We should have an asset uri by now.');
  2085. goog.asserts.assert(
  2086. this.abrManager_, 'We should have an abr manager by now.');
  2087. this.makeStateChangeEvent_('load');
  2088. const mediaElement = this.video_;
  2089. this.playRateController_ = new shaka.media.PlayRateController({
  2090. getRate: () => mediaElement.playbackRate,
  2091. getDefaultRate: () => mediaElement.defaultPlaybackRate,
  2092. setRate: (rate) => { mediaElement.playbackRate = rate; },
  2093. movePlayhead: (delta) => { mediaElement.currentTime += delta; },
  2094. });
  2095. const updateStateHistory = () => this.updateStateHistory_();
  2096. const onRateChange = () => this.onRateChange_();
  2097. this.loadEventManager_.listen(
  2098. mediaElement, 'playing', updateStateHistory);
  2099. this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory);
  2100. this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory);
  2101. this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange);
  2102. // Check the status of the LCEVC Dec Object. Reset, create, or close
  2103. // depending on the config.
  2104. this.setupLcevc_(this.config_);
  2105. this.currentTextLanguage_ = this.config_.preferredTextLanguage;
  2106. this.currentTextRole_ = this.config_.preferredTextRole;
  2107. this.currentTextForced_ = this.config_.preferForcedSubs;
  2108. shaka.Player.applyPlayRange_(this.manifest_.presentationTimeline,
  2109. this.config_.playRangeStart,
  2110. this.config_.playRangeEnd);
  2111. this.abrManager_.init((variant, clearBuffer, safeMargin) => {
  2112. return this.switch_(variant, clearBuffer, safeMargin);
  2113. });
  2114. this.abrManager_.setMediaElement(mediaElement);
  2115. this.abrManager_.setCmsdManager(this.cmsdManager_);
  2116. this.streamingEngine_ = this.createStreamingEngine();
  2117. this.streamingEngine_.configure(this.config_.streaming);
  2118. // Set the load mode to "loaded with media source" as late as possible so
  2119. // that public methods won't try to access internal components until
  2120. // they're all initialized. We MUST switch to loaded before calling
  2121. // "streaming" so that they can access internal information.
  2122. this.loadMode_ = shaka.Player.LoadMode.MEDIA_SOURCE;
  2123. if (mediaElement.textTracks) {
  2124. this.loadEventManager_.listen(
  2125. mediaElement.textTracks, 'addtrack', (e) => {
  2126. const trackEvent = /** @type {!TrackEvent} */(e);
  2127. if (trackEvent.track) {
  2128. const track = trackEvent.track;
  2129. goog.asserts.assert(
  2130. track instanceof TextTrack, 'Wrong track type!');
  2131. switch (track.kind) {
  2132. case 'chapters':
  2133. this.activateChaptersTrack_(track);
  2134. break;
  2135. }
  2136. }
  2137. });
  2138. }
  2139. // The event must be fired after we filter by restrictions but before the
  2140. // active stream is picked to allow those listening for the "streaming"
  2141. // event to make changes before streaming starts.
  2142. this.dispatchEvent(shaka.Player.makeEvent_(
  2143. shaka.util.FakeEvent.EventName.Streaming));
  2144. // Pick the initial streams to play.
  2145. // Unless the user has already picked a variant, anyway, by calling
  2146. // selectVariantTrack before this loading stage.
  2147. let initialVariant = prefetchedVariant;
  2148. let toLazyLoad;
  2149. let activeVariant;
  2150. do {
  2151. activeVariant = this.streamingEngine_.getCurrentVariant();
  2152. if (!activeVariant && !initialVariant) {
  2153. initialVariant = this.chooseVariant_(/* initialSelection= */ true);
  2154. goog.asserts.assert(initialVariant, 'Must choose an initial variant!');
  2155. }
  2156. // Lazy-load the stream, so we will have enough info to make the playhead.
  2157. const createSegmentIndexPromises = [];
  2158. toLazyLoad = activeVariant || initialVariant;
  2159. for (const stream of [toLazyLoad.video, toLazyLoad.audio]) {
  2160. if (stream && !stream.segmentIndex) {
  2161. createSegmentIndexPromises.push(stream.createSegmentIndex());
  2162. }
  2163. }
  2164. if (createSegmentIndexPromises.length > 0) {
  2165. // eslint-disable-next-line no-await-in-loop
  2166. await Promise.all(createSegmentIndexPromises);
  2167. }
  2168. } while (!toLazyLoad || toLazyLoad.disabledUntilTime != 0);
  2169. if (this.parser_ && this.parser_.onInitialVariantChosen) {
  2170. this.parser_.onInitialVariantChosen(toLazyLoad);
  2171. }
  2172. if (this.manifest_.isLowLatency && !this.config_.streaming.lowLatencyMode) {
  2173. shaka.log.alwaysWarn('Low-latency live stream detected, but ' +
  2174. 'low-latency streaming mode is not enabled in Shaka Player. ' +
  2175. 'Set streaming.lowLatencyMode configuration to true, and see ' +
  2176. 'https://bit.ly/3clctcj for details.');
  2177. }
  2178. shaka.Player.applyPlayRange_(this.manifest_.presentationTimeline,
  2179. this.config_.playRangeStart,
  2180. this.config_.playRangeEnd);
  2181. this.streamingEngine_.applyPlayRange(
  2182. this.config_.playRangeStart, this.config_.playRangeEnd);
  2183. const setupPlayhead = (startTime) => {
  2184. this.playhead_ = this.createPlayhead(startTime);
  2185. this.playheadObservers_ =
  2186. this.createPlayheadObserversForMSE_(startTime);
  2187. // We need to start the buffer management code near the end because it
  2188. // will set the initial buffering state and that depends on other
  2189. // components being initialized.
  2190. const rebufferThreshold = Math.max(
  2191. this.manifest_.minBufferTime,
  2192. this.config_.streaming.rebufferingGoal);
  2193. this.startBufferManagement_(mediaElement, rebufferThreshold);
  2194. };
  2195. if (!this.config_.streaming.startAtSegmentBoundary) {
  2196. setupPlayhead(this.startTime_);
  2197. }
  2198. // Now we can switch to the initial variant.
  2199. if (!activeVariant) {
  2200. goog.asserts.assert(initialVariant,
  2201. 'Must have choosen an initial variant!');
  2202. // Now that we have initial streams, we may adjust the start time to
  2203. // align to a segment boundary.
  2204. if (this.config_.streaming.startAtSegmentBoundary) {
  2205. const timeline = this.manifest_.presentationTimeline;
  2206. let initialTime = this.startTime_ || this.video_.currentTime;
  2207. const seekRangeStart = timeline.getSeekRangeStart();
  2208. const seekRangeEnd = timeline.getSeekRangeEnd();
  2209. if (initialTime < seekRangeStart) {
  2210. initialTime = seekRangeStart;
  2211. } else if (initialTime > seekRangeEnd) {
  2212. initialTime = seekRangeEnd;
  2213. }
  2214. const startTime = await this.adjustStartTime_(
  2215. initialVariant, initialTime);
  2216. setupPlayhead(startTime);
  2217. }
  2218. this.switchVariant_(initialVariant, /* fromAdaptation= */ true,
  2219. /* clearBuffer= */ false, /* safeMargin= */ 0);
  2220. }
  2221. this.playhead_.ready();
  2222. // Decide if text should be shown automatically.
  2223. // similar to video/audio track, we would skip switch initial text track
  2224. // if user already pick text track (via selectTextTrack api)
  2225. const activeTextTrack = this.getTextTracks().find((t) => t.active);
  2226. if (!activeTextTrack) {
  2227. const initialTextStream = this.chooseTextStream_();
  2228. if (initialTextStream) {
  2229. this.addTextStreamToSwitchHistory_(
  2230. initialTextStream, /* fromAdaptation= */ true);
  2231. }
  2232. if (initialVariant) {
  2233. this.setInitialTextState_(initialVariant, initialTextStream);
  2234. }
  2235. // Don't initialize with a text stream unless we should be streaming
  2236. // text.
  2237. if (initialTextStream && this.shouldStreamText_()) {
  2238. this.streamingEngine_.switchTextStream(initialTextStream);
  2239. }
  2240. }
  2241. // Start streaming content. This will start the flow of content down to
  2242. // media source.
  2243. await this.streamingEngine_.start(segmentPrefetchById);
  2244. if (this.config_.abr.enabled) {
  2245. this.abrManager_.enable();
  2246. this.onAbrStatusChanged_();
  2247. }
  2248. // Dispatch a 'trackschanged' event now that all initial filtering is
  2249. // done.
  2250. this.onTracksChanged_();
  2251. // Now that we've filtered out variants that aren't compatible with the
  2252. // active one, update abr manager with filtered variants.
  2253. // NOTE: This may be unnecessary. We've already chosen one codec in
  2254. // chooseCodecsAndFilterManifest_ before we started streaming. But it
  2255. // doesn't hurt, and this will all change when we start using
  2256. // MediaCapabilities and codec switching.
  2257. // TODO(#1391): Re-evaluate with MediaCapabilities and codec switching.
  2258. this.updateAbrManagerVariants_();
  2259. const hasPrimary = this.manifest_.variants.some((v) => v.primary);
  2260. if (!this.config_.preferredAudioLanguage && !hasPrimary) {
  2261. shaka.log.warning('No preferred audio language set. ' +
  2262. 'We have chosen an arbitrary language initially');
  2263. }
  2264. const isLive = this.isLive();
  2265. if ((isLive && ((this.config_.streaming.liveSync &&
  2266. this.config_.streaming.liveSync.enabled) ||
  2267. this.manifest_.serviceDescription ||
  2268. this.config_.streaming.liveSync.panicMode)) ||
  2269. this.config_.streaming.vodDynamicPlaybackRate) {
  2270. const onTimeUpdate = () => this.onTimeUpdate_();
  2271. this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
  2272. }
  2273. if (!isLive) {
  2274. const onVideoProgress = () => this.onVideoProgress_();
  2275. this.loadEventManager_.listen(
  2276. mediaElement, 'timeupdate', onVideoProgress);
  2277. this.onVideoProgress_();
  2278. if (this.manifest_.nextUrl) {
  2279. if (this.config_.streaming.preloadNextUrlWindow > 0) {
  2280. const onTimeUpdate = async () => {
  2281. const timeToEnd = this.video_.duration - this.video_.currentTime;
  2282. if (!isNaN(timeToEnd)) {
  2283. if (timeToEnd <= this.config_.streaming.preloadNextUrlWindow) {
  2284. this.loadEventManager_.unlisten(
  2285. mediaElement, 'timeupdate', onTimeUpdate);
  2286. goog.asserts.assert(this.manifest_.nextUrl,
  2287. 'this.manifest_.nextUrl should be valid.');
  2288. this.preloadNextUrl_ =
  2289. await this.preload(this.manifest_.nextUrl);
  2290. }
  2291. }
  2292. };
  2293. this.loadEventManager_.listen(
  2294. mediaElement, 'timeupdate', onTimeUpdate);
  2295. }
  2296. this.loadEventManager_.listen(mediaElement, 'ended', () => {
  2297. this.load(this.preloadNextUrl_ || this.manifest_.nextUrl);
  2298. });
  2299. }
  2300. }
  2301. if (this.adManager_) {
  2302. this.adManager_.onManifestUpdated(isLive);
  2303. }
  2304. this.fullyLoaded_ = true;
  2305. // Wait for the 'loadedmetadata' event to measure load() latency.
  2306. this.loadEventManager_.listenOnce(mediaElement, 'loadedmetadata', () => {
  2307. const now = Date.now() / 1000;
  2308. const delta = now - startTimeOfLoad;
  2309. this.stats_.setLoadLatency(delta);
  2310. });
  2311. }
  2312. /**
  2313. * Initializes the DRM engine for use by src equals.
  2314. *
  2315. * @param {string} mimeType
  2316. * @return {!Promise}
  2317. * @private
  2318. */
  2319. async initializeSrcEqualsDrmInner_(mimeType) {
  2320. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  2321. goog.asserts.assert(
  2322. this.networkingEngine_,
  2323. '|onInitializeSrcEqualsDrm_| should never be called after |destroy|');
  2324. goog.asserts.assert(
  2325. this.config_,
  2326. '|onInitializeSrcEqualsDrm_| should never be called after |destroy|');
  2327. const startTime = Date.now() / 1000;
  2328. let firstEvent = true;
  2329. this.drmEngine_ = this.createDrmEngine({
  2330. netEngine: this.networkingEngine_,
  2331. onError: (e) => {
  2332. this.onError_(e);
  2333. },
  2334. onKeyStatus: (map) => {
  2335. // According to this.onKeyStatus_, we can't even use this information
  2336. // in src= mode, so this is just a no-op.
  2337. },
  2338. onExpirationUpdated: (id, expiration) => {
  2339. const event = shaka.Player.makeEvent_(
  2340. shaka.util.FakeEvent.EventName.ExpirationUpdated);
  2341. this.dispatchEvent(event);
  2342. },
  2343. onEvent: (e) => {
  2344. this.dispatchEvent(e);
  2345. if (e.type == shaka.util.FakeEvent.EventName.DrmSessionUpdate &&
  2346. firstEvent) {
  2347. firstEvent = false;
  2348. const now = Date.now() / 1000;
  2349. const delta = now - startTime;
  2350. this.stats_.setDrmTime(delta);
  2351. }
  2352. },
  2353. });
  2354. this.drmEngine_.configure(this.config_.drm);
  2355. // TODO: Instead of feeding DrmEngine with Variants, we should refactor
  2356. // DrmEngine so that it takes a minimal config derived from Variants. In
  2357. // cases like this one or in removal of stored content, the details are
  2358. // largely unimportant. We should have a saner way to initialize
  2359. // DrmEngine.
  2360. // That would also insulate DrmEngine from manifest changes in the future.
  2361. // For now, that is time-consuming and this synthetic Variant is easy, so
  2362. // I'm putting it off. Since this is only expected to be used for native
  2363. // HLS in Safari, this should be safe. -JCP
  2364. /** @type {shaka.extern.Variant} */
  2365. const variant = {
  2366. id: 0,
  2367. language: 'und',
  2368. disabledUntilTime: 0,
  2369. primary: false,
  2370. audio: null,
  2371. video: null,
  2372. bandwidth: 100,
  2373. allowedByApplication: true,
  2374. allowedByKeySystem: true,
  2375. decodingInfos: [],
  2376. };
  2377. const stream = {
  2378. id: 0,
  2379. originalId: null,
  2380. groupId: null,
  2381. createSegmentIndex: () => Promise.resolve(),
  2382. segmentIndex: null,
  2383. mimeType: mimeType ? shaka.util.MimeUtils.getBasicType(mimeType) : '',
  2384. codecs: mimeType ? shaka.util.MimeUtils.getCodecs(mimeType) : '',
  2385. encrypted: true,
  2386. drmInfos: [], // Filled in by DrmEngine config.
  2387. keyIds: new Set(),
  2388. language: 'und',
  2389. originalLanguage: null,
  2390. label: null,
  2391. type: ContentType.VIDEO,
  2392. primary: false,
  2393. trickModeVideo: null,
  2394. emsgSchemeIdUris: null,
  2395. roles: [],
  2396. forced: false,
  2397. channelsCount: null,
  2398. audioSamplingRate: null,
  2399. spatialAudio: false,
  2400. closedCaptions: null,
  2401. accessibilityPurpose: null,
  2402. external: false,
  2403. fastSwitching: false,
  2404. fullMimeTypes: new Set(),
  2405. };
  2406. stream.fullMimeTypes.add(shaka.util.MimeUtils.getFullType(
  2407. stream.mimeType, stream.codecs));
  2408. if (mimeType.startsWith('audio/')) {
  2409. stream.type = ContentType.AUDIO;
  2410. variant.audio = stream;
  2411. } else {
  2412. variant.video = stream;
  2413. }
  2414. this.drmEngine_.setSrcEquals(/* srcEquals= */ true);
  2415. await this.drmEngine_.initForPlayback(
  2416. [variant], /* offlineSessionIds= */ []);
  2417. await this.drmEngine_.attach(this.video_);
  2418. }
  2419. /**
  2420. * Passes the asset URI along to the media element, so it can be played src
  2421. * equals style.
  2422. *
  2423. * @param {number} startTimeOfLoad
  2424. * @param {string} mimeType
  2425. * @return {!Promise}
  2426. *
  2427. * @private
  2428. */
  2429. async srcEqualsInner_(startTimeOfLoad, mimeType) {
  2430. this.makeStateChangeEvent_('src-equals');
  2431. goog.asserts.assert(
  2432. this.video_, 'We should have a media element when loading.');
  2433. goog.asserts.assert(
  2434. this.assetUri_, 'We should have a valid uri when loading.');
  2435. const mediaElement = this.video_;
  2436. this.playhead_ = new shaka.media.SrcEqualsPlayhead(mediaElement);
  2437. // This flag is used below in the language preference setup to check if
  2438. // this load was canceled before the necessary awaits completed.
  2439. let unloaded = false;
  2440. this.cleanupOnUnload_.push(() => {
  2441. unloaded = true;
  2442. });
  2443. if (this.startTime_ != null) {
  2444. this.playhead_.setStartTime(this.startTime_);
  2445. }
  2446. this.playRateController_ = new shaka.media.PlayRateController({
  2447. getRate: () => mediaElement.playbackRate,
  2448. getDefaultRate: () => mediaElement.defaultPlaybackRate,
  2449. setRate: (rate) => { mediaElement.playbackRate = rate; },
  2450. movePlayhead: (delta) => { mediaElement.currentTime += delta; },
  2451. });
  2452. // We need to start the buffer management code near the end because it
  2453. // will set the initial buffering state and that depends on other
  2454. // components being initialized.
  2455. const rebufferThreshold = this.config_.streaming.rebufferingGoal;
  2456. this.startBufferManagement_(mediaElement, rebufferThreshold);
  2457. // Add all media element listeners.
  2458. const updateStateHistory = () => this.updateStateHistory_();
  2459. const onRateChange = () => this.onRateChange_();
  2460. this.loadEventManager_.listen(
  2461. mediaElement, 'playing', updateStateHistory);
  2462. this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory);
  2463. this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory);
  2464. this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange);
  2465. // Wait for the 'loadedmetadata' event to measure load() latency, but only
  2466. // if preload is set in a way that would result in this event firing
  2467. // automatically.
  2468. // See https://github.com/shaka-project/shaka-player/issues/2483
  2469. if (mediaElement.preload != 'none') {
  2470. this.loadEventManager_.listenOnce(
  2471. mediaElement, 'loadedmetadata', () => {
  2472. const now = Date.now() / 1000;
  2473. const delta = now - startTimeOfLoad;
  2474. this.stats_.setLoadLatency(delta);
  2475. });
  2476. }
  2477. // The audio tracks are only available on Safari at the moment, but this
  2478. // drives the tracks API for Safari's native HLS. So when they change,
  2479. // fire the corresponding Shaka Player event.
  2480. if (mediaElement.audioTracks) {
  2481. this.loadEventManager_.listen(mediaElement.audioTracks, 'addtrack',
  2482. () => this.onTracksChanged_());
  2483. this.loadEventManager_.listen(mediaElement.audioTracks, 'removetrack',
  2484. () => this.onTracksChanged_());
  2485. this.loadEventManager_.listen(mediaElement.audioTracks, 'change',
  2486. () => this.onTracksChanged_());
  2487. }
  2488. if (mediaElement.textTracks) {
  2489. this.loadEventManager_.listen(
  2490. mediaElement.textTracks, 'addtrack', (e) => {
  2491. const trackEvent = /** @type {!TrackEvent} */(e);
  2492. if (trackEvent.track) {
  2493. const track = trackEvent.track;
  2494. goog.asserts.assert(
  2495. track instanceof TextTrack, 'Wrong track type!');
  2496. switch (track.kind) {
  2497. case 'metadata':
  2498. this.processTimedMetadataSrcEqls_(track);
  2499. break;
  2500. case 'chapters':
  2501. this.activateChaptersTrack_(track);
  2502. break;
  2503. default:
  2504. this.onTracksChanged_();
  2505. break;
  2506. }
  2507. }
  2508. });
  2509. this.loadEventManager_.listen(
  2510. mediaElement.textTracks, 'removetrack',
  2511. () => this.onTracksChanged_());
  2512. this.loadEventManager_.listen(
  2513. mediaElement.textTracks, 'change',
  2514. () => this.onTracksChanged_());
  2515. }
  2516. // By setting |src| we are done "loading" with src=. We don't need to set
  2517. // the current time because |playhead| will do that for us.
  2518. mediaElement.src = this.cmcdManager_.appendSrcData(
  2519. this.assetUri_, mimeType);
  2520. // Tizen 3 / WebOS won't load anything unless you call load() explicitly,
  2521. // no matter the value of the preload attribute. This is harmful on some
  2522. // other platforms by triggering unbounded loading of media data, but is
  2523. // necessary here.
  2524. if (shaka.util.Platform.isTizen() || shaka.util.Platform.isWebOS()) {
  2525. mediaElement.load();
  2526. }
  2527. // In Safari using HLS won't load anything unless you call load()
  2528. // explicitly, no matter the value of the preload attribute.
  2529. // Note: this only happens when there are not autoplay.
  2530. if (mediaElement.preload != 'none' && !mediaElement.autoplay &&
  2531. shaka.util.MimeUtils.isHlsType(mimeType) &&
  2532. shaka.util.Platform.safariVersion()) {
  2533. mediaElement.load();
  2534. }
  2535. // Set the load mode last so that we know that all our components are
  2536. // initialized.
  2537. this.loadMode_ = shaka.Player.LoadMode.SRC_EQUALS;
  2538. // The event doesn't mean as much for src= playback, since we don't
  2539. // control streaming. But we should fire it in this path anyway since
  2540. // some applications may be expecting it as a life-cycle event.
  2541. this.dispatchEvent(shaka.Player.makeEvent_(
  2542. shaka.util.FakeEvent.EventName.Streaming));
  2543. // The "load" Promise is resolved when we have loaded the metadata. If we
  2544. // wait for the full data, that won't happen on Safari until the play
  2545. // button is hit.
  2546. const fullyLoaded = new shaka.util.PublicPromise();
  2547. shaka.util.MediaReadyState.waitForReadyState(mediaElement,
  2548. HTMLMediaElement.HAVE_METADATA,
  2549. this.loadEventManager_,
  2550. () => {
  2551. this.playhead_.ready();
  2552. fullyLoaded.resolve();
  2553. });
  2554. // We can't switch to preferred languages, though, until the data is
  2555. // loaded.
  2556. shaka.util.MediaReadyState.waitForReadyState(mediaElement,
  2557. HTMLMediaElement.HAVE_CURRENT_DATA,
  2558. this.loadEventManager_,
  2559. async () => {
  2560. this.setupPreferredAudioOnSrc_();
  2561. // Applying the text preference too soon can result in it being
  2562. // reverted. Wait for native HLS to pick something first.
  2563. const textTracks = this.getFilteredTextTracks_();
  2564. if (!textTracks.find((t) => t.mode != 'disabled')) {
  2565. await new Promise((resolve) => {
  2566. this.loadEventManager_.listenOnce(
  2567. mediaElement.textTracks, 'change', resolve);
  2568. // We expect the event to fire because it does on Safari.
  2569. // But in case it doesn't on some other platform or future
  2570. // version, move on in 1 second no matter what. This keeps the
  2571. // language settings from being completely ignored if something
  2572. // goes wrong.
  2573. new shaka.util.Timer(resolve).tickAfter(1);
  2574. });
  2575. } else if (textTracks.length > 0) {
  2576. this.isTextVisible_ = true;
  2577. }
  2578. // If we have moved on to another piece of content while waiting for
  2579. // the above event/timer, we should not change tracks here.
  2580. if (unloaded) {
  2581. return;
  2582. }
  2583. this.setupPreferredTextOnSrc_();
  2584. });
  2585. if (mediaElement.error) {
  2586. // Already failed!
  2587. fullyLoaded.reject(this.videoErrorToShakaError_());
  2588. } else if (mediaElement.preload == 'none') {
  2589. shaka.log.alwaysWarn(
  2590. 'With <video preload="none">, the browser will not load anything ' +
  2591. 'until play() is called. We are unable to measure load latency ' +
  2592. 'in a meaningful way, and we cannot provide track info yet. ' +
  2593. 'Please do not use preload="none" with Shaka Player.');
  2594. // We can't wait for an event load loadedmetadata, since that will be
  2595. // blocked until a user interaction. So resolve the Promise now.
  2596. fullyLoaded.resolve();
  2597. }
  2598. this.loadEventManager_.listenOnce(mediaElement, 'error', () => {
  2599. fullyLoaded.reject(this.videoErrorToShakaError_());
  2600. });
  2601. const timeout = new Promise((resolve, reject) => {
  2602. const timer = new shaka.util.Timer(reject);
  2603. timer.tickAfter(this.config_.streaming.loadTimeout);
  2604. });
  2605. await Promise.race([
  2606. fullyLoaded,
  2607. timeout,
  2608. ]);
  2609. const isLive = this.isLive();
  2610. if ((isLive && ((this.config_.streaming.liveSync &&
  2611. this.config_.streaming.liveSync.enabled) ||
  2612. this.config_.streaming.liveSync.panicMode)) ||
  2613. this.config_.streaming.vodDynamicPlaybackRate) {
  2614. const onTimeUpdate = () => this.onTimeUpdate_();
  2615. this.loadEventManager_.listen(mediaElement, 'timeupdate', onTimeUpdate);
  2616. }
  2617. if (!isLive) {
  2618. const onVideoProgress = () => this.onVideoProgress_();
  2619. this.loadEventManager_.listen(
  2620. mediaElement, 'timeupdate', onVideoProgress);
  2621. this.onVideoProgress_();
  2622. }
  2623. if (this.adManager_) {
  2624. this.adManager_.onManifestUpdated(isLive);
  2625. // There is no good way to detect when the manifest has been updated,
  2626. // so we use seekRange().end so we can tell when it has been updated.
  2627. if (isLive) {
  2628. let prevSeekRangeEnd = this.seekRange().end;
  2629. this.loadEventManager_.listen(mediaElement, 'progress', () => {
  2630. const newSeekRangeEnd = this.seekRange().end;
  2631. if (prevSeekRangeEnd != newSeekRangeEnd) {
  2632. this.adManager_.onManifestUpdated(this.isLive());
  2633. prevSeekRangeEnd = newSeekRangeEnd;
  2634. }
  2635. });
  2636. }
  2637. }
  2638. this.fullyLoaded_ = true;
  2639. }
  2640. /**
  2641. * This method setup the preferred audio using src=..
  2642. *
  2643. * @private
  2644. */
  2645. setupPreferredAudioOnSrc_() {
  2646. const preferredAudioLanguage = this.config_.preferredAudioLanguage;
  2647. // If the user has not selected a preference, the browser preference is
  2648. // left.
  2649. if (preferredAudioLanguage == '') {
  2650. return;
  2651. }
  2652. const preferredVariantRole = this.config_.preferredVariantRole;
  2653. this.selectAudioLanguage(preferredAudioLanguage, preferredVariantRole);
  2654. }
  2655. /**
  2656. * This method setup the preferred text using src=.
  2657. *
  2658. * @private
  2659. */
  2660. setupPreferredTextOnSrc_() {
  2661. const preferredTextLanguage = this.config_.preferredTextLanguage;
  2662. // If the user has not selected a preference, the browser preference is
  2663. // left.
  2664. if (preferredTextLanguage == '') {
  2665. return;
  2666. }
  2667. const preferForcedSubs = this.config_.preferForcedSubs;
  2668. const preferredTextRole = this.config_.preferredTextRole;
  2669. this.selectTextLanguage(preferredTextLanguage, preferredTextRole,
  2670. preferForcedSubs);
  2671. }
  2672. /**
  2673. * We're looking for metadata tracks to process id3 tags. One of the uses is
  2674. * for ad info on LIVE streams
  2675. *
  2676. * @param {!TextTrack} track
  2677. * @private
  2678. */
  2679. processTimedMetadataSrcEqls_(track) {
  2680. if (track.kind != 'metadata') {
  2681. return;
  2682. }
  2683. // Hidden mode is required for the cuechange event to launch correctly
  2684. track.mode = 'hidden';
  2685. this.loadEventManager_.listen(track, 'cuechange', () => {
  2686. if (!track.activeCues) {
  2687. return;
  2688. }
  2689. /** @type {!Array.<shaka.extern.Interstitial>} */
  2690. const interstitials = [];
  2691. for (const cue of track.activeCues) {
  2692. this.dispatchMetadataEvent_(cue.startTime, cue.endTime,
  2693. cue.type, cue.value);
  2694. if (this.adManager_) {
  2695. this.adManager_.onCueMetadataChange(cue.value);
  2696. }
  2697. if (cue.type == 'com.apple.quicktime.HLS' && cue.startTime != null) {
  2698. let interstitial = interstitials.find((i) => {
  2699. return i.startTime == cue.startTime && i.endTime == cue.endTime;
  2700. });
  2701. if (!interstitial) {
  2702. interstitial = /** @type {shaka.extern.Interstitial} */ ({
  2703. startTime: cue.startTime,
  2704. endTime: cue.endTime,
  2705. values: [],
  2706. });
  2707. interstitials.push(interstitial);
  2708. }
  2709. interstitial.values.push(cue.value);
  2710. }
  2711. }
  2712. for (const interstitial of interstitials) {
  2713. const isValidInterstitial = interstitial.values.some((value) => {
  2714. return value.key == 'X-ASSET-URI' || value.key == 'X-ASSET-LIST';
  2715. });
  2716. if (!isValidInterstitial) {
  2717. continue;
  2718. }
  2719. if (this.adManager_) {
  2720. // It seems that CUE is natively omitted, by default we use CUE=ONCE
  2721. // to avoid repeating them.
  2722. interstitial.values.push({
  2723. key: 'CUE',
  2724. description: '',
  2725. data: 'ONCE',
  2726. mimeType: null,
  2727. pictureType: null,
  2728. });
  2729. goog.asserts.assert(this.video_, 'Must have video');
  2730. this.adManager_.onInterstitialMetadata(
  2731. this, this.video_, interstitial);
  2732. }
  2733. }
  2734. });
  2735. // In Safari the initial assignment does not always work, so we schedule
  2736. // this process to be repeated several times to ensure that it has been put
  2737. // in the correct mode.
  2738. const timer = new shaka.util.Timer(() => {
  2739. const textTracks = this.getMetadataTracks_();
  2740. for (const textTrack of textTracks) {
  2741. textTrack.mode = 'hidden';
  2742. }
  2743. }).tickNow().tickAfter(0.5);
  2744. this.cleanupOnUnload_.push(() => {
  2745. timer.stop();
  2746. });
  2747. }
  2748. /**
  2749. * @param {!Array.<shaka.extern.ID3Metadata>} metadata
  2750. * @param {number} offset
  2751. * @param {?number} segmentEndTime
  2752. * @private
  2753. */
  2754. processTimedMetadataMediaSrc_(metadata, offset, segmentEndTime) {
  2755. for (const sample of metadata) {
  2756. if (sample.data && sample.cueTime && sample.frames) {
  2757. const start = sample.cueTime + offset;
  2758. let end = segmentEndTime;
  2759. // This can happen when the ID3 info arrives in a previous segment.
  2760. if (end && start > end) {
  2761. end = start;
  2762. }
  2763. const metadataType = 'org.id3';
  2764. for (const frame of sample.frames) {
  2765. const payload = frame;
  2766. this.dispatchMetadataEvent_(start, end, metadataType, payload);
  2767. }
  2768. if (this.adManager_) {
  2769. this.adManager_.onHlsTimedMetadata(sample, start);
  2770. }
  2771. }
  2772. }
  2773. }
  2774. /**
  2775. * Construct and fire a Player.Metadata event
  2776. *
  2777. * @param {number} startTime
  2778. * @param {?number} endTime
  2779. * @param {string} metadataType
  2780. * @param {shaka.extern.MetadataFrame} payload
  2781. * @private
  2782. */
  2783. dispatchMetadataEvent_(startTime, endTime, metadataType, payload) {
  2784. goog.asserts.assert(!endTime || startTime <= endTime,
  2785. 'Metadata start time should be less or equal to the end time!');
  2786. const eventName = shaka.util.FakeEvent.EventName.Metadata;
  2787. const data = new Map()
  2788. .set('startTime', startTime)
  2789. .set('endTime', endTime)
  2790. .set('metadataType', metadataType)
  2791. .set('payload', payload);
  2792. this.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
  2793. }
  2794. /**
  2795. * Set the mode on a chapters track so that it loads.
  2796. *
  2797. * @param {?TextTrack} track
  2798. * @private
  2799. */
  2800. activateChaptersTrack_(track) {
  2801. if (!track || track.kind != 'chapters') {
  2802. return;
  2803. }
  2804. // Hidden mode is required for the cuechange event to launch correctly and
  2805. // get the cues and the activeCues
  2806. track.mode = 'hidden';
  2807. // In Safari the initial assignment does not always work, so we schedule
  2808. // this process to be repeated several times to ensure that it has been put
  2809. // in the correct mode.
  2810. const timer = new shaka.util.Timer(() => {
  2811. track.mode = 'hidden';
  2812. }).tickNow().tickAfter(0.5);
  2813. this.cleanupOnUnload_.push(() => {
  2814. timer.stop();
  2815. });
  2816. }
  2817. /**
  2818. * Releases all of the mutexes of the player. Meant for use by the tests.
  2819. * @export
  2820. */
  2821. releaseAllMutexes() {
  2822. this.mutex_.releaseAll();
  2823. }
  2824. /**
  2825. * Create a new DrmEngine instance. This may be replaced by tests to create
  2826. * fake instances. Configuration and initialization will be handled after
  2827. * |createDrmEngine|.
  2828. *
  2829. * @param {shaka.media.DrmEngine.PlayerInterface} playerInterface
  2830. * @return {!shaka.media.DrmEngine}
  2831. */
  2832. createDrmEngine(playerInterface) {
  2833. return new shaka.media.DrmEngine(playerInterface);
  2834. }
  2835. /**
  2836. * Creates a new instance of NetworkingEngine. This can be replaced by tests
  2837. * to create fake instances instead.
  2838. *
  2839. * @param {(function():?shaka.media.PreloadManager)=} getPreloadManager
  2840. * @return {!shaka.net.NetworkingEngine}
  2841. */
  2842. createNetworkingEngine(getPreloadManager) {
  2843. if (!getPreloadManager) {
  2844. getPreloadManager = () => null;
  2845. }
  2846. const getAbrManager = () => {
  2847. if (getPreloadManager()) {
  2848. return getPreloadManager().getAbrManager();
  2849. } else {
  2850. return this.abrManager_;
  2851. }
  2852. };
  2853. const getParser = () => {
  2854. if (getPreloadManager()) {
  2855. return getPreloadManager().getParser();
  2856. } else {
  2857. return this.parser_;
  2858. }
  2859. };
  2860. const lateQueue = (fn) => {
  2861. if (getPreloadManager()) {
  2862. getPreloadManager().addQueuedOperation(true, fn);
  2863. } else {
  2864. fn();
  2865. }
  2866. };
  2867. const dispatchEvent = (event) => {
  2868. if (getPreloadManager()) {
  2869. getPreloadManager().dispatchEvent(event);
  2870. } else {
  2871. this.dispatchEvent(event);
  2872. }
  2873. };
  2874. const getStats = () => {
  2875. if (getPreloadManager()) {
  2876. return getPreloadManager().getStats();
  2877. } else {
  2878. return this.stats_;
  2879. }
  2880. };
  2881. /** @type {shaka.net.NetworkingEngine.onProgressUpdated} */
  2882. const onProgressUpdated_ = (deltaTimeMs,
  2883. bytesDownloaded, allowSwitch, request) => {
  2884. // In some situations, such as during offline storage, the abr manager
  2885. // might not yet exist. Therefore, we need to check if abr manager has
  2886. // been initialized before using it.
  2887. const abrManager = getAbrManager();
  2888. if (abrManager) {
  2889. abrManager.segmentDownloaded(deltaTimeMs, bytesDownloaded,
  2890. allowSwitch, request);
  2891. }
  2892. };
  2893. /** @type {shaka.net.NetworkingEngine.OnHeadersReceived} */
  2894. const onHeadersReceived_ = (headers, request, requestType) => {
  2895. // Release a 'downloadheadersreceived' event.
  2896. const name = shaka.util.FakeEvent.EventName.DownloadHeadersReceived;
  2897. const data = new Map()
  2898. .set('headers', headers)
  2899. .set('request', request)
  2900. .set('requestType', requestType);
  2901. dispatchEvent(shaka.Player.makeEvent_(name, data));
  2902. lateQueue(() => {
  2903. if (this.cmsdManager_) {
  2904. this.cmsdManager_.processHeaders(headers);
  2905. }
  2906. });
  2907. };
  2908. /** @type {shaka.net.NetworkingEngine.OnDownloadFailed} */
  2909. const onDownloadFailed_ = (request, error, httpResponseCode, aborted) => {
  2910. // Release a 'downloadfailed' event.
  2911. const name = shaka.util.FakeEvent.EventName.DownloadFailed;
  2912. const data = new Map()
  2913. .set('request', request)
  2914. .set('error', error)
  2915. .set('httpResponseCode', httpResponseCode)
  2916. .set('aborted', aborted);
  2917. dispatchEvent(shaka.Player.makeEvent_(name, data));
  2918. };
  2919. /** @type {shaka.net.NetworkingEngine.OnRequest} */
  2920. const onRequest_ = (type, request, context) => {
  2921. lateQueue(() => {
  2922. this.cmcdManager_.applyData(type, request, context);
  2923. });
  2924. };
  2925. /** @type {shaka.net.NetworkingEngine.OnRetry} */
  2926. const onRetry_ = (type, context, newUrl, oldUrl) => {
  2927. const parser = getParser();
  2928. if (parser && parser.banLocation) {
  2929. parser.banLocation(oldUrl);
  2930. }
  2931. };
  2932. /** @type {shaka.net.NetworkingEngine.OnResponse} */
  2933. const onResponse_ = (type, response, context) => {
  2934. if (response.data) {
  2935. const bytesDownloaded = response.data.byteLength;
  2936. const stats = getStats();
  2937. if (stats) {
  2938. stats.addBytesDownloaded(bytesDownloaded);
  2939. if (type === shaka.net.NetworkingEngine.RequestType.MANIFEST) {
  2940. stats.setManifestSize(bytesDownloaded);
  2941. }
  2942. }
  2943. }
  2944. };
  2945. return new shaka.net.NetworkingEngine(
  2946. onProgressUpdated_, onHeadersReceived_, onDownloadFailed_, onRequest_,
  2947. onRetry_, onResponse_);
  2948. }
  2949. /**
  2950. * Creates a new instance of Playhead. This can be replaced by tests to
  2951. * create fake instances instead.
  2952. *
  2953. * @param {?number} startTime
  2954. * @return {!shaka.media.Playhead}
  2955. */
  2956. createPlayhead(startTime) {
  2957. goog.asserts.assert(this.manifest_, 'Must have manifest');
  2958. goog.asserts.assert(this.video_, 'Must have video');
  2959. return new shaka.media.MediaSourcePlayhead(
  2960. this.video_,
  2961. this.manifest_,
  2962. this.config_.streaming,
  2963. startTime,
  2964. () => this.onSeek_(),
  2965. (event) => this.dispatchEvent(event));
  2966. }
  2967. /**
  2968. * Create the observers for MSE playback. These observers are responsible for
  2969. * notifying the app and player of specific events during MSE playback.
  2970. *
  2971. * @param {number} startTime
  2972. * @return {!shaka.media.PlayheadObserverManager}
  2973. * @private
  2974. */
  2975. createPlayheadObserversForMSE_(startTime) {
  2976. goog.asserts.assert(this.manifest_, 'Must have manifest');
  2977. goog.asserts.assert(this.regionTimeline_, 'Must have region timeline');
  2978. goog.asserts.assert(this.video_, 'Must have video element');
  2979. const startsPastZero = this.isLive() || startTime > 0;
  2980. // Create the region observer. This will allow us to notify the app when we
  2981. // move in and out of timeline regions.
  2982. const regionObserver = new shaka.media.RegionObserver(
  2983. this.regionTimeline_, startsPastZero);
  2984. regionObserver.addEventListener('enter', (event) => {
  2985. /** @type {shaka.extern.TimelineRegionInfo} */
  2986. const region = event['region'];
  2987. this.onRegionEvent_(
  2988. shaka.util.FakeEvent.EventName.TimelineRegionEnter, region);
  2989. });
  2990. regionObserver.addEventListener('exit', (event) => {
  2991. /** @type {shaka.extern.TimelineRegionInfo} */
  2992. const region = event['region'];
  2993. this.onRegionEvent_(
  2994. shaka.util.FakeEvent.EventName.TimelineRegionExit, region);
  2995. });
  2996. regionObserver.addEventListener('skip', (event) => {
  2997. /** @type {shaka.extern.TimelineRegionInfo} */
  2998. const region = event['region'];
  2999. /** @type {boolean} */
  3000. const seeking = event['seeking'];
  3001. // If we are seeking, we don't want to surface the enter/exit events since
  3002. // they didn't play through them.
  3003. if (!seeking) {
  3004. this.onRegionEvent_(
  3005. shaka.util.FakeEvent.EventName.TimelineRegionEnter, region);
  3006. this.onRegionEvent_(
  3007. shaka.util.FakeEvent.EventName.TimelineRegionExit, region);
  3008. }
  3009. });
  3010. // Now that we have all our observers, create a manager for them.
  3011. const manager = new shaka.media.PlayheadObserverManager(this.video_);
  3012. manager.manage(regionObserver);
  3013. if (this.qualityObserver_) {
  3014. manager.manage(this.qualityObserver_);
  3015. }
  3016. return manager;
  3017. }
  3018. /**
  3019. * Initialize and start the buffering system (observer and timer) so that we
  3020. * can monitor our buffer lead during playback.
  3021. *
  3022. * @param {!HTMLMediaElement} mediaElement
  3023. * @param {number} rebufferingGoal
  3024. * @private
  3025. */
  3026. startBufferManagement_(mediaElement, rebufferingGoal) {
  3027. goog.asserts.assert(
  3028. !this.bufferObserver_,
  3029. 'No buffering observer should exist before initialization.');
  3030. goog.asserts.assert(
  3031. !this.bufferPoller_,
  3032. 'No buffer timer should exist before initialization.');
  3033. // Give dummy values, will be updated below.
  3034. this.bufferObserver_ = new shaka.media.BufferingObserver(1, 2);
  3035. // Force us back to a buffering state. This ensure everything is starting in
  3036. // the same state.
  3037. this.bufferObserver_.setState(shaka.media.BufferingObserver.State.STARVING);
  3038. this.updateBufferingSettings_(rebufferingGoal);
  3039. this.updateBufferState_();
  3040. this.bufferPoller_ = new shaka.util.Timer(() => {
  3041. this.pollBufferState_();
  3042. }).tickEvery(/* seconds= */ 0.25);
  3043. this.loadEventManager_.listen(mediaElement, 'waiting',
  3044. (e) => this.pollBufferState_());
  3045. this.loadEventManager_.listen(mediaElement, 'stalled',
  3046. (e) => this.pollBufferState_());
  3047. this.loadEventManager_.listen(mediaElement, 'canplaythrough',
  3048. (e) => this.pollBufferState_());
  3049. this.loadEventManager_.listen(mediaElement, 'progress',
  3050. (e) => this.pollBufferState_());
  3051. }
  3052. /**
  3053. * Updates the buffering thresholds based on the new rebuffering goal.
  3054. *
  3055. * @param {number} rebufferingGoal
  3056. * @private
  3057. */
  3058. updateBufferingSettings_(rebufferingGoal) {
  3059. // The threshold to transition back to satisfied when starving.
  3060. const starvingThreshold = rebufferingGoal;
  3061. // The threshold to transition into starving when satisfied.
  3062. // We use a "typical" threshold, unless the rebufferingGoal is unusually
  3063. // low.
  3064. // Then we force the value down to half the rebufferingGoal, since
  3065. // starvingThreshold must be strictly larger than satisfiedThreshold for the
  3066. // logic in BufferingObserver to work correctly.
  3067. const satisfiedThreshold = Math.min(
  3068. shaka.Player.TYPICAL_BUFFERING_THRESHOLD_, rebufferingGoal / 2);
  3069. this.bufferObserver_.setThresholds(starvingThreshold, satisfiedThreshold);
  3070. }
  3071. /**
  3072. * This method is called periodically to check what the buffering observer
  3073. * says so that we can update the rest of the buffering behaviours.
  3074. *
  3075. * @private
  3076. */
  3077. pollBufferState_() {
  3078. goog.asserts.assert(
  3079. this.video_,
  3080. 'Need a media element to update the buffering observer');
  3081. goog.asserts.assert(
  3082. this.bufferObserver_,
  3083. 'Need a buffering observer to update');
  3084. let bufferedToEnd;
  3085. switch (this.loadMode_) {
  3086. case shaka.Player.LoadMode.SRC_EQUALS:
  3087. bufferedToEnd = this.isBufferedToEndSrc_();
  3088. break;
  3089. case shaka.Player.LoadMode.MEDIA_SOURCE:
  3090. bufferedToEnd = this.isBufferedToEndMS_();
  3091. break;
  3092. default:
  3093. bufferedToEnd = false;
  3094. break;
  3095. }
  3096. const bufferLead = shaka.media.TimeRangesUtils.bufferedAheadOf(
  3097. this.video_.buffered,
  3098. this.video_.currentTime);
  3099. const stateChanged = this.bufferObserver_.update(bufferLead, bufferedToEnd);
  3100. // If the state changed, we need to surface the event.
  3101. if (stateChanged) {
  3102. this.updateBufferState_();
  3103. }
  3104. }
  3105. /**
  3106. * Create a new media source engine. This will ONLY be replaced by tests as a
  3107. * way to inject fake media source engine instances.
  3108. *
  3109. * @param {!HTMLMediaElement} mediaElement
  3110. * @param {!shaka.extern.TextDisplayer} textDisplayer
  3111. * @param {!shaka.media.MediaSourceEngine.PlayerInterface} playerInterface
  3112. * @param {shaka.lcevc.Dec} lcevcDec
  3113. *
  3114. * @return {!shaka.media.MediaSourceEngine}
  3115. */
  3116. createMediaSourceEngine(mediaElement, textDisplayer, playerInterface,
  3117. lcevcDec) {
  3118. return new shaka.media.MediaSourceEngine(
  3119. mediaElement,
  3120. textDisplayer,
  3121. playerInterface,
  3122. lcevcDec);
  3123. }
  3124. /**
  3125. * Create a new CMCD manager.
  3126. *
  3127. * @private
  3128. */
  3129. createCmcd_() {
  3130. /** @type {shaka.util.CmcdManager.PlayerInterface} */
  3131. const playerInterface = {
  3132. getBandwidthEstimate: () => this.abrManager_ ?
  3133. this.abrManager_.getBandwidthEstimate() : NaN,
  3134. getBufferedInfo: () => this.getBufferedInfo(),
  3135. getCurrentTime: () => this.video_ ? this.video_.currentTime : 0,
  3136. getPlaybackRate: () => this.getPlaybackRate(),
  3137. getNetworkingEngine: () => this.getNetworkingEngine(),
  3138. getVariantTracks: () => this.getVariantTracks(),
  3139. isLive: () => this.isLive(),
  3140. };
  3141. return new shaka.util.CmcdManager(playerInterface, this.config_.cmcd);
  3142. }
  3143. /**
  3144. * Create a new CMSD manager.
  3145. *
  3146. * @private
  3147. */
  3148. createCmsd_() {
  3149. return new shaka.util.CmsdManager(this.config_.cmsd);
  3150. }
  3151. /**
  3152. * Creates a new instance of StreamingEngine. This can be replaced by tests
  3153. * to create fake instances instead.
  3154. *
  3155. * @return {!shaka.media.StreamingEngine}
  3156. */
  3157. createStreamingEngine() {
  3158. goog.asserts.assert(
  3159. this.abrManager_ && this.mediaSourceEngine_ && this.manifest_,
  3160. 'Must not be destroyed');
  3161. /** @type {shaka.media.StreamingEngine.PlayerInterface} */
  3162. const playerInterface = {
  3163. getPresentationTime: () => this.playhead_ ? this.playhead_.getTime() : 0,
  3164. getBandwidthEstimate: () => this.abrManager_.getBandwidthEstimate(),
  3165. getPlaybackRate: () => this.getPlaybackRate(),
  3166. mediaSourceEngine: this.mediaSourceEngine_,
  3167. netEngine: this.networkingEngine_,
  3168. onError: (error) => this.onError_(error),
  3169. onEvent: (event) => this.dispatchEvent(event),
  3170. onManifestUpdate: () => this.onManifestUpdate_(),
  3171. onSegmentAppended: (reference, stream) => {
  3172. this.onSegmentAppended_(
  3173. reference.startTime, reference.endTime, stream.type,
  3174. stream.codecs.includes(','));
  3175. if (this.abrManager_ && stream.fastSwitching &&
  3176. reference.isPartial() && reference.isLastPartial()) {
  3177. this.abrManager_.trySuggestStreams();
  3178. }
  3179. },
  3180. onInitSegmentAppended: (position, initSegment) => {
  3181. const mediaQuality = initSegment.getMediaQuality();
  3182. if (mediaQuality && this.qualityObserver_) {
  3183. this.qualityObserver_.addMediaQualityChange(mediaQuality, position);
  3184. }
  3185. },
  3186. beforeAppendSegment: (contentType, segment) => {
  3187. return this.drmEngine_.parseInbandPssh(contentType, segment);
  3188. },
  3189. onMetadata: (metadata, offset, endTime) => {
  3190. this.processTimedMetadataMediaSrc_(metadata, offset, endTime);
  3191. },
  3192. disableStream: (stream, time) => this.disableStream(stream, time),
  3193. };
  3194. return new shaka.media.StreamingEngine(this.manifest_, playerInterface);
  3195. }
  3196. /**
  3197. * Changes configuration settings on the Player. This checks the names of
  3198. * keys and the types of values to avoid coding errors. If there are errors,
  3199. * this logs them to the console and returns false. Correct fields are still
  3200. * applied even if there are other errors. You can pass an explicit
  3201. * <code>undefined</code> value to restore the default value. This has two
  3202. * modes of operation:
  3203. *
  3204. * <p>
  3205. * First, this can be passed a single "plain" object. This object should
  3206. * follow the {@link shaka.extern.PlayerConfiguration} object. Not all fields
  3207. * need to be set; unset fields retain their old values.
  3208. *
  3209. * <p>
  3210. * Second, this can be passed two arguments. The first is the name of the key
  3211. * to set. This should be a '.' separated path to the key. For example,
  3212. * <code>'streaming.alwaysStreamText'</code>. The second argument is the
  3213. * value to set.
  3214. *
  3215. * @param {string|!Object} config This should either be a field name or an
  3216. * object.
  3217. * @param {*=} value In the second mode, this is the value to set.
  3218. * @return {boolean} True if the passed config object was valid, false if
  3219. * there were invalid entries.
  3220. * @export
  3221. */
  3222. configure(config, value) {
  3223. const Platform = shaka.util.Platform;
  3224. goog.asserts.assert(this.config_, 'Config must not be null!');
  3225. goog.asserts.assert(typeof(config) == 'object' || arguments.length == 2,
  3226. 'String configs should have values!');
  3227. // ('fieldName', value) format
  3228. if (arguments.length == 2 && typeof(config) == 'string') {
  3229. config = shaka.util.ConfigUtils.convertToConfigObject(config, value);
  3230. }
  3231. goog.asserts.assert(typeof(config) == 'object', 'Should be an object!');
  3232. // Deprecate 'streaming.forceTransmuxTS' configuration.
  3233. if (config['streaming'] && 'forceTransmuxTS' in config['streaming']) {
  3234. shaka.Deprecate.deprecateFeature(5,
  3235. 'streaming.forceTransmuxTS configuration',
  3236. 'Please Use mediaSource.forceTransmux instead.');
  3237. config['mediaSource']['mediaSource'] =
  3238. config['streaming']['forceTransmuxTS'];
  3239. delete config['streaming']['forceTransmuxTS'];
  3240. }
  3241. // Deprecate 'streaming.forceTransmux' configuration.
  3242. if (config['streaming'] && 'forceTransmux' in config['streaming']) {
  3243. shaka.Deprecate.deprecateFeature(5,
  3244. 'streaming.forceTransmux configuration',
  3245. 'Please Use mediaSource.forceTransmux instead.');
  3246. config['mediaSource']['mediaSource'] =
  3247. config['streaming']['forceTransmux'];
  3248. delete config['streaming']['forceTransmux'];
  3249. }
  3250. // Deprecate 'streaming.useNativeHlsOnSafari' configuration.
  3251. if (config['streaming'] && 'useNativeHlsOnSafari' in config['streaming']) {
  3252. shaka.Deprecate.deprecateFeature(5,
  3253. 'streaming.useNativeHlsOnSafari configuration',
  3254. 'Please Use streaming.useNativeHlsForFairPlay or ' +
  3255. 'streaming.preferNativeHls instead.');
  3256. config['streaming']['preferNativeHls'] =
  3257. config['streaming']['useNativeHlsOnSafari'] && Platform.isApple();
  3258. delete config['streaming']['useNativeHlsOnSafari'];
  3259. }
  3260. // Deprecate 'streaming.liveSync' boolean configuration.
  3261. if (config['streaming'] &&
  3262. typeof config['streaming']['liveSync'] == 'boolean') {
  3263. shaka.Deprecate.deprecateFeature(5,
  3264. 'streaming.liveSync',
  3265. 'Please Use streaming.liveSync.enabled instead.');
  3266. const liveSyncValue = config['streaming']['liveSync'];
  3267. config['streaming']['liveSync'] = {};
  3268. config['streaming']['liveSync']['enabled'] = liveSyncValue;
  3269. }
  3270. // map liveSyncMinLatency and liveSyncMaxLatency to liveSync.targetLatency
  3271. // if liveSync.targetLatency isn't set.
  3272. if (config['streaming'] && (!config['streaming']['liveSync'] ||
  3273. !('targetLatency' in config['streaming']['liveSync'])) &&
  3274. ('liveSyncMinLatency' in config['streaming'] ||
  3275. 'liveSyncMaxLatency' in config['streaming'])) {
  3276. const min = config['streaming']['liveSyncMinLatency'] || 0;
  3277. const max = config['streaming']['liveSyncMaxLatency'] || 1;
  3278. const mid = Math.abs(max - min) / 2;
  3279. config['streaming']['liveSync'] = config['streaming']['liveSync'] || {};
  3280. config['streaming']['liveSync']['targetLatency'] = min + mid;
  3281. config['streaming']['liveSync']['targetLatencyTolerance'] = mid;
  3282. }
  3283. // Deprecate 'streaming.liveSyncMaxLatency' configuration.
  3284. if (config['streaming'] && 'liveSyncMaxLatency' in config['streaming']) {
  3285. shaka.Deprecate.deprecateFeature(5,
  3286. 'streaming.liveSyncMaxLatency',
  3287. 'Please Use streaming.liveSync.targetLatency and ' +
  3288. 'streaming.liveSync.targetLatencyTolerance instead. ' +
  3289. 'Or, set the values in your DASH manifest');
  3290. delete config['streaming']['liveSyncMaxLatency'];
  3291. }
  3292. // Deprecate 'streaming.liveSyncMinLatency' configuration.
  3293. if (config['streaming'] && 'liveSyncMinLatency' in config['streaming']) {
  3294. shaka.Deprecate.deprecateFeature(5,
  3295. 'streaming.liveSyncMinLatency',
  3296. 'Please Use streaming.liveSync.targetLatency and ' +
  3297. 'streaming.liveSync.targetLatencyTolerance instead. ' +
  3298. 'Or, set the values in your DASH manifest');
  3299. delete config['streaming']['liveSyncMinLatency'];
  3300. }
  3301. // Deprecate 'streaming.liveSyncTargetLatency' configuration.
  3302. if (config['streaming'] && 'liveSyncTargetLatency' in config['streaming']) {
  3303. shaka.Deprecate.deprecateFeature(5,
  3304. 'streaming.liveSyncTargetLatency',
  3305. 'Please Use streaming.liveSync.targetLatency instead.');
  3306. config['streaming']['liveSync'] = config['streaming']['liveSync'] || {};
  3307. config['streaming']['liveSync']['targetLatency'] =
  3308. config['streaming']['liveSyncTargetLatency'];
  3309. delete config['streaming']['liveSyncTargetLatency'];
  3310. }
  3311. // Deprecate 'streaming.liveSyncTargetLatencyTolerance' configuration.
  3312. if (config['streaming'] &&
  3313. 'liveSyncTargetLatencyTolerance' in config['streaming']) {
  3314. shaka.Deprecate.deprecateFeature(5,
  3315. 'streaming.liveSyncTargetLatencyTolerance',
  3316. 'Please Use streaming.liveSync.targetLatencyTolerance instead.');
  3317. config['streaming']['liveSync'] = config['streaming']['liveSync'] || {};
  3318. config['streaming']['liveSync']['targetLatencyTolerance'] =
  3319. config['streaming']['liveSyncTargetLatencyTolerance'];
  3320. delete config['streaming']['liveSyncTargetLatencyTolerance'];
  3321. }
  3322. // Deprecate 'streaming.liveSyncPlaybackRate' configuration.
  3323. if (config['streaming'] && 'liveSyncPlaybackRate' in config['streaming']) {
  3324. shaka.Deprecate.deprecateFeature(5,
  3325. 'streaming.liveSyncPlaybackRate',
  3326. 'Please Use streaming.liveSync.maxPlaybackRate instead.');
  3327. config['streaming']['liveSync'] = config['streaming']['liveSync'] || {};
  3328. config['streaming']['liveSync']['maxPlaybackRate'] =
  3329. config['streaming']['liveSyncPlaybackRate'];
  3330. delete config['streaming']['liveSyncPlaybackRate'];
  3331. }
  3332. // Deprecate 'streaming.liveSyncMinPlaybackRate' configuration.
  3333. if (config['streaming'] &&
  3334. 'liveSyncMinPlaybackRate' in config['streaming']) {
  3335. shaka.Deprecate.deprecateFeature(5,
  3336. 'streaming.liveSyncMinPlaybackRate',
  3337. 'Please Use streaming.liveSync.minPlaybackRate instead.');
  3338. config['streaming']['liveSync'] = config['streaming']['liveSync'] || {};
  3339. config['streaming']['liveSync']['minPlaybackRate'] =
  3340. config['streaming']['liveSyncMinPlaybackRate'];
  3341. delete config['streaming']['liveSyncMinPlaybackRate'];
  3342. }
  3343. // Deprecate 'streaming.liveSyncPanicMode' configuration.
  3344. if (config['streaming'] && 'liveSyncPanicMode' in config['streaming']) {
  3345. shaka.Deprecate.deprecateFeature(5,
  3346. 'streaming.liveSyncPanicMode',
  3347. 'Please Use streaming.liveSync.panicMode instead.');
  3348. config['streaming']['liveSync'] = config['streaming']['liveSync'] || {};
  3349. config['streaming']['liveSync']['panicMode'] =
  3350. config['streaming']['liveSyncPanicMode'];
  3351. delete config['streaming']['liveSyncPanicMode'];
  3352. }
  3353. // Deprecate 'streaming.liveSyncPanicThreshold' configuration.
  3354. if (config['streaming'] &&
  3355. 'liveSyncPanicThreshold' in config['streaming']) {
  3356. shaka.Deprecate.deprecateFeature(5,
  3357. 'streaming.liveSyncPanicThreshold',
  3358. 'Please Use streaming.liveSync.panicThreshold instead.');
  3359. config['streaming']['liveSync'] = config['streaming']['liveSync'] || {};
  3360. config['streaming']['liveSync']['panicThreshold'] =
  3361. config['streaming']['liveSyncPanicThreshold'];
  3362. delete config['streaming']['liveSyncPanicThreshold'];
  3363. }
  3364. // Deprecate 'mediaSource.sourceBufferExtraFeatures' configuration.
  3365. if (config['mediaSource'] &&
  3366. 'sourceBufferExtraFeatures' in config['mediaSource']) {
  3367. shaka.Deprecate.deprecateFeature(5,
  3368. 'mediaSource.sourceBufferExtraFeatures configuration',
  3369. 'Please Use mediaSource.addExtraFeaturesToSourceBuffer() instead.');
  3370. const sourceBufferExtraFeatures =
  3371. config['mediaSource']['sourceBufferExtraFeatures'];
  3372. config['mediaSource']['addExtraFeaturesToSourceBuffer'] = () => {
  3373. return sourceBufferExtraFeatures;
  3374. };
  3375. delete config['mediaSource']['sourceBufferExtraFeatures'];
  3376. }
  3377. // If lowLatencyMode is enabled, and inaccurateManifestTolerance and
  3378. // rebufferingGoal and segmentPrefetchLimit and baseDelay and
  3379. // autoCorrectDrift and maxDisabledTime are not specified, set
  3380. // inaccurateManifestTolerance to 0 and rebufferingGoal to 0.01 and
  3381. // segmentPrefetchLimit to 2 and updateIntervalSeconds to 0.1 and and
  3382. // baseDelay to 100 and autoCorrectDrift to false and maxDisabledTime
  3383. // to 1 by default for low latency streaming.
  3384. if (config['streaming'] && config['streaming']['lowLatencyMode']) {
  3385. if (config['streaming']['inaccurateManifestTolerance'] == undefined) {
  3386. config['streaming']['inaccurateManifestTolerance'] = 0;
  3387. }
  3388. if (config['streaming']['rebufferingGoal'] == undefined) {
  3389. config['streaming']['rebufferingGoal'] = 0.01;
  3390. }
  3391. if (config['streaming']['segmentPrefetchLimit'] == undefined) {
  3392. config['streaming']['segmentPrefetchLimit'] = 2;
  3393. }
  3394. if (config['streaming']['updateIntervalSeconds'] == undefined) {
  3395. config['streaming']['updateIntervalSeconds'] = 0.1;
  3396. }
  3397. if (config['streaming']['maxDisabledTime'] == undefined) {
  3398. config['streaming']['maxDisabledTime'] = 1;
  3399. }
  3400. if (config['streaming']['retryParameters'] == undefined) {
  3401. config['streaming']['retryParameters'] = {};
  3402. }
  3403. if (config['streaming']['retryParameters']['baseDelay'] == undefined) {
  3404. config['streaming']['retryParameters']['baseDelay'] = 100;
  3405. }
  3406. if (config['manifest'] == undefined) {
  3407. config['manifest'] = {};
  3408. }
  3409. if (config['manifest']['dash'] == undefined) {
  3410. config['manifest']['dash'] = {};
  3411. }
  3412. if (config['manifest']['dash']['autoCorrectDrift'] == undefined) {
  3413. config['manifest']['dash']['autoCorrectDrift'] = false;
  3414. }
  3415. if (config['manifest']['retryParameters'] == undefined) {
  3416. config['manifest']['retryParameters'] = {};
  3417. }
  3418. if (config['manifest']['retryParameters']['baseDelay'] == undefined) {
  3419. config['manifest']['retryParameters']['baseDelay'] = 100;
  3420. }
  3421. if (config['drm'] == undefined) {
  3422. config['drm'] = {};
  3423. }
  3424. if (config['drm']['retryParameters'] == undefined) {
  3425. config['drm']['retryParameters'] = {};
  3426. }
  3427. if (config['drm']['retryParameters']['baseDelay'] == undefined) {
  3428. config['drm']['retryParameters']['baseDelay'] = 100;
  3429. }
  3430. }
  3431. const ret = shaka.util.PlayerConfiguration.mergeConfigObjects(
  3432. this.config_, config, this.defaultConfig_());
  3433. this.applyConfig_();
  3434. return ret;
  3435. }
  3436. /**
  3437. * Apply config changes.
  3438. * @private
  3439. */
  3440. applyConfig_() {
  3441. this.manifestFilterer_ = new shaka.media.ManifestFilterer(
  3442. this.config_, this.maxHwRes_, this.drmEngine_);
  3443. if (this.parser_) {
  3444. const manifestConfig =
  3445. shaka.util.ObjectUtils.cloneObject(this.config_.manifest);
  3446. // Don't read video segments if the player is attached to an audio element
  3447. if (this.video_ && this.video_.nodeName === 'AUDIO') {
  3448. manifestConfig.disableVideo = true;
  3449. }
  3450. this.parser_.configure(manifestConfig);
  3451. }
  3452. if (this.drmEngine_) {
  3453. this.drmEngine_.configure(this.config_.drm);
  3454. }
  3455. if (this.streamingEngine_) {
  3456. this.streamingEngine_.configure(this.config_.streaming);
  3457. // Need to apply the restrictions.
  3458. // this.filterManifestWithRestrictions_() may throw.
  3459. try {
  3460. if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) {
  3461. if (this.manifestFilterer_.filterManifestWithRestrictions(
  3462. this.manifest_)) {
  3463. this.onTracksChanged_();
  3464. }
  3465. }
  3466. } catch (error) {
  3467. this.onError_(error);
  3468. }
  3469. if (this.abrManager_) {
  3470. // Update AbrManager variants to match these new settings.
  3471. this.updateAbrManagerVariants_();
  3472. }
  3473. // If the streams we are playing are restricted, we need to switch.
  3474. const activeVariant = this.streamingEngine_.getCurrentVariant();
  3475. if (activeVariant) {
  3476. if (!activeVariant.allowedByApplication ||
  3477. !activeVariant.allowedByKeySystem) {
  3478. shaka.log.debug('Choosing new variant after changing configuration');
  3479. this.chooseVariantAndSwitch_();
  3480. }
  3481. }
  3482. }
  3483. if (this.networkingEngine_) {
  3484. this.networkingEngine_.setForceHTTP(this.config_.streaming.forceHTTP);
  3485. this.networkingEngine_.setForceHTTPS(this.config_.streaming.forceHTTPS);
  3486. }
  3487. if (this.mediaSourceEngine_) {
  3488. this.mediaSourceEngine_.configure(this.config_.mediaSource);
  3489. const {segmentRelativeVttTiming} = this.config_.manifest;
  3490. this.mediaSourceEngine_.setSegmentRelativeVttTiming(
  3491. segmentRelativeVttTiming);
  3492. const textDisplayerFactory = this.config_.textDisplayFactory;
  3493. if (this.lastTextFactory_ != textDisplayerFactory) {
  3494. const displayer = textDisplayerFactory();
  3495. if (displayer.configure) {
  3496. displayer.configure(this.config_.textDisplayer);
  3497. } else {
  3498. shaka.Deprecate.deprecateFeature(5,
  3499. 'Text displayer w/ configure',
  3500. 'Text displayer should have a "configure" method!');
  3501. }
  3502. this.mediaSourceEngine_.setTextDisplayer(displayer);
  3503. this.lastTextFactory_ = textDisplayerFactory;
  3504. if (this.streamingEngine_) {
  3505. // Reload the text stream, so the cues will load again.
  3506. this.streamingEngine_.reloadTextStream();
  3507. }
  3508. } else {
  3509. const displayer = this.mediaSourceEngine_.getTextDisplayer();
  3510. if (displayer.configure) {
  3511. displayer.configure(this.config_.textDisplayer);
  3512. }
  3513. }
  3514. }
  3515. if (this.abrManager_) {
  3516. this.abrManager_.configure(this.config_.abr);
  3517. // Simply enable/disable ABR with each call, since multiple calls to these
  3518. // methods have no effect.
  3519. if (this.config_.abr.enabled) {
  3520. this.abrManager_.enable();
  3521. } else {
  3522. this.abrManager_.disable();
  3523. }
  3524. this.onAbrStatusChanged_();
  3525. }
  3526. if (this.bufferObserver_) {
  3527. let rebufferThreshold = this.config_.streaming.rebufferingGoal;
  3528. if (this.manifest_) {
  3529. rebufferThreshold =
  3530. Math.max(rebufferThreshold, this.manifest_.minBufferTime);
  3531. }
  3532. this.updateBufferingSettings_(rebufferThreshold);
  3533. }
  3534. if (this.manifest_) {
  3535. shaka.Player.applyPlayRange_(this.manifest_.presentationTimeline,
  3536. this.config_.playRangeStart,
  3537. this.config_.playRangeEnd);
  3538. }
  3539. if (this.adManager_) {
  3540. this.adManager_.configure(this.config_.ads);
  3541. }
  3542. if (this.cmcdManager_) {
  3543. this.cmcdManager_.configure(this.config_.cmcd);
  3544. }
  3545. if (this.cmsdManager_) {
  3546. this.cmsdManager_.configure(this.config_.cmsd);
  3547. }
  3548. }
  3549. /**
  3550. * Return a copy of the current configuration. Modifications of the returned
  3551. * value will not affect the Player's active configuration. You must call
  3552. * <code>player.configure()</code> to make changes.
  3553. *
  3554. * @return {shaka.extern.PlayerConfiguration}
  3555. * @export
  3556. */
  3557. getConfiguration() {
  3558. goog.asserts.assert(this.config_, 'Config must not be null!');
  3559. const ret = this.defaultConfig_();
  3560. shaka.util.PlayerConfiguration.mergeConfigObjects(
  3561. ret, this.config_, this.defaultConfig_());
  3562. return ret;
  3563. }
  3564. /**
  3565. * Return a copy of the current non default configuration. Modifications of
  3566. * the returned value will not affect the Player's active configuration.
  3567. * You must call <code>player.configure()</code> to make changes.
  3568. *
  3569. * @return {!Object}
  3570. * @export
  3571. */
  3572. getNonDefaultConfiguration() {
  3573. goog.asserts.assert(this.config_, 'Config must not be null!');
  3574. const ret = this.defaultConfig_();
  3575. shaka.util.PlayerConfiguration.mergeConfigObjects(
  3576. ret, this.config_, this.defaultConfig_());
  3577. return shaka.util.ConfigUtils.getDifferenceFromConfigObjects(
  3578. this.config_, this.defaultConfig_());
  3579. }
  3580. /**
  3581. * Return a reference to the current configuration. Modifications to the
  3582. * returned value will affect the Player's active configuration. This method
  3583. * is not exported as sharing configuration with external objects is not
  3584. * supported.
  3585. *
  3586. * @return {shaka.extern.PlayerConfiguration}
  3587. */
  3588. getSharedConfiguration() {
  3589. goog.asserts.assert(
  3590. this.config_, 'Cannot call getSharedConfiguration after call destroy!');
  3591. return this.config_;
  3592. }
  3593. /**
  3594. * Returns the ratio of video length buffered compared to buffering Goal
  3595. * @return {number}
  3596. * @export
  3597. */
  3598. getBufferFullness() {
  3599. if (this.video_) {
  3600. const bufferedLength = this.video_.buffered.length;
  3601. const bufferedEnd =
  3602. bufferedLength ? this.video_.buffered.end(bufferedLength - 1) : 0;
  3603. const bufferingGoal = this.getConfiguration().streaming.bufferingGoal;
  3604. const lengthToBeBuffered = Math.min(this.video_.currentTime +
  3605. bufferingGoal, this.seekRange().end);
  3606. if (bufferedEnd >= lengthToBeBuffered) {
  3607. return 1;
  3608. } else if (bufferedEnd <= this.video_.currentTime) {
  3609. return 0;
  3610. } else if (bufferedEnd < lengthToBeBuffered) {
  3611. return ((bufferedEnd - this.video_.currentTime) /
  3612. (lengthToBeBuffered - this.video_.currentTime));
  3613. }
  3614. }
  3615. return 0;
  3616. }
  3617. /**
  3618. * Reset configuration to default.
  3619. * @export
  3620. */
  3621. resetConfiguration() {
  3622. goog.asserts.assert(this.config_, 'Cannot be destroyed');
  3623. // Remove the old keys so we remove open-ended dictionaries like drm.servers
  3624. // but keeps the same object reference.
  3625. for (const key in this.config_) {
  3626. delete this.config_[key];
  3627. }
  3628. shaka.util.PlayerConfiguration.mergeConfigObjects(
  3629. this.config_, this.defaultConfig_(), this.defaultConfig_());
  3630. this.applyConfig_();
  3631. }
  3632. /**
  3633. * Get the current load mode.
  3634. *
  3635. * @return {shaka.Player.LoadMode}
  3636. * @export
  3637. */
  3638. getLoadMode() {
  3639. return this.loadMode_;
  3640. }
  3641. /**
  3642. * Get the current manifest type.
  3643. *
  3644. * @return {?string}
  3645. * @export
  3646. */
  3647. getManifestType() {
  3648. if (!this.manifest_) {
  3649. return null;
  3650. }
  3651. return this.manifest_.type;
  3652. }
  3653. /**
  3654. * Get the media element that the player is currently using to play loaded
  3655. * content. If the player has not loaded content, this will return
  3656. * <code>null</code>.
  3657. *
  3658. * @return {HTMLMediaElement}
  3659. * @export
  3660. */
  3661. getMediaElement() {
  3662. return this.video_;
  3663. }
  3664. /**
  3665. * @return {shaka.net.NetworkingEngine} A reference to the Player's networking
  3666. * engine. Applications may use this to make requests through Shaka's
  3667. * networking plugins.
  3668. * @export
  3669. */
  3670. getNetworkingEngine() {
  3671. return this.networkingEngine_;
  3672. }
  3673. /**
  3674. * Get the uri to the asset that the player has loaded. If the player has not
  3675. * loaded content, this will return <code>null</code>.
  3676. *
  3677. * @return {?string}
  3678. * @export
  3679. */
  3680. getAssetUri() {
  3681. return this.assetUri_;
  3682. }
  3683. /**
  3684. * Returns a shaka.ads.AdManager instance, responsible for Dynamic
  3685. * Ad Insertion functionality.
  3686. *
  3687. * @return {shaka.extern.IAdManager}
  3688. * @export
  3689. */
  3690. getAdManager() {
  3691. // NOTE: this clause is redundant, but it keeps the compiler from
  3692. // inlining this function. Inlining leads to setting the adManager
  3693. // not taking effect in the compiled build.
  3694. // Closure has a @noinline flag, but apparently not all cases are
  3695. // supported by it, and ours isn't.
  3696. // If they expand support, we might be able to get rid of this
  3697. // clause.
  3698. if (!this.adManager_) {
  3699. return null;
  3700. }
  3701. return this.adManager_;
  3702. }
  3703. /**
  3704. * Get if the player is playing live content. If the player has not loaded
  3705. * content, this will return <code>false</code>.
  3706. *
  3707. * @return {boolean}
  3708. * @export
  3709. */
  3710. isLive() {
  3711. if (this.manifest_) {
  3712. return this.manifest_.presentationTimeline.isLive();
  3713. }
  3714. // For native HLS, the duration for live streams seems to be Infinity.
  3715. if (this.video_ && this.video_.src) {
  3716. return this.video_.duration == Infinity;
  3717. }
  3718. return false;
  3719. }
  3720. /**
  3721. * Get if the player is playing in-progress content. If the player has not
  3722. * loaded content, this will return <code>false</code>.
  3723. *
  3724. * @return {boolean}
  3725. * @export
  3726. */
  3727. isInProgress() {
  3728. return this.manifest_ ?
  3729. this.manifest_.presentationTimeline.isInProgress() :
  3730. false;
  3731. }
  3732. /**
  3733. * Check if the manifest contains only audio-only content. If the player has
  3734. * not loaded content, this will return <code>false</code>.
  3735. *
  3736. * <p>
  3737. * The player does not support content that contain more than one type of
  3738. * variants (i.e. mixing audio-only, video-only, audio-video). Content will be
  3739. * filtered to only contain one type of variant.
  3740. *
  3741. * @return {boolean}
  3742. * @export
  3743. */
  3744. isAudioOnly() {
  3745. if (this.manifest_) {
  3746. const variants = this.manifest_.variants;
  3747. if (!variants.length) {
  3748. return false;
  3749. }
  3750. // Note that if there are some audio-only variants and some audio-video
  3751. // variants, the audio-only variants are removed during filtering.
  3752. // Therefore if the first variant has no video, that's sufficient to say
  3753. // it is audio-only content.
  3754. return !variants[0].video;
  3755. } else if (this.video_ && this.video_.src) {
  3756. // If we have video track info, use that. It will be the least
  3757. // error-prone way with native HLS. In contrast, videoHeight might be
  3758. // unset until the first frame is loaded. Since isAudioOnly is queried
  3759. // by the UI on the 'trackschanged' event, the videoTracks info should be
  3760. // up-to-date.
  3761. if (this.video_.videoTracks) {
  3762. return this.video_.videoTracks.length == 0;
  3763. }
  3764. // We cast to the more specific HTMLVideoElement to access videoHeight.
  3765. // This might be an audio element, though, in which case videoHeight will
  3766. // be undefined at runtime. For audio elements, this will always return
  3767. // true.
  3768. const video = /** @type {HTMLVideoElement} */(this.video_);
  3769. return video.videoHeight == 0;
  3770. } else {
  3771. return false;
  3772. }
  3773. }
  3774. /**
  3775. * Get the range of time (in seconds) that seeking is allowed. If the player
  3776. * has not loaded content and the manifest is HLS, this will return a range
  3777. * from 0 to 0.
  3778. *
  3779. * @return {{start: number, end: number}}
  3780. * @export
  3781. */
  3782. seekRange() {
  3783. if (this.manifest_) {
  3784. // With HLS lazy-loading, there were some situations where the manifest
  3785. // had partially loaded, enough to move onto further load stages, but no
  3786. // segments had been loaded, so the timeline is still unknown.
  3787. // See: https://github.com/shaka-project/shaka-player/pull/4590
  3788. if (!this.fullyLoaded_ &&
  3789. this.manifest_.type == shaka.media.ManifestParser.HLS) {
  3790. return {'start': 0, 'end': 0};
  3791. }
  3792. const timeline = this.manifest_.presentationTimeline;
  3793. return {
  3794. 'start': timeline.getSeekRangeStart(),
  3795. 'end': timeline.getSeekRangeEnd(),
  3796. };
  3797. }
  3798. // If we have loaded content with src=, we ask the video element for its
  3799. // seekable range. This covers both plain mp4s and native HLS playbacks.
  3800. if (this.video_ && this.video_.src) {
  3801. const seekable = this.video_.seekable;
  3802. if (seekable.length) {
  3803. return {
  3804. 'start': seekable.start(0),
  3805. 'end': seekable.end(seekable.length - 1),
  3806. };
  3807. }
  3808. }
  3809. return {'start': 0, 'end': 0};
  3810. }
  3811. /**
  3812. * Go to live in a live stream.
  3813. *
  3814. * @export
  3815. */
  3816. goToLive() {
  3817. if (this.isLive()) {
  3818. this.video_.currentTime = this.seekRange().end;
  3819. } else {
  3820. shaka.log.warning('goToLive is for live streams!');
  3821. }
  3822. }
  3823. /**
  3824. * Indicates if the player has fully loaded the stream.
  3825. *
  3826. * @return {boolean}
  3827. * @export
  3828. */
  3829. isFullyLoaded() {
  3830. return this.fullyLoaded_;
  3831. }
  3832. /**
  3833. * Get the key system currently used by EME. If EME is not being used, this
  3834. * will return an empty string. If the player has not loaded content, this
  3835. * will return an empty string.
  3836. *
  3837. * @return {string}
  3838. * @export
  3839. */
  3840. keySystem() {
  3841. return shaka.media.DrmEngine.keySystem(this.drmInfo());
  3842. }
  3843. /**
  3844. * Get the drm info used to initialize EME. If EME is not being used, this
  3845. * will return <code>null</code>. If the player is idle or has not initialized
  3846. * EME yet, this will return <code>null</code>.
  3847. *
  3848. * @return {?shaka.extern.DrmInfo}
  3849. * @export
  3850. */
  3851. drmInfo() {
  3852. return this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
  3853. }
  3854. /**
  3855. * Get the drm engine.
  3856. * This method should only be used for testing. Applications SHOULD NOT
  3857. * use this in production.
  3858. *
  3859. * @return {?shaka.media.DrmEngine}
  3860. */
  3861. getDrmEngine() {
  3862. return this.drmEngine_;
  3863. }
  3864. /**
  3865. * Get the next known expiration time for any EME session. If the session
  3866. * never expires, this will return <code>Infinity</code>. If there are no EME
  3867. * sessions, this will return <code>Infinity</code>. If the player has not
  3868. * loaded content, this will return <code>Infinity</code>.
  3869. *
  3870. * @return {number}
  3871. * @export
  3872. */
  3873. getExpiration() {
  3874. return this.drmEngine_ ? this.drmEngine_.getExpiration() : Infinity;
  3875. }
  3876. /**
  3877. * Returns the active sessions metadata
  3878. *
  3879. * @return {!Array.<shaka.extern.DrmSessionMetadata>}
  3880. * @export
  3881. */
  3882. getActiveSessionsMetadata() {
  3883. return this.drmEngine_ ? this.drmEngine_.getActiveSessionsMetadata() : [];
  3884. }
  3885. /**
  3886. * Gets a map of EME key ID to the current key status.
  3887. *
  3888. * @return {!Object<string, string>}
  3889. * @export
  3890. */
  3891. getKeyStatuses() {
  3892. return this.drmEngine_ ? this.drmEngine_.getKeyStatuses() : {};
  3893. }
  3894. /**
  3895. * Check if the player is currently in a buffering state (has too little
  3896. * content to play smoothly). If the player has not loaded content, this will
  3897. * return <code>false</code>.
  3898. *
  3899. * @return {boolean}
  3900. * @export
  3901. */
  3902. isBuffering() {
  3903. const State = shaka.media.BufferingObserver.State;
  3904. return this.bufferObserver_ ?
  3905. this.bufferObserver_.getState() == State.STARVING :
  3906. false;
  3907. }
  3908. /**
  3909. * Get the playback rate of what is playing right now. If we are using trick
  3910. * play, this will return the trick play rate.
  3911. * If no content is playing, this will return 0.
  3912. * If content is buffering, this will return the expected playback rate once
  3913. * the video starts playing.
  3914. *
  3915. * <p>
  3916. * If the player has not loaded content, this will return a playback rate of
  3917. * 0.
  3918. *
  3919. * @return {number}
  3920. * @export
  3921. */
  3922. getPlaybackRate() {
  3923. if (!this.video_) {
  3924. return 0;
  3925. }
  3926. return this.playRateController_ ?
  3927. this.playRateController_.getRealRate() :
  3928. 1;
  3929. }
  3930. /**
  3931. * Enable trick play to skip through content without playing by repeatedly
  3932. * seeking. For example, a rate of 2.5 would result in 2.5 seconds of content
  3933. * being skipped every second. A negative rate will result in moving
  3934. * backwards.
  3935. *
  3936. * <p>
  3937. * If the player has not loaded content or is still loading content this will
  3938. * be a no-op. Wait until <code>load</code> has completed before calling.
  3939. *
  3940. * <p>
  3941. * Trick play will be canceled automatically if the playhead hits the
  3942. * beginning or end of the seekable range for the content.
  3943. *
  3944. * @param {number} rate
  3945. * @export
  3946. */
  3947. trickPlay(rate) {
  3948. // A playbackRate of 0 is used internally when we are in a buffering state,
  3949. // and doesn't make sense for trick play. If you set a rate of 0 for trick
  3950. // play, we will reject it and issue a warning. If it happens during a
  3951. // test, we will fail the test through this assertion.
  3952. goog.asserts.assert(rate != 0, 'Should never set a trick play rate of 0!');
  3953. if (rate == 0) {
  3954. shaka.log.alwaysWarn('A trick play rate of 0 is unsupported!');
  3955. return;
  3956. }
  3957. this.trickPlayEventManager_.removeAll();
  3958. if (this.video_.paused) {
  3959. // Our fast forward is implemented with playbackRate and needs the video
  3960. // to be playing (to not be paused) to take immediate effect.
  3961. // If the video is paused, "unpause" it.
  3962. this.video_.play();
  3963. }
  3964. this.playRateController_.set(rate);
  3965. if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
  3966. this.abrManager_.playbackRateChanged(rate);
  3967. this.streamingEngine_.setTrickPlay(Math.abs(rate) > 1);
  3968. }
  3969. if (this.isLive()) {
  3970. this.trickPlayEventManager_.listen(this.video_, 'timeupdate', () => {
  3971. const currentTime = this.video_.currentTime;
  3972. const seekRange = this.seekRange();
  3973. const safeSeekOffset = this.config_.streaming.safeSeekOffset;
  3974. // Cancel trick play if we hit the beginning or end of the seekable
  3975. // (Sub-second accuracy not required here)
  3976. if (rate > 0) {
  3977. if (Math.floor(currentTime) >= Math.floor(seekRange.end)) {
  3978. this.cancelTrickPlay();
  3979. }
  3980. } else {
  3981. if (Math.floor(currentTime) <=
  3982. Math.floor(seekRange.start + safeSeekOffset)) {
  3983. this.cancelTrickPlay();
  3984. }
  3985. }
  3986. });
  3987. }
  3988. }
  3989. /**
  3990. * Cancel trick-play. If the player has not loaded content or is still loading
  3991. * content this will be a no-op.
  3992. *
  3993. * @export
  3994. */
  3995. cancelTrickPlay() {
  3996. const defaultPlaybackRate = this.playRateController_.getDefaultRate();
  3997. if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
  3998. this.playRateController_.set(defaultPlaybackRate);
  3999. }
  4000. if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
  4001. this.playRateController_.set(defaultPlaybackRate);
  4002. this.abrManager_.playbackRateChanged(defaultPlaybackRate);
  4003. this.streamingEngine_.setTrickPlay(false);
  4004. }
  4005. this.trickPlayEventManager_.removeAll();
  4006. }
  4007. /**
  4008. * Return a list of variant tracks that can be switched to.
  4009. *
  4010. * <p>
  4011. * If the player has not loaded content, this will return an empty list.
  4012. *
  4013. * @return {!Array.<shaka.extern.Track>}
  4014. * @export
  4015. */
  4016. getVariantTracks() {
  4017. if (this.manifest_) {
  4018. const currentVariant = this.streamingEngine_ ?
  4019. this.streamingEngine_.getCurrentVariant() : null;
  4020. const tracks = [];
  4021. let activeTracks = 0;
  4022. // Convert each variant to a track.
  4023. for (const variant of this.manifest_.variants) {
  4024. if (!shaka.util.StreamUtils.isPlayable(variant)) {
  4025. continue;
  4026. }
  4027. const track = shaka.util.StreamUtils.variantToTrack(variant);
  4028. track.active = variant == currentVariant;
  4029. if (!track.active && activeTracks != 1 && currentVariant != null &&
  4030. variant.video == currentVariant.video &&
  4031. variant.audio == currentVariant.audio) {
  4032. track.active = true;
  4033. }
  4034. if (track.active) {
  4035. activeTracks++;
  4036. }
  4037. tracks.push(track);
  4038. }
  4039. goog.asserts.assert(activeTracks <= 1,
  4040. 'It should only have one active track');
  4041. return tracks;
  4042. } else if (this.video_ && this.video_.audioTracks) {
  4043. // Safari's native HLS always shows a single element in videoTracks.
  4044. // You can't use that API to change resolutions. But we can use
  4045. // audioTracks to generate a variant list that is usable for changing
  4046. // languages.
  4047. const audioTracks = Array.from(this.video_.audioTracks);
  4048. return audioTracks.map((audio) =>
  4049. shaka.util.StreamUtils.html5AudioTrackToTrack(audio));
  4050. } else {
  4051. return [];
  4052. }
  4053. }
  4054. /**
  4055. * Return a list of text tracks that can be switched to.
  4056. *
  4057. * <p>
  4058. * If the player has not loaded content, this will return an empty list.
  4059. *
  4060. * @return {!Array.<shaka.extern.Track>}
  4061. * @export
  4062. */
  4063. getTextTracks() {
  4064. if (this.manifest_) {
  4065. const currentTextStream = this.streamingEngine_ ?
  4066. this.streamingEngine_.getCurrentTextStream() : null;
  4067. const tracks = [];
  4068. // Convert all selectable text streams to tracks.
  4069. for (const text of this.manifest_.textStreams) {
  4070. const track = shaka.util.StreamUtils.textStreamToTrack(text);
  4071. track.active = text == currentTextStream;
  4072. tracks.push(track);
  4073. }
  4074. return tracks;
  4075. } else if (this.video_ && this.video_.src && this.video_.textTracks) {
  4076. const textTracks = this.getFilteredTextTracks_();
  4077. const StreamUtils = shaka.util.StreamUtils;
  4078. return textTracks.map((text) => StreamUtils.html5TextTrackToTrack(text));
  4079. } else {
  4080. return [];
  4081. }
  4082. }
  4083. /**
  4084. * Return a list of image tracks that can be switched to.
  4085. *
  4086. * If the player has not loaded content, this will return an empty list.
  4087. *
  4088. * @return {!Array.<shaka.extern.Track>}
  4089. * @export
  4090. */
  4091. getImageTracks() {
  4092. const StreamUtils = shaka.util.StreamUtils;
  4093. let imageStreams = this.externalSrcEqualsThumbnailsStreams_;
  4094. if (this.manifest_) {
  4095. imageStreams = this.manifest_.imageStreams;
  4096. }
  4097. return imageStreams.map((image) => StreamUtils.imageStreamToTrack(image));
  4098. }
  4099. /**
  4100. * Returns Thumbnail objects for each thumbnail for a given image track ID.
  4101. *
  4102. * If the player has not loaded content, this will return a null.
  4103. *
  4104. * @param {number} trackId
  4105. * @return {!Promise.<?Array<!shaka.extern.Thumbnail>>}
  4106. * @export
  4107. */
  4108. async getAllThumbnails(trackId) {
  4109. if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE &&
  4110. this.loadMode_ != shaka.Player.LoadMode.SRC_EQUALS) {
  4111. return null;
  4112. }
  4113. let imageStreams = this.externalSrcEqualsThumbnailsStreams_;
  4114. if (this.manifest_) {
  4115. imageStreams = this.manifest_.imageStreams;
  4116. }
  4117. const imageStream = imageStreams.find(
  4118. (stream) => stream.id == trackId);
  4119. if (!imageStream) {
  4120. return null;
  4121. }
  4122. if (!imageStream.segmentIndex) {
  4123. await imageStream.createSegmentIndex();
  4124. }
  4125. const promises = [];
  4126. imageStream.segmentIndex.forEachTopLevelReference((reference) => {
  4127. const dimensions = this.parseTilesLayout_(
  4128. reference.getTilesLayout() || imageStream.tilesLayout);
  4129. if (dimensions) {
  4130. const numThumbnails = dimensions.rows * dimensions.columns;
  4131. const duration = reference.trueEndTime - reference.startTime;
  4132. for (let i = 0; i < numThumbnails; i++) {
  4133. const sampleTime = reference.startTime + duration * i / numThumbnails;
  4134. promises.push(this.getThumbnails(trackId, sampleTime));
  4135. }
  4136. }
  4137. });
  4138. const thumbnails = await Promise.all(promises);
  4139. return thumbnails.filter((t) => t);
  4140. }
  4141. /**
  4142. * Parses a tiles layout.
  4143. *
  4144. * @param {string|undefined} tilesLayout
  4145. * @return {?{
  4146. * columns: number,
  4147. * rows: number
  4148. * }}
  4149. * @private
  4150. */
  4151. parseTilesLayout_(tilesLayout) {
  4152. if (!tilesLayout) {
  4153. return null;
  4154. }
  4155. // This expression is used to detect one or more numbers (0-9) followed
  4156. // by an x and after one or more numbers (0-9)
  4157. const match = /(\d+)x(\d+)/.exec(tilesLayout);
  4158. if (!match) {
  4159. shaka.log.warning('Tiles layout does not contain a valid format ' +
  4160. ' (columns x rows)');
  4161. return null;
  4162. }
  4163. const columns = parseInt(match[1], 10);
  4164. const rows = parseInt(match[2], 10);
  4165. return {columns, rows};
  4166. }
  4167. /**
  4168. * Return a Thumbnail object from a image track Id and time.
  4169. *
  4170. * If the player has not loaded content, this will return a null.
  4171. *
  4172. * @param {number} trackId
  4173. * @param {number} time
  4174. * @return {!Promise.<?shaka.extern.Thumbnail>}
  4175. * @export
  4176. */
  4177. async getThumbnails(trackId, time) {
  4178. if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE &&
  4179. this.loadMode_ != shaka.Player.LoadMode.SRC_EQUALS) {
  4180. return null;
  4181. }
  4182. let imageStreams = this.externalSrcEqualsThumbnailsStreams_;
  4183. if (this.manifest_) {
  4184. imageStreams = this.manifest_.imageStreams;
  4185. }
  4186. const imageStream = imageStreams.find(
  4187. (stream) => stream.id == trackId);
  4188. if (!imageStream) {
  4189. return null;
  4190. }
  4191. if (!imageStream.segmentIndex) {
  4192. await imageStream.createSegmentIndex();
  4193. }
  4194. const referencePosition = imageStream.segmentIndex.find(time);
  4195. if (referencePosition == null) {
  4196. return null;
  4197. }
  4198. const reference = imageStream.segmentIndex.get(referencePosition);
  4199. const dimensions = this.parseTilesLayout_(
  4200. reference.getTilesLayout() || imageStream.tilesLayout);
  4201. if (!dimensions) {
  4202. return null;
  4203. }
  4204. const fullImageWidth = imageStream.width || 0;
  4205. const fullImageHeight = imageStream.height || 0;
  4206. let width = fullImageWidth / dimensions.columns;
  4207. let height = fullImageHeight / dimensions.rows;
  4208. const totalImages = dimensions.columns * dimensions.rows;
  4209. const segmentDuration = reference.trueEndTime - reference.startTime;
  4210. const thumbnailDuration =
  4211. reference.getTileDuration() || (segmentDuration / totalImages);
  4212. let thumbnailTime = reference.startTime;
  4213. let positionX = 0;
  4214. let positionY = 0;
  4215. // If the number of images in the segment is greater than 1, we have to
  4216. // find the correct image. For that we will return to the app the
  4217. // coordinates of the position of the correct image.
  4218. // Image search is always from left to right and top to bottom.
  4219. // Note: The time between images within the segment is always
  4220. // equidistant.
  4221. //
  4222. // Eg: Total images 5, tileLayout 5x1, segmentDuration 5, thumbnailTime 2
  4223. // positionX = 0.4 * fullImageWidth
  4224. // positionY = 0
  4225. if (totalImages > 1) {
  4226. const thumbnailPosition =
  4227. Math.floor((time - reference.startTime) / thumbnailDuration);
  4228. thumbnailTime = reference.startTime +
  4229. (thumbnailPosition * thumbnailDuration);
  4230. positionX = (thumbnailPosition % dimensions.columns) * width;
  4231. positionY = Math.floor(thumbnailPosition / dimensions.columns) * height;
  4232. }
  4233. let sprite = false;
  4234. const thumbnailSprite = reference.getThumbnailSprite();
  4235. if (thumbnailSprite) {
  4236. sprite = true;
  4237. height = thumbnailSprite.height;
  4238. positionX = thumbnailSprite.positionX;
  4239. positionY = thumbnailSprite.positionY;
  4240. width = thumbnailSprite.width;
  4241. }
  4242. return {
  4243. segment: reference,
  4244. imageHeight: fullImageHeight,
  4245. imageWidth: fullImageWidth,
  4246. height: height,
  4247. positionX: positionX,
  4248. positionY: positionY,
  4249. startTime: thumbnailTime,
  4250. duration: thumbnailDuration,
  4251. uris: reference.getUris(),
  4252. width: width,
  4253. sprite: sprite,
  4254. };
  4255. }
  4256. /**
  4257. * Select a specific text track. <code>track</code> should come from a call to
  4258. * <code>getTextTracks</code>. If the track is not found, this will be a
  4259. * no-op. If the player has not loaded content, this will be a no-op.
  4260. *
  4261. * <p>
  4262. * Note that <code>AdaptationEvents</code> are not fired for manual track
  4263. * selections.
  4264. *
  4265. * @param {shaka.extern.Track} track
  4266. * @export
  4267. */
  4268. selectTextTrack(track) {
  4269. if (this.manifest_ && this.streamingEngine_) {
  4270. const stream = this.manifest_.textStreams.find(
  4271. (stream) => stream.id == track.id);
  4272. if (!stream) {
  4273. shaka.log.error('No stream with id', track.id);
  4274. return;
  4275. }
  4276. if (stream == this.streamingEngine_.getCurrentTextStream()) {
  4277. shaka.log.debug('Text track already selected.');
  4278. return;
  4279. }
  4280. // Add entries to the history.
  4281. this.addTextStreamToSwitchHistory_(stream, /* fromAdaptation= */ false);
  4282. this.streamingEngine_.switchTextStream(stream);
  4283. this.onTextChanged_();
  4284. // Workaround for
  4285. // https://github.com/shaka-project/shaka-player/issues/1299
  4286. // When track is selected, back-propagate the language to
  4287. // currentTextLanguage_.
  4288. this.currentTextLanguage_ = stream.language;
  4289. } else if (this.video_ && this.video_.src && this.video_.textTracks) {
  4290. const textTracks = this.getFilteredTextTracks_();
  4291. for (const textTrack of textTracks) {
  4292. if (shaka.util.StreamUtils.html5TrackId(textTrack) == track.id) {
  4293. // Leave the track in 'hidden' if it's selected but not showing.
  4294. textTrack.mode = this.isTextVisible_ ? 'showing' : 'hidden';
  4295. } else {
  4296. // Safari allows multiple text tracks to have mode == 'showing', so be
  4297. // explicit in resetting the others.
  4298. textTrack.mode = 'disabled';
  4299. }
  4300. }
  4301. this.onTextChanged_();
  4302. }
  4303. }
  4304. /**
  4305. * Select a specific variant track to play. <code>track</code> should come
  4306. * from a call to <code>getVariantTracks</code>. If <code>track</code> cannot
  4307. * be found, this will be a no-op. If the player has not loaded content, this
  4308. * will be a no-op.
  4309. *
  4310. * <p>
  4311. * Changing variants will take effect once the currently buffered content has
  4312. * been played. To force the change to happen sooner, use
  4313. * <code>clearBuffer</code> with <code>safeMargin</code>. Setting
  4314. * <code>clearBuffer</code> to <code>true</code> will clear all buffered
  4315. * content after <code>safeMargin</code>, allowing the new variant to start
  4316. * playing sooner.
  4317. *
  4318. * <p>
  4319. * Note that <code>AdaptationEvents</code> are not fired for manual track
  4320. * selections.
  4321. *
  4322. * @param {shaka.extern.Track} track
  4323. * @param {boolean=} clearBuffer
  4324. * @param {number=} safeMargin Optional amount of buffer (in seconds) to
  4325. * retain when clearing the buffer. Useful for switching variant quickly
  4326. * without causing a buffering event. Defaults to 0 if not provided. Ignored
  4327. * if clearBuffer is false. Can cause hiccups on some browsers if chosen too
  4328. * small, e.g. The amount of two segments is a fair minimum to consider as
  4329. * safeMargin value.
  4330. * @export
  4331. */
  4332. selectVariantTrack(track, clearBuffer = false, safeMargin = 0) {
  4333. if (this.manifest_ && this.streamingEngine_) {
  4334. if (this.config_.abr.enabled) {
  4335. shaka.log.alwaysWarn('Changing tracks while abr manager is enabled ' +
  4336. 'will likely result in the selected track ' +
  4337. 'being overriden. Consider disabling abr before ' +
  4338. 'calling selectVariantTrack().');
  4339. }
  4340. const variant = this.manifest_.variants.find(
  4341. (variant) => variant.id == track.id);
  4342. if (!variant) {
  4343. shaka.log.error('No variant with id', track.id);
  4344. return;
  4345. }
  4346. // Double check that the track is allowed to be played. The track list
  4347. // should only contain playable variants, but if restrictions change and
  4348. // |selectVariantTrack| is called before the track list is updated, we
  4349. // could get a now-restricted variant.
  4350. if (!shaka.util.StreamUtils.isPlayable(variant)) {
  4351. shaka.log.error('Unable to switch to restricted track', track.id);
  4352. return;
  4353. }
  4354. this.switchVariant_(
  4355. variant, /* fromAdaptation= */ false, clearBuffer, safeMargin);
  4356. // Workaround for
  4357. // https://github.com/shaka-project/shaka-player/issues/1299
  4358. // When track is selected, back-propagate the language to
  4359. // currentAudioLanguage_.
  4360. this.currentAdaptationSetCriteria_ = new shaka.media.ExampleBasedCriteria(
  4361. variant,
  4362. this.config_.mediaSource.codecSwitchingStrategy,
  4363. this.config_.manifest.dash.enableAudioGroups);
  4364. // Update AbrManager variants to match these new settings.
  4365. this.updateAbrManagerVariants_();
  4366. } else if (this.video_ && this.video_.audioTracks) {
  4367. // Safari's native HLS won't let you choose an explicit variant, though
  4368. // you can choose audio languages this way.
  4369. const audioTracks = Array.from(this.video_.audioTracks);
  4370. for (const audioTrack of audioTracks) {
  4371. if (shaka.util.StreamUtils.html5TrackId(audioTrack) == track.id) {
  4372. // This will reset the "enabled" of other tracks to false.
  4373. this.switchHtml5Track_(audioTrack);
  4374. return;
  4375. }
  4376. }
  4377. }
  4378. }
  4379. /**
  4380. * Return a list of audio language-role combinations available. If the
  4381. * player has not loaded any content, this will return an empty list.
  4382. *
  4383. * @return {!Array.<shaka.extern.LanguageRole>}
  4384. * @export
  4385. */
  4386. getAudioLanguagesAndRoles() {
  4387. return shaka.Player.getLanguageAndRolesFrom_(this.getVariantTracks());
  4388. }
  4389. /**
  4390. * Return a list of text language-role combinations available. If the player
  4391. * has not loaded any content, this will be return an empty list.
  4392. *
  4393. * @return {!Array.<shaka.extern.LanguageRole>}
  4394. * @export
  4395. */
  4396. getTextLanguagesAndRoles() {
  4397. return shaka.Player.getLanguageAndRolesFrom_(this.getTextTracks());
  4398. }
  4399. /**
  4400. * Return a list of audio languages available. If the player has not loaded
  4401. * any content, this will return an empty list.
  4402. *
  4403. * @return {!Array.<string>}
  4404. * @export
  4405. */
  4406. getAudioLanguages() {
  4407. return Array.from(shaka.Player.getLanguagesFrom_(this.getVariantTracks()));
  4408. }
  4409. /**
  4410. * Return a list of text languages available. If the player has not loaded
  4411. * any content, this will return an empty list.
  4412. *
  4413. * @return {!Array.<string>}
  4414. * @export
  4415. */
  4416. getTextLanguages() {
  4417. return Array.from(shaka.Player.getLanguagesFrom_(this.getTextTracks()));
  4418. }
  4419. /**
  4420. * Sets the current audio language and current variant role to the selected
  4421. * language, role and channel count, and chooses a new variant if need be.
  4422. * If the player has not loaded any content, this will be a no-op.
  4423. *
  4424. * @param {string} language
  4425. * @param {string=} role
  4426. * @param {number=} channelsCount
  4427. * @param {number=} safeMargin
  4428. * @param {string=} codec
  4429. * @export
  4430. */
  4431. selectAudioLanguage(language, role, channelsCount = 0, safeMargin = 0,
  4432. codec = '') {
  4433. if (this.manifest_ && this.playhead_) {
  4434. this.currentAdaptationSetCriteria_ =
  4435. new shaka.media.PreferenceBasedCriteria(
  4436. language,
  4437. role || '',
  4438. channelsCount,
  4439. /* hdrLevel= */ '',
  4440. /* spatialAudio= */ false,
  4441. /* videoLayout= */ '',
  4442. /* audioLabel= */ '',
  4443. /* videoLabel= */ '',
  4444. this.config_.mediaSource.codecSwitchingStrategy,
  4445. this.config_.manifest.dash.enableAudioGroups,
  4446. codec);
  4447. const diff = (a, b) => {
  4448. if (!a.video && !b.video) {
  4449. return 0;
  4450. } else if (!a.video || !b.video) {
  4451. return Infinity;
  4452. } else {
  4453. return Math.abs((a.video.height || 0) - (b.video.height || 0)) +
  4454. Math.abs((a.video.width || 0) - (b.video.width || 0));
  4455. }
  4456. };
  4457. // Find the variant whose size is closest to the active variant. This
  4458. // ensures we stay at about the same resolution when just changing the
  4459. // language/role.
  4460. const active = this.streamingEngine_.getCurrentVariant();
  4461. const set =
  4462. this.currentAdaptationSetCriteria_.create(this.manifest_.variants);
  4463. let bestVariant = null;
  4464. for (const curVariant of set.values()) {
  4465. if (!shaka.util.StreamUtils.isPlayable(curVariant)) {
  4466. continue;
  4467. }
  4468. if (!bestVariant ||
  4469. diff(bestVariant, active) > diff(curVariant, active)) {
  4470. bestVariant = curVariant;
  4471. }
  4472. }
  4473. if (bestVariant) {
  4474. const track = shaka.util.StreamUtils.variantToTrack(bestVariant);
  4475. this.selectVariantTrack(track, /* clearBuffer= */ true, safeMargin);
  4476. return;
  4477. }
  4478. // If we haven't switched yet, just use ABR to find a new track.
  4479. this.chooseVariantAndSwitch_();
  4480. } else if (this.video_ && this.video_.audioTracks) {
  4481. const track = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
  4482. this.getVariantTracks(), language, role || '', false)[0];
  4483. if (track) {
  4484. this.selectVariantTrack(track);
  4485. }
  4486. }
  4487. }
  4488. /**
  4489. * Sets the current text language and current text role to the selected
  4490. * language and role, and chooses a new variant if need be. If the player has
  4491. * not loaded any content, this will be a no-op.
  4492. *
  4493. * @param {string} language
  4494. * @param {string=} role
  4495. * @param {boolean=} forced
  4496. * @export
  4497. */
  4498. selectTextLanguage(language, role, forced = false) {
  4499. if (this.manifest_ && this.playhead_) {
  4500. this.currentTextLanguage_ = language;
  4501. this.currentTextRole_ = role || '';
  4502. this.currentTextForced_ = forced;
  4503. const chosenText = this.chooseTextStream_();
  4504. if (chosenText) {
  4505. if (chosenText == this.streamingEngine_.getCurrentTextStream()) {
  4506. shaka.log.debug('Text track already selected.');
  4507. return;
  4508. }
  4509. this.addTextStreamToSwitchHistory_(
  4510. chosenText, /* fromAdaptation= */ false);
  4511. if (this.shouldStreamText_()) {
  4512. this.streamingEngine_.switchTextStream(chosenText);
  4513. this.onTextChanged_();
  4514. }
  4515. }
  4516. } else {
  4517. const track = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
  4518. this.getTextTracks(), language, role || '', forced)[0];
  4519. if (track) {
  4520. this.selectTextTrack(track);
  4521. }
  4522. }
  4523. }
  4524. /**
  4525. * Select variant tracks that have a given label. This assumes the
  4526. * label uniquely identifies an audio stream, so all the variants
  4527. * are expected to have the same variant.audio.
  4528. *
  4529. * @param {string} label
  4530. * @param {boolean=} clearBuffer Optional clear buffer or not when
  4531. * switch to new variant
  4532. * Defaults to true if not provided
  4533. * @param {number=} safeMargin Optional amount of buffer (in seconds) to
  4534. * retain when clearing the buffer.
  4535. * Defaults to 0 if not provided. Ignored if clearBuffer is false.
  4536. * @export
  4537. */
  4538. selectVariantsByLabel(label, clearBuffer = true, safeMargin = 0) {
  4539. if (this.manifest_ && this.playhead_) {
  4540. let firstVariantWithLabel = null;
  4541. for (const variant of this.manifest_.variants) {
  4542. if (variant.audio.label == label) {
  4543. firstVariantWithLabel = variant;
  4544. break;
  4545. }
  4546. }
  4547. if (firstVariantWithLabel == null) {
  4548. shaka.log.warning('No variants were found with label: ' +
  4549. label + '. Ignoring the request to switch.');
  4550. return;
  4551. }
  4552. // Label is a unique identifier of a variant's audio stream.
  4553. // Because of that we assume that all the variants with the same
  4554. // label have the same language.
  4555. this.currentAdaptationSetCriteria_ =
  4556. new shaka.media.PreferenceBasedCriteria(
  4557. firstVariantWithLabel.language,
  4558. /* role= */ '',
  4559. /* channelCount= */ 0,
  4560. /* hdrLevel= */ '',
  4561. /* spatialAudio= */ false,
  4562. /* videoLayout= */ '',
  4563. label,
  4564. /* videoLabel= */ '',
  4565. this.config_.mediaSource.codecSwitchingStrategy,
  4566. this.config_.manifest.dash.enableAudioGroups);
  4567. this.chooseVariantAndSwitch_(clearBuffer, safeMargin);
  4568. } else if (this.video_ && this.video_.audioTracks) {
  4569. const audioTracks = Array.from(this.video_.audioTracks);
  4570. let trackMatch = null;
  4571. for (const audioTrack of audioTracks) {
  4572. if (audioTrack.label == label) {
  4573. trackMatch = audioTrack;
  4574. }
  4575. }
  4576. if (trackMatch) {
  4577. this.switchHtml5Track_(trackMatch);
  4578. }
  4579. }
  4580. }
  4581. /**
  4582. * Check if the text displayer is enabled.
  4583. *
  4584. * @return {boolean}
  4585. * @export
  4586. */
  4587. isTextTrackVisible() {
  4588. const expected = this.isTextVisible_;
  4589. if (this.mediaSourceEngine_ &&
  4590. this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
  4591. // Make sure our values are still in-sync.
  4592. const actual = this.mediaSourceEngine_.getTextDisplayer().isTextVisible();
  4593. goog.asserts.assert(
  4594. actual == expected, 'text visibility has fallen out of sync');
  4595. // Always return the actual value so that the app has the most accurate
  4596. // information (in the case that the values come out of sync in prod).
  4597. return actual;
  4598. } else if (this.video_ && this.video_.src && this.video_.textTracks) {
  4599. const textTracks = this.getFilteredTextTracks_();
  4600. return textTracks.some((t) => t.mode == 'showing');
  4601. }
  4602. return expected;
  4603. }
  4604. /**
  4605. * Return a list of chapters tracks.
  4606. *
  4607. * @return {!Array.<shaka.extern.Track>}
  4608. * @export
  4609. */
  4610. getChaptersTracks() {
  4611. if (this.video_ && this.video_.src && this.video_.textTracks) {
  4612. const textTracks = this.getChaptersTracks_();
  4613. const StreamUtils = shaka.util.StreamUtils;
  4614. return textTracks.map((text) => StreamUtils.html5TextTrackToTrack(text));
  4615. } else {
  4616. return [];
  4617. }
  4618. }
  4619. /**
  4620. * This returns the list of chapters.
  4621. *
  4622. * @param {string} language
  4623. * @return {!Array.<shaka.extern.Chapter>}
  4624. * @export
  4625. */
  4626. getChapters(language) {
  4627. if (!this.video_ || !this.video_.src || !this.video_.textTracks) {
  4628. return [];
  4629. }
  4630. const LanguageUtils = shaka.util.LanguageUtils;
  4631. const inputlanguage = LanguageUtils.normalize(language);
  4632. const chaptersTracks = this.getChaptersTracks_();
  4633. const chaptersTracksWithLanguage = chaptersTracks
  4634. .filter((t) => LanguageUtils.normalize(t.language) == inputlanguage);
  4635. if (!chaptersTracksWithLanguage || !chaptersTracksWithLanguage.length) {
  4636. return [];
  4637. }
  4638. const chapters = [];
  4639. const uniqueChapters = new Set();
  4640. for (const chaptersTrack of chaptersTracksWithLanguage) {
  4641. if (chaptersTrack && chaptersTrack.cues) {
  4642. for (const cue of chaptersTrack.cues) {
  4643. let id = cue.id;
  4644. if (!id || id == '') {
  4645. id = cue.startTime + '-' + cue.endTime + '-' + cue.text;
  4646. }
  4647. /** @type {shaka.extern.Chapter} */
  4648. const chapter = {
  4649. id: id,
  4650. title: cue.text,
  4651. startTime: cue.startTime,
  4652. endTime: cue.endTime,
  4653. };
  4654. if (!uniqueChapters.has(id)) {
  4655. chapters.push(chapter);
  4656. uniqueChapters.add(id);
  4657. }
  4658. }
  4659. }
  4660. }
  4661. return chapters;
  4662. }
  4663. /**
  4664. * Ignore the TextTracks with the 'metadata' or 'chapters' kind, or the one
  4665. * generated by the SimpleTextDisplayer.
  4666. *
  4667. * @return {!Array.<TextTrack>}
  4668. * @private
  4669. */
  4670. getFilteredTextTracks_() {
  4671. goog.asserts.assert(this.video_.textTracks,
  4672. 'TextTracks should be valid.');
  4673. return Array.from(this.video_.textTracks)
  4674. .filter((t) => t.kind != 'metadata' && t.kind != 'chapters' &&
  4675. t.label != shaka.Player.TextTrackLabel);
  4676. }
  4677. /**
  4678. * Get the TextTracks with the 'metadata' kind.
  4679. *
  4680. * @return {!Array.<TextTrack>}
  4681. * @private
  4682. */
  4683. getMetadataTracks_() {
  4684. goog.asserts.assert(this.video_.textTracks,
  4685. 'TextTracks should be valid.');
  4686. return Array.from(this.video_.textTracks)
  4687. .filter((t) => t.kind == 'metadata');
  4688. }
  4689. /**
  4690. * Get the TextTracks with the 'chapters' kind.
  4691. *
  4692. * @return {!Array.<TextTrack>}
  4693. * @private
  4694. */
  4695. getChaptersTracks_() {
  4696. goog.asserts.assert(this.video_.textTracks,
  4697. 'TextTracks should be valid.');
  4698. return Array.from(this.video_.textTracks)
  4699. .filter((t) => t.kind == 'chapters');
  4700. }
  4701. /**
  4702. * Enable or disable the text displayer. If the player is in an unloaded
  4703. * state, the request will be applied next time content is loaded.
  4704. *
  4705. * @param {boolean} isVisible
  4706. * @export
  4707. */
  4708. setTextTrackVisibility(isVisible) {
  4709. const oldVisibilty = this.isTextVisible_;
  4710. // Convert to boolean in case apps pass 0/1 instead false/true.
  4711. const newVisibility = !!isVisible;
  4712. if (oldVisibilty == newVisibility) {
  4713. return;
  4714. }
  4715. this.isTextVisible_ = newVisibility;
  4716. // Hold of on setting the text visibility until we have all the components
  4717. // we need. This ensures that they stay in-sync.
  4718. if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
  4719. this.mediaSourceEngine_.getTextDisplayer()
  4720. .setTextVisibility(newVisibility);
  4721. // When the user wants to see captions, we stream captions. When the user
  4722. // doesn't want to see captions, we don't stream captions. This is to
  4723. // avoid bandwidth consumption by an unused resource. The app developer
  4724. // can override this and configure us to always stream captions.
  4725. if (!this.config_.streaming.alwaysStreamText) {
  4726. if (newVisibility) {
  4727. if (this.streamingEngine_.getCurrentTextStream()) {
  4728. // We already have a selected text stream.
  4729. } else {
  4730. // Find the text stream that best matches the user's preferences.
  4731. const streams =
  4732. shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
  4733. this.manifest_.textStreams,
  4734. this.currentTextLanguage_,
  4735. this.currentTextRole_,
  4736. this.currentTextForced_);
  4737. // It is possible that there are no streams to play.
  4738. if (streams.length > 0) {
  4739. this.streamingEngine_.switchTextStream(streams[0]);
  4740. this.onTextChanged_();
  4741. }
  4742. }
  4743. } else {
  4744. this.streamingEngine_.unloadTextStream();
  4745. }
  4746. }
  4747. } else if (this.video_ && this.video_.src && this.video_.textTracks) {
  4748. const textTracks = this.getFilteredTextTracks_();
  4749. // Find the active track by looking for one which is not disabled. This
  4750. // is the only way to identify the track which is currently displayed.
  4751. // Set it to 'showing' or 'hidden' based on newVisibility.
  4752. for (const textTrack of textTracks) {
  4753. if (textTrack.mode != 'disabled') {
  4754. textTrack.mode = newVisibility ? 'showing' : 'hidden';
  4755. }
  4756. }
  4757. }
  4758. // We need to fire the event after we have updated everything so that
  4759. // everything will be in a stable state when the app responds to the
  4760. // event.
  4761. this.onTextTrackVisibility_();
  4762. }
  4763. /**
  4764. * Get the current playhead position as a date.
  4765. *
  4766. * @return {Date}
  4767. * @export
  4768. */
  4769. getPlayheadTimeAsDate() {
  4770. let presentationTime = 0;
  4771. if (this.playhead_) {
  4772. presentationTime = this.playhead_.getTime();
  4773. } else if (this.startTime_ == null) {
  4774. // A live stream with no requested start time and no playhead yet. We
  4775. // would start at the live edge, but we don't have that yet, so return
  4776. // the current date & time.
  4777. return new Date();
  4778. } else {
  4779. // A specific start time has been requested. This is what Playhead will
  4780. // use once it is created.
  4781. presentationTime = this.startTime_;
  4782. }
  4783. if (this.manifest_) {
  4784. const timeline = this.manifest_.presentationTimeline;
  4785. const startTime = timeline.getInitialProgramDateTime() ||
  4786. timeline.getPresentationStartTime();
  4787. return new Date(/* ms= */ (startTime + presentationTime) * 1000);
  4788. } else if (this.video_ && this.video_.getStartDate) {
  4789. // Apple's native HLS gives us getStartDate(), which is only available if
  4790. // EXT-X-PROGRAM-DATETIME is in the playlist.
  4791. const startDate = this.video_.getStartDate();
  4792. if (isNaN(startDate.getTime())) {
  4793. shaka.log.warning(
  4794. 'EXT-X-PROGRAM-DATETIME required to get playhead time as Date!');
  4795. return null;
  4796. }
  4797. return new Date(startDate.getTime() + (presentationTime * 1000));
  4798. } else {
  4799. shaka.log.warning('No way to get playhead time as Date!');
  4800. return null;
  4801. }
  4802. }
  4803. /**
  4804. * Get the presentation start time as a date.
  4805. *
  4806. * @return {Date}
  4807. * @export
  4808. */
  4809. getPresentationStartTimeAsDate() {
  4810. if (this.manifest_) {
  4811. const timeline = this.manifest_.presentationTimeline;
  4812. const startTime = timeline.getInitialProgramDateTime() ||
  4813. timeline.getPresentationStartTime();
  4814. goog.asserts.assert(startTime != null,
  4815. 'Presentation start time should not be null!');
  4816. return new Date(/* ms= */ startTime * 1000);
  4817. } else if (this.video_ && this.video_.getStartDate) {
  4818. // Apple's native HLS gives us getStartDate(), which is only available if
  4819. // EXT-X-PROGRAM-DATETIME is in the playlist.
  4820. const startDate = this.video_.getStartDate();
  4821. if (isNaN(startDate.getTime())) {
  4822. shaka.log.warning(
  4823. 'EXT-X-PROGRAM-DATETIME required to get presentation start time ' +
  4824. 'as Date!');
  4825. return null;
  4826. }
  4827. return startDate;
  4828. } else {
  4829. shaka.log.warning('No way to get presentation start time as Date!');
  4830. return null;
  4831. }
  4832. }
  4833. /**
  4834. * Get the presentation segment availability duration. This should only be
  4835. * called when the player has loaded a live stream. If the player has not
  4836. * loaded a live stream, this will return <code>null</code>.
  4837. *
  4838. * @return {?number}
  4839. * @export
  4840. */
  4841. getSegmentAvailabilityDuration() {
  4842. if (!this.isLive()) {
  4843. shaka.log.warning('getSegmentAvailabilityDuration is for live streams!');
  4844. return null;
  4845. }
  4846. if (this.manifest_) {
  4847. const timeline = this.manifest_.presentationTimeline;
  4848. return timeline.getSegmentAvailabilityDuration();
  4849. } else {
  4850. shaka.log.warning('No way to get segment segment availability duration!');
  4851. return null;
  4852. }
  4853. }
  4854. /**
  4855. * Get information about what the player has buffered. If the player has not
  4856. * loaded content or is currently loading content, the buffered content will
  4857. * be empty.
  4858. *
  4859. * @return {shaka.extern.BufferedInfo}
  4860. * @export
  4861. */
  4862. getBufferedInfo() {
  4863. if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
  4864. return this.mediaSourceEngine_.getBufferedInfo();
  4865. }
  4866. const info = {
  4867. total: [],
  4868. audio: [],
  4869. video: [],
  4870. text: [],
  4871. };
  4872. if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
  4873. const TimeRangesUtils = shaka.media.TimeRangesUtils;
  4874. info.total = TimeRangesUtils.getBufferedInfo(this.video_.buffered);
  4875. }
  4876. return info;
  4877. }
  4878. /**
  4879. * Get statistics for the current playback session. If the player is not
  4880. * playing content, this will return an empty stats object.
  4881. *
  4882. * @return {shaka.extern.Stats}
  4883. * @export
  4884. */
  4885. getStats() {
  4886. // If the Player is not in a fully-loaded state, then return an empty stats
  4887. // blob so that this call will never fail.
  4888. const loaded = this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ||
  4889. this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS;
  4890. if (!loaded) {
  4891. return shaka.util.Stats.getEmptyBlob();
  4892. }
  4893. this.updateStateHistory_();
  4894. goog.asserts.assert(this.video_, 'If we have stats, we should have video_');
  4895. const element = /** @type {!HTMLVideoElement} */ (this.video_);
  4896. const completionRatio = element.currentTime / element.duration;
  4897. if (!isNaN(completionRatio)) {
  4898. this.stats_.setCompletionPercent(Math.round(100 * completionRatio));
  4899. }
  4900. if (this.playhead_) {
  4901. this.stats_.setGapsJumped(this.playhead_.getGapsJumped());
  4902. this.stats_.setStallsDetected(this.playhead_.getStallsDetected());
  4903. }
  4904. if (element.getVideoPlaybackQuality) {
  4905. const info = element.getVideoPlaybackQuality();
  4906. this.stats_.setDroppedFrames(
  4907. Number(info.droppedVideoFrames),
  4908. Number(info.totalVideoFrames));
  4909. this.stats_.setCorruptedFrames(Number(info.corruptedVideoFrames));
  4910. }
  4911. const licenseSeconds =
  4912. this.drmEngine_ ? this.drmEngine_.getLicenseTime() : NaN;
  4913. this.stats_.setLicenseTime(licenseSeconds);
  4914. if (this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE) {
  4915. // Event through we are loaded, it is still possible that we don't have a
  4916. // variant yet because we set the load mode before we select the first
  4917. // variant to stream.
  4918. const variant = this.streamingEngine_.getCurrentVariant();
  4919. const textStream = this.streamingEngine_.getCurrentTextStream();
  4920. if (variant) {
  4921. const rate = this.playRateController_ ?
  4922. this.playRateController_.getRealRate() : 1;
  4923. const variantBandwidth = rate * variant.bandwidth;
  4924. let currentStreamBandwidth = variantBandwidth;
  4925. if (textStream && textStream.bandwidth) {
  4926. currentStreamBandwidth += (rate * textStream.bandwidth);
  4927. }
  4928. this.stats_.setCurrentStreamBandwidth(currentStreamBandwidth);
  4929. }
  4930. if (variant && variant.video) {
  4931. this.stats_.setResolution(
  4932. /* width= */ variant.video.width || NaN,
  4933. /* height= */ variant.video.height || NaN);
  4934. }
  4935. if (this.isLive()) {
  4936. const now = this.getPresentationStartTimeAsDate().valueOf() +
  4937. element.currentTime * 1000;
  4938. const latency = (Date.now() - now) / 1000;
  4939. this.stats_.setLiveLatency(latency);
  4940. }
  4941. if (this.manifest_) {
  4942. this.stats_.setManifestPeriodCount(this.manifest_.periodCount);
  4943. this.stats_.setManifestGapCount(this.manifest_.gapCount);
  4944. if (this.manifest_.presentationTimeline) {
  4945. const maxSegmentDuration =
  4946. this.manifest_.presentationTimeline.getMaxSegmentDuration();
  4947. this.stats_.setMaxSegmentDuration(maxSegmentDuration);
  4948. }
  4949. }
  4950. const estimate = this.abrManager_.getBandwidthEstimate();
  4951. this.stats_.setBandwidthEstimate(estimate);
  4952. }
  4953. if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
  4954. this.stats_.setResolution(
  4955. /* width= */ element.videoWidth || NaN,
  4956. /* height= */ element.videoHeight || NaN);
  4957. }
  4958. return this.stats_.getBlob();
  4959. }
  4960. /**
  4961. * Adds the given text track to the loaded manifest. <code>load()</code> must
  4962. * resolve before calling. The presentation must have a duration.
  4963. *
  4964. * This returns the created track, which can immediately be selected by the
  4965. * application. The track will not be automatically selected.
  4966. *
  4967. * @param {string} uri
  4968. * @param {string} language
  4969. * @param {string} kind
  4970. * @param {string=} mimeType
  4971. * @param {string=} codec
  4972. * @param {string=} label
  4973. * @param {boolean=} forced
  4974. * @return {!Promise.<shaka.extern.Track>}
  4975. * @export
  4976. */
  4977. async addTextTrackAsync(uri, language, kind, mimeType, codec, label,
  4978. forced = false) {
  4979. if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE &&
  4980. this.loadMode_ != shaka.Player.LoadMode.SRC_EQUALS) {
  4981. shaka.log.error(
  4982. 'Must call load() and wait for it to resolve before adding text ' +
  4983. 'tracks.');
  4984. throw new shaka.util.Error(
  4985. shaka.util.Error.Severity.RECOVERABLE,
  4986. shaka.util.Error.Category.PLAYER,
  4987. shaka.util.Error.Code.CONTENT_NOT_LOADED);
  4988. }
  4989. if (kind != 'subtitles' && kind != 'captions') {
  4990. shaka.log.alwaysWarn(
  4991. 'Using a kind value different of `subtitles` or `captions` can ' +
  4992. 'cause unwanted issues.');
  4993. }
  4994. if (!mimeType) {
  4995. mimeType = await this.getTextMimetype_(uri);
  4996. }
  4997. let adCuePoints = [];
  4998. if (this.adManager_) {
  4999. adCuePoints = this.adManager_.getCuePoints();
  5000. }
  5001. if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
  5002. if (forced) {
  5003. // See: https://github.com/whatwg/html/issues/4472
  5004. kind = 'forced';
  5005. }
  5006. await this.addSrcTrackElement_(uri, language, kind, mimeType, label || '',
  5007. adCuePoints);
  5008. const LanguageUtils = shaka.util.LanguageUtils;
  5009. const languageNormalized = LanguageUtils.normalize(language);
  5010. const textTracks = this.getTextTracks();
  5011. const srcTrack = textTracks.find((t) => {
  5012. return LanguageUtils.normalize(t.language) == languageNormalized &&
  5013. t.label == (label || '') &&
  5014. t.kind == kind;
  5015. });
  5016. if (srcTrack) {
  5017. this.onTracksChanged_();
  5018. return srcTrack;
  5019. }
  5020. // This should not happen, but there are browser implementations that may
  5021. // not support the Track element.
  5022. shaka.log.error('Cannot add this text when loaded with src=');
  5023. throw new shaka.util.Error(
  5024. shaka.util.Error.Severity.RECOVERABLE,
  5025. shaka.util.Error.Category.TEXT,
  5026. shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_TEXT_TO_SRC_EQUALS);
  5027. }
  5028. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  5029. let duration = this.video_.duration;
  5030. if (this.manifest_) {
  5031. duration = this.manifest_.presentationTimeline.getDuration();
  5032. }
  5033. if (duration == Infinity) {
  5034. throw new shaka.util.Error(
  5035. shaka.util.Error.Severity.RECOVERABLE,
  5036. shaka.util.Error.Category.MANIFEST,
  5037. shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_TEXT_TO_LIVE_STREAM);
  5038. }
  5039. if (adCuePoints.length) {
  5040. goog.asserts.assert(
  5041. this.networkingEngine_, 'Need networking engine.');
  5042. const data = await this.getTextData_(uri,
  5043. this.networkingEngine_,
  5044. this.config_.streaming.retryParameters);
  5045. const vvtText = this.convertToWebVTT_(data, mimeType, adCuePoints);
  5046. const blob = new Blob([vvtText], {type: 'text/vtt'});
  5047. uri = shaka.media.MediaSourceEngine.createObjectURL(blob);
  5048. mimeType = 'text/vtt';
  5049. }
  5050. /** @type {shaka.extern.Stream} */
  5051. const stream = {
  5052. id: this.nextExternalStreamId_++,
  5053. originalId: null,
  5054. groupId: null,
  5055. createSegmentIndex: () => Promise.resolve(),
  5056. segmentIndex: shaka.media.SegmentIndex.forSingleSegment(
  5057. /* startTime= */ 0,
  5058. /* duration= */ duration,
  5059. /* uris= */ [uri]),
  5060. mimeType: mimeType || '',
  5061. codecs: codec || '',
  5062. kind: kind,
  5063. encrypted: false,
  5064. drmInfos: [],
  5065. keyIds: new Set(),
  5066. language: language,
  5067. originalLanguage: language,
  5068. label: label || null,
  5069. type: ContentType.TEXT,
  5070. primary: false,
  5071. trickModeVideo: null,
  5072. emsgSchemeIdUris: null,
  5073. roles: [],
  5074. forced: !!forced,
  5075. channelsCount: null,
  5076. audioSamplingRate: null,
  5077. spatialAudio: false,
  5078. closedCaptions: null,
  5079. accessibilityPurpose: null,
  5080. external: true,
  5081. fastSwitching: false,
  5082. fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
  5083. mimeType || '', codec || '')]),
  5084. };
  5085. const fullMimeType = shaka.util.MimeUtils.getFullType(
  5086. stream.mimeType, stream.codecs);
  5087. const supported = shaka.text.TextEngine.isTypeSupported(fullMimeType);
  5088. if (!supported) {
  5089. throw new shaka.util.Error(
  5090. shaka.util.Error.Severity.CRITICAL,
  5091. shaka.util.Error.Category.TEXT,
  5092. shaka.util.Error.Code.MISSING_TEXT_PLUGIN,
  5093. mimeType);
  5094. }
  5095. this.manifest_.textStreams.push(stream);
  5096. this.onTracksChanged_();
  5097. return shaka.util.StreamUtils.textStreamToTrack(stream);
  5098. }
  5099. /**
  5100. * Adds the given thumbnails track to the loaded manifest.
  5101. * <code>load()</code> must resolve before calling. The presentation must
  5102. * have a duration.
  5103. *
  5104. * This returns the created track, which can immediately be used by the
  5105. * application.
  5106. *
  5107. * @param {string} uri
  5108. * @param {string=} mimeType
  5109. * @return {!Promise.<shaka.extern.Track>}
  5110. * @export
  5111. */
  5112. async addThumbnailsTrack(uri, mimeType) {
  5113. if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE &&
  5114. this.loadMode_ != shaka.Player.LoadMode.SRC_EQUALS) {
  5115. shaka.log.error(
  5116. 'Must call load() and wait for it to resolve before adding image ' +
  5117. 'tracks.');
  5118. throw new shaka.util.Error(
  5119. shaka.util.Error.Severity.RECOVERABLE,
  5120. shaka.util.Error.Category.PLAYER,
  5121. shaka.util.Error.Code.CONTENT_NOT_LOADED);
  5122. }
  5123. if (!mimeType) {
  5124. mimeType = await this.getTextMimetype_(uri);
  5125. }
  5126. if (mimeType != 'text/vtt') {
  5127. throw new shaka.util.Error(
  5128. shaka.util.Error.Severity.RECOVERABLE,
  5129. shaka.util.Error.Category.TEXT,
  5130. shaka.util.Error.Code.UNSUPPORTED_EXTERNAL_THUMBNAILS_URI,
  5131. uri);
  5132. }
  5133. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  5134. let duration = this.video_.duration;
  5135. if (this.manifest_) {
  5136. duration = this.manifest_.presentationTimeline.getDuration();
  5137. }
  5138. if (duration == Infinity) {
  5139. throw new shaka.util.Error(
  5140. shaka.util.Error.Severity.RECOVERABLE,
  5141. shaka.util.Error.Category.MANIFEST,
  5142. shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_THUMBNAILS_TO_LIVE_STREAM);
  5143. }
  5144. goog.asserts.assert(
  5145. this.networkingEngine_, 'Need networking engine.');
  5146. const buffer = await this.getTextData_(uri,
  5147. this.networkingEngine_,
  5148. this.config_.streaming.retryParameters);
  5149. const factory = shaka.text.TextEngine.findParser(mimeType);
  5150. if (!factory) {
  5151. throw new shaka.util.Error(
  5152. shaka.util.Error.Severity.CRITICAL,
  5153. shaka.util.Error.Category.TEXT,
  5154. shaka.util.Error.Code.MISSING_TEXT_PLUGIN,
  5155. mimeType);
  5156. }
  5157. const TextParser = factory();
  5158. const time = {
  5159. periodStart: 0,
  5160. segmentStart: 0,
  5161. segmentEnd: duration,
  5162. vttOffset: 0,
  5163. };
  5164. const data = shaka.util.BufferUtils.toUint8(buffer);
  5165. const cues = TextParser.parseMedia(data, time, uri);
  5166. const references = [];
  5167. for (const cue of cues) {
  5168. let uris = null;
  5169. const getUris = () => {
  5170. if (uris == null) {
  5171. uris = shaka.util.ManifestParserUtils.resolveUris(
  5172. [uri], [cue.payload]);
  5173. }
  5174. return uris || [];
  5175. };
  5176. const reference = new shaka.media.SegmentReference(
  5177. cue.startTime,
  5178. cue.endTime,
  5179. getUris,
  5180. /* startByte= */ 0,
  5181. /* endByte= */ null,
  5182. /* initSegmentReference= */ null,
  5183. /* timestampOffset= */ 0,
  5184. /* appendWindowStart= */ 0,
  5185. /* appendWindowEnd= */ Infinity,
  5186. );
  5187. if (cue.payload.includes('#xywh')) {
  5188. const spriteInfo = cue.payload.split('#xywh=')[1].split(',');
  5189. if (spriteInfo.length === 4) {
  5190. reference.setThumbnailSprite({
  5191. height: parseInt(spriteInfo[3], 10),
  5192. positionX: parseInt(spriteInfo[0], 10),
  5193. positionY: parseInt(spriteInfo[1], 10),
  5194. width: parseInt(spriteInfo[2], 10),
  5195. });
  5196. }
  5197. }
  5198. references.push(reference);
  5199. }
  5200. /** @type {shaka.extern.Stream} */
  5201. const stream = {
  5202. id: this.nextExternalStreamId_++,
  5203. originalId: null,
  5204. groupId: null,
  5205. createSegmentIndex: () => Promise.resolve(),
  5206. segmentIndex: new shaka.media.SegmentIndex(references),
  5207. mimeType: mimeType || '',
  5208. codecs: '',
  5209. kind: '',
  5210. encrypted: false,
  5211. drmInfos: [],
  5212. keyIds: new Set(),
  5213. language: 'und',
  5214. originalLanguage: null,
  5215. label: null,
  5216. type: ContentType.IMAGE,
  5217. primary: false,
  5218. trickModeVideo: null,
  5219. emsgSchemeIdUris: null,
  5220. roles: [],
  5221. forced: false,
  5222. channelsCount: null,
  5223. audioSamplingRate: null,
  5224. spatialAudio: false,
  5225. closedCaptions: null,
  5226. tilesLayout: '1x1',
  5227. accessibilityPurpose: null,
  5228. external: true,
  5229. fastSwitching: false,
  5230. fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
  5231. mimeType || '', '')]),
  5232. };
  5233. if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
  5234. this.externalSrcEqualsThumbnailsStreams_.push(stream);
  5235. } else {
  5236. this.manifest_.imageStreams.push(stream);
  5237. }
  5238. this.onTracksChanged_();
  5239. return shaka.util.StreamUtils.imageStreamToTrack(stream);
  5240. }
  5241. /**
  5242. * Adds the given chapters track to the loaded manifest. <code>load()</code>
  5243. * must resolve before calling. The presentation must have a duration.
  5244. *
  5245. * This returns the created track.
  5246. *
  5247. * @param {string} uri
  5248. * @param {string} language
  5249. * @param {string=} mimeType
  5250. * @return {!Promise.<shaka.extern.Track>}
  5251. * @export
  5252. */
  5253. async addChaptersTrack(uri, language, mimeType) {
  5254. if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE &&
  5255. this.loadMode_ != shaka.Player.LoadMode.SRC_EQUALS) {
  5256. shaka.log.error(
  5257. 'Must call load() and wait for it to resolve before adding ' +
  5258. 'chapters tracks.');
  5259. throw new shaka.util.Error(
  5260. shaka.util.Error.Severity.RECOVERABLE,
  5261. shaka.util.Error.Category.PLAYER,
  5262. shaka.util.Error.Code.CONTENT_NOT_LOADED);
  5263. }
  5264. if (!mimeType) {
  5265. mimeType = await this.getTextMimetype_(uri);
  5266. }
  5267. let adCuePoints = [];
  5268. if (this.adManager_) {
  5269. adCuePoints = this.adManager_.getCuePoints();
  5270. }
  5271. /** @type {!HTMLTrackElement} */
  5272. const trackElement = await this.addSrcTrackElement_(
  5273. uri, language, /* kind= */ 'chapters', mimeType, /* label= */ '',
  5274. adCuePoints);
  5275. const chaptersTracks = this.getChaptersTracks();
  5276. const chaptersTrack = chaptersTracks.find((t) => {
  5277. return t.language == language;
  5278. });
  5279. if (chaptersTrack) {
  5280. await new Promise((resolve, reject) => {
  5281. // The chapter data isn't available until the 'load' event fires, and
  5282. // that won't happen until the chapters track is activated by the
  5283. // activateChaptersTrack_ method.
  5284. this.loadEventManager_.listenOnce(trackElement, 'load', resolve);
  5285. this.loadEventManager_.listenOnce(trackElement, 'error', (event) => {
  5286. reject(new shaka.util.Error(
  5287. shaka.util.Error.Severity.RECOVERABLE,
  5288. shaka.util.Error.Category.TEXT,
  5289. shaka.util.Error.Code.CHAPTERS_TRACK_FAILED));
  5290. });
  5291. });
  5292. this.onTracksChanged_();
  5293. return chaptersTrack;
  5294. }
  5295. // This should not happen, but there are browser implementations that may
  5296. // not support the Track element.
  5297. shaka.log.error('Cannot add this text when loaded with src=');
  5298. throw new shaka.util.Error(
  5299. shaka.util.Error.Severity.RECOVERABLE,
  5300. shaka.util.Error.Category.TEXT,
  5301. shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_TEXT_TO_SRC_EQUALS);
  5302. }
  5303. /**
  5304. * @param {string} uri
  5305. * @return {!Promise.<string>}
  5306. * @private
  5307. */
  5308. async getTextMimetype_(uri) {
  5309. let mimeType;
  5310. try {
  5311. goog.asserts.assert(
  5312. this.networkingEngine_, 'Need networking engine.');
  5313. // eslint-disable-next-line require-atomic-updates
  5314. mimeType = await shaka.net.NetworkingUtils.getMimeType(uri,
  5315. this.networkingEngine_,
  5316. this.config_.streaming.retryParameters);
  5317. } catch (error) {}
  5318. if (mimeType) {
  5319. return mimeType;
  5320. }
  5321. shaka.log.error(
  5322. 'The mimeType has not been provided and it could not be deduced ' +
  5323. 'from its uri.');
  5324. throw new shaka.util.Error(
  5325. shaka.util.Error.Severity.RECOVERABLE,
  5326. shaka.util.Error.Category.TEXT,
  5327. shaka.util.Error.Code.TEXT_COULD_NOT_GUESS_MIME_TYPE,
  5328. uri);
  5329. }
  5330. /**
  5331. * @param {string} uri
  5332. * @param {string} language
  5333. * @param {string} kind
  5334. * @param {string} mimeType
  5335. * @param {string} label
  5336. * @param {!Array.<!shaka.extern.AdCuePoint>} adCuePoints
  5337. * @return {!Promise.<!HTMLTrackElement>}
  5338. * @private
  5339. */
  5340. async addSrcTrackElement_(uri, language, kind, mimeType, label,
  5341. adCuePoints) {
  5342. if (mimeType != 'text/vtt' || adCuePoints.length) {
  5343. goog.asserts.assert(
  5344. this.networkingEngine_, 'Need networking engine.');
  5345. const data = await this.getTextData_(uri,
  5346. this.networkingEngine_,
  5347. this.config_.streaming.retryParameters);
  5348. const vvtText = this.convertToWebVTT_(data, mimeType, adCuePoints);
  5349. const blob = new Blob([vvtText], {type: 'text/vtt'});
  5350. uri = shaka.media.MediaSourceEngine.createObjectURL(blob);
  5351. mimeType = 'text/vtt';
  5352. }
  5353. const trackElement =
  5354. /** @type {!HTMLTrackElement} */(document.createElement('track'));
  5355. trackElement.src = this.cmcdManager_.appendTextTrackData(uri);
  5356. trackElement.label = label;
  5357. trackElement.kind = kind;
  5358. trackElement.srclang = language;
  5359. // Because we're pulling in the text track file via Javascript, the
  5360. // same-origin policy applies. If you'd like to have a player served
  5361. // from one domain, but the text track served from another, you'll
  5362. // need to enable CORS in order to do so. In addition to enabling CORS
  5363. // on the server serving the text tracks, you will need to add the
  5364. // crossorigin attribute to the video element itself.
  5365. if (!this.video_.getAttribute('crossorigin')) {
  5366. this.video_.setAttribute('crossorigin', 'anonymous');
  5367. }
  5368. this.video_.appendChild(trackElement);
  5369. return trackElement;
  5370. }
  5371. /**
  5372. * @param {string} uri
  5373. * @param {!shaka.net.NetworkingEngine} netEngine
  5374. * @param {shaka.extern.RetryParameters} retryParams
  5375. * @return {!Promise.<BufferSource>}
  5376. * @private
  5377. */
  5378. async getTextData_(uri, netEngine, retryParams) {
  5379. const type = shaka.net.NetworkingEngine.RequestType.SEGMENT;
  5380. const request = shaka.net.NetworkingEngine.makeRequest([uri], retryParams);
  5381. request.method = 'GET';
  5382. this.cmcdManager_.applyTextData(request);
  5383. const response = await netEngine.request(type, request).promise;
  5384. return response.data;
  5385. }
  5386. /**
  5387. * Converts an input string to a WebVTT format string.
  5388. *
  5389. * @param {BufferSource} buffer
  5390. * @param {string} mimeType
  5391. * @param {!Array.<!shaka.extern.AdCuePoint>} adCuePoints
  5392. * @return {string}
  5393. * @private
  5394. */
  5395. convertToWebVTT_(buffer, mimeType, adCuePoints) {
  5396. const factory = shaka.text.TextEngine.findParser(mimeType);
  5397. if (factory) {
  5398. const obj = factory();
  5399. const time = {
  5400. periodStart: 0,
  5401. segmentStart: 0,
  5402. segmentEnd: this.video_.duration,
  5403. vttOffset: 0,
  5404. };
  5405. const data = shaka.util.BufferUtils.toUint8(buffer);
  5406. const cues = obj.parseMedia(data, time, /* uri= */ null);
  5407. return shaka.text.WebVttGenerator.convert(cues, adCuePoints);
  5408. }
  5409. throw new shaka.util.Error(
  5410. shaka.util.Error.Severity.CRITICAL,
  5411. shaka.util.Error.Category.TEXT,
  5412. shaka.util.Error.Code.MISSING_TEXT_PLUGIN,
  5413. mimeType);
  5414. }
  5415. /**
  5416. * Set the maximum resolution that the platform's hardware can handle.
  5417. *
  5418. * @param {number} width
  5419. * @param {number} height
  5420. * @export
  5421. */
  5422. setMaxHardwareResolution(width, height) {
  5423. this.maxHwRes_.width = width;
  5424. this.maxHwRes_.height = height;
  5425. }
  5426. /**
  5427. * Retry streaming after a streaming failure has occurred. When the player has
  5428. * not loaded content or is loading content, this will be a no-op and will
  5429. * return <code>false</code>.
  5430. *
  5431. * <p>
  5432. * If the player has loaded content, and streaming has not seen an error, this
  5433. * will return <code>false</code>.
  5434. *
  5435. * <p>
  5436. * If the player has loaded content, and streaming seen an error, but the
  5437. * could not resume streaming, this will return <code>false</code>.
  5438. *
  5439. * @param {number=} retryDelaySeconds
  5440. * @return {boolean}
  5441. * @export
  5442. */
  5443. retryStreaming(retryDelaySeconds = 0.1) {
  5444. return this.loadMode_ == shaka.Player.LoadMode.MEDIA_SOURCE ?
  5445. this.streamingEngine_.retry(retryDelaySeconds) :
  5446. false;
  5447. }
  5448. /**
  5449. * Get the manifest that the player has loaded. If the player has not loaded
  5450. * any content, this will return <code>null</code>.
  5451. *
  5452. * NOTE: This structure is NOT covered by semantic versioning compatibility
  5453. * guarantees. It may change at any time!
  5454. *
  5455. * This is marked as deprecated to warn Closure Compiler users at compile-time
  5456. * to avoid using this method.
  5457. *
  5458. * @return {?shaka.extern.Manifest}
  5459. * @export
  5460. * @deprecated
  5461. */
  5462. getManifest() {
  5463. shaka.log.alwaysWarn(
  5464. 'Shaka Player\'s internal Manifest structure is NOT covered by ' +
  5465. 'semantic versioning compatibility guarantees. It may change at any ' +
  5466. 'time! Please consider filing a feature request for whatever you ' +
  5467. 'use getManifest() for.');
  5468. return this.manifest_;
  5469. }
  5470. /**
  5471. * Get the type of manifest parser that the player is using. If the player has
  5472. * not loaded any content, this will return <code>null</code>.
  5473. *
  5474. * @return {?shaka.extern.ManifestParser.Factory}
  5475. * @export
  5476. */
  5477. getManifestParserFactory() {
  5478. return this.parserFactory_;
  5479. }
  5480. /**
  5481. * @param {shaka.extern.Variant} variant
  5482. * @param {boolean} fromAdaptation
  5483. * @private
  5484. */
  5485. addVariantToSwitchHistory_(variant, fromAdaptation) {
  5486. const switchHistory = this.stats_.getSwitchHistory();
  5487. switchHistory.updateCurrentVariant(variant, fromAdaptation);
  5488. }
  5489. /**
  5490. * @param {shaka.extern.Stream} textStream
  5491. * @param {boolean} fromAdaptation
  5492. * @private
  5493. */
  5494. addTextStreamToSwitchHistory_(textStream, fromAdaptation) {
  5495. const switchHistory = this.stats_.getSwitchHistory();
  5496. switchHistory.updateCurrentText(textStream, fromAdaptation);
  5497. }
  5498. /**
  5499. * @return {shaka.extern.PlayerConfiguration}
  5500. * @private
  5501. */
  5502. defaultConfig_() {
  5503. const config = shaka.util.PlayerConfiguration.createDefault();
  5504. config.streaming.failureCallback = (error) => {
  5505. this.defaultStreamingFailureCallback_(error);
  5506. };
  5507. // Because this.video_ may not be set when the config is built, the default
  5508. // TextDisplay factory must capture a reference to "this".
  5509. config.textDisplayFactory = () => {
  5510. if (this.videoContainer_) {
  5511. const latestConfig = this.getConfiguration();
  5512. return new shaka.text.UITextDisplayer(
  5513. this.video_, this.videoContainer_, latestConfig.textDisplayer);
  5514. } else {
  5515. // eslint-disable-next-line no-restricted-syntax
  5516. if (HTMLMediaElement.prototype.addTextTrack) {
  5517. return new shaka.text.SimpleTextDisplayer(
  5518. this.video_, shaka.Player.TextTrackLabel);
  5519. } else {
  5520. shaka.log.warning('Text tracks are not supported by the ' +
  5521. 'browser, disabling.');
  5522. return new shaka.text.StubTextDisplayer();
  5523. }
  5524. }
  5525. };
  5526. return config;
  5527. }
  5528. /**
  5529. * Set the videoContainer to construct UITextDisplayer.
  5530. * @param {HTMLElement} videoContainer
  5531. * @export
  5532. */
  5533. setVideoContainer(videoContainer) {
  5534. this.videoContainer_ = videoContainer;
  5535. }
  5536. /**
  5537. * @param {!shaka.util.Error} error
  5538. * @private
  5539. */
  5540. defaultStreamingFailureCallback_(error) {
  5541. // For live streams, we retry streaming automatically for certain errors.
  5542. // For VOD streams, all streaming failures are fatal.
  5543. if (!this.isLive()) {
  5544. return;
  5545. }
  5546. let retryDelaySeconds = null;
  5547. if (error.code == shaka.util.Error.Code.BAD_HTTP_STATUS ||
  5548. error.code == shaka.util.Error.Code.HTTP_ERROR) {
  5549. // These errors can be near-instant, so delay a bit before retrying.
  5550. retryDelaySeconds = 1;
  5551. if (this.config_.streaming.lowLatencyMode) {
  5552. retryDelaySeconds = 0.1;
  5553. }
  5554. } else if (error.code == shaka.util.Error.Code.TIMEOUT) {
  5555. // We already waited for a timeout, so retry quickly.
  5556. retryDelaySeconds = 0.1;
  5557. }
  5558. if (retryDelaySeconds != null) {
  5559. error.severity = shaka.util.Error.Severity.RECOVERABLE;
  5560. shaka.log.warning('Live streaming error. Retrying automatically...');
  5561. this.retryStreaming(retryDelaySeconds);
  5562. }
  5563. }
  5564. /**
  5565. * For CEA closed captions embedded in the video streams, create dummy text
  5566. * stream. This can be safely called again on existing manifests, for
  5567. * manifest updates.
  5568. * @param {!shaka.extern.Manifest} manifest
  5569. * @private
  5570. */
  5571. makeTextStreamsForClosedCaptions_(manifest) {
  5572. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  5573. const TextStreamKind = shaka.util.ManifestParserUtils.TextStreamKind;
  5574. const CEA608_MIME = shaka.util.MimeUtils.CEA608_CLOSED_CAPTION_MIMETYPE;
  5575. const CEA708_MIME = shaka.util.MimeUtils.CEA708_CLOSED_CAPTION_MIMETYPE;
  5576. // A set, to make sure we don't create two text streams for the same video.
  5577. const closedCaptionsSet = new Set();
  5578. for (const textStream of manifest.textStreams) {
  5579. if (textStream.mimeType == CEA608_MIME ||
  5580. textStream.mimeType == CEA708_MIME) {
  5581. // This function might be called on a manifest update, so don't make a
  5582. // new text stream for closed caption streams we have seen before.
  5583. closedCaptionsSet.add(textStream.originalId);
  5584. }
  5585. }
  5586. for (const variant of manifest.variants) {
  5587. const video = variant.video;
  5588. if (video && video.closedCaptions) {
  5589. for (const id of video.closedCaptions.keys()) {
  5590. if (!closedCaptionsSet.has(id)) {
  5591. const mimeType = id.startsWith('CC') ? CEA608_MIME : CEA708_MIME;
  5592. // Add an empty segmentIndex, for the benefit of the period combiner
  5593. // in our builtin DASH parser.
  5594. const segmentIndex = new shaka.media.MetaSegmentIndex();
  5595. const language = video.closedCaptions.get(id);
  5596. const textStream = {
  5597. id: this.nextExternalStreamId_++, // A globally unique ID.
  5598. originalId: id, // The CC ID string, like 'CC1', 'CC3', etc.
  5599. groupId: null,
  5600. createSegmentIndex: () => Promise.resolve(),
  5601. segmentIndex,
  5602. mimeType,
  5603. codecs: '',
  5604. kind: TextStreamKind.CLOSED_CAPTION,
  5605. encrypted: false,
  5606. drmInfos: [],
  5607. keyIds: new Set(),
  5608. language,
  5609. originalLanguage: language,
  5610. label: null,
  5611. type: ContentType.TEXT,
  5612. primary: false,
  5613. trickModeVideo: null,
  5614. emsgSchemeIdUris: null,
  5615. roles: video.roles,
  5616. forced: false,
  5617. channelsCount: null,
  5618. audioSamplingRate: null,
  5619. spatialAudio: false,
  5620. closedCaptions: null,
  5621. accessibilityPurpose: null,
  5622. external: false,
  5623. fastSwitching: false,
  5624. fullMimeTypes: new Set([shaka.util.MimeUtils.getFullType(
  5625. mimeType, '')]),
  5626. };
  5627. manifest.textStreams.push(textStream);
  5628. closedCaptionsSet.add(id);
  5629. }
  5630. }
  5631. }
  5632. }
  5633. }
  5634. /**
  5635. * @param {shaka.extern.Variant} initialVariant
  5636. * @param {number} time
  5637. * @return {!Promise.<number>}
  5638. * @private
  5639. */
  5640. async adjustStartTime_(initialVariant, time) {
  5641. /** @type {?shaka.extern.Stream} */
  5642. const activeAudio = initialVariant.audio;
  5643. /** @type {?shaka.extern.Stream} */
  5644. const activeVideo = initialVariant.video;
  5645. /**
  5646. * @param {?shaka.extern.Stream} stream
  5647. * @param {number} time
  5648. * @return {!Promise.<?number>}
  5649. */
  5650. const getAdjustedTime = async (stream, time) => {
  5651. if (!stream) {
  5652. return null;
  5653. }
  5654. await stream.createSegmentIndex();
  5655. const iter = stream.segmentIndex.getIteratorForTime(time);
  5656. const ref = iter ? iter.next().value : null;
  5657. if (!ref) {
  5658. return null;
  5659. }
  5660. const refTime = ref.startTime;
  5661. goog.asserts.assert(refTime <= time,
  5662. 'Segment should start before target time!');
  5663. return refTime;
  5664. };
  5665. const audioStartTime = await getAdjustedTime(activeAudio, time);
  5666. const videoStartTime = await getAdjustedTime(activeVideo, time);
  5667. // If we have both video and audio times, pick the larger one. If we picked
  5668. // the smaller one, that one will download an entire segment to buffer the
  5669. // difference.
  5670. if (videoStartTime != null && audioStartTime != null) {
  5671. return Math.max(videoStartTime, audioStartTime);
  5672. } else if (videoStartTime != null) {
  5673. return videoStartTime;
  5674. } else if (audioStartTime != null) {
  5675. return audioStartTime;
  5676. } else {
  5677. return time;
  5678. }
  5679. }
  5680. /**
  5681. * Update the buffering state to be either "we are buffering" or "we are not
  5682. * buffering", firing events to the app as needed.
  5683. *
  5684. * @private
  5685. */
  5686. updateBufferState_() {
  5687. const isBuffering = this.isBuffering();
  5688. shaka.log.v2('Player changing buffering state to', isBuffering);
  5689. // Make sure we have all the components we need before we consider ourselves
  5690. // as being loaded.
  5691. // TODO: Make the check for "loaded" simpler.
  5692. const loaded = this.stats_ && this.bufferObserver_ && this.playhead_;
  5693. if (loaded) {
  5694. this.playRateController_.setBuffering(isBuffering);
  5695. if (this.cmcdManager_) {
  5696. this.cmcdManager_.setBuffering(isBuffering);
  5697. }
  5698. this.updateStateHistory_();
  5699. const dynamicTargetLatency =
  5700. this.config_.streaming.liveSync.dynamicTargetLatency.enabled;
  5701. const maxAttempts =
  5702. this.config_.streaming.liveSync.dynamicTargetLatency.maxAttempts;
  5703. if (dynamicTargetLatency && isBuffering &&
  5704. this.rebufferingCount_ < maxAttempts) {
  5705. const maxLatency =
  5706. this.config_.streaming.liveSync.dynamicTargetLatency.maxLatency;
  5707. const targetLatencyTolerance =
  5708. this.config_.streaming.liveSync.targetLatencyTolerance;
  5709. const rebufferIncrement =
  5710. this.config_.streaming.liveSync.dynamicTargetLatency
  5711. .rebufferIncrement;
  5712. if (this.currentTargetLatency_) {
  5713. this.currentTargetLatency_ = Math.min(
  5714. this.currentTargetLatency_ +
  5715. ++this.rebufferingCount_ * rebufferIncrement,
  5716. maxLatency - targetLatencyTolerance);
  5717. }
  5718. }
  5719. }
  5720. // Surface the buffering event so that the app knows if/when we are
  5721. // buffering.
  5722. const eventName = shaka.util.FakeEvent.EventName.Buffering;
  5723. const data = (new Map()).set('buffering', isBuffering);
  5724. this.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
  5725. }
  5726. /**
  5727. * A callback for when the playback rate changes. We need to watch the
  5728. * playback rate so that if the playback rate on the media element changes
  5729. * (that was not caused by our play rate controller) we can notify the
  5730. * controller so that it can stay in-sync with the change.
  5731. *
  5732. * @private
  5733. */
  5734. onRateChange_() {
  5735. /** @type {number} */
  5736. const newRate = this.video_.playbackRate;
  5737. // On Edge, when someone seeks using the native controls, it will set the
  5738. // playback rate to zero until they finish seeking, after which it will
  5739. // return the playback rate.
  5740. //
  5741. // If the playback rate changes while seeking, Edge will cache the playback
  5742. // rate and use it after seeking.
  5743. //
  5744. // https://github.com/shaka-project/shaka-player/issues/951
  5745. if (newRate == 0) {
  5746. return;
  5747. }
  5748. if (this.playRateController_) {
  5749. // The playback rate has changed. This could be us or someone else.
  5750. // If this was us, setting the rate again will be a no-op.
  5751. this.playRateController_.set(newRate);
  5752. }
  5753. const event = shaka.Player.makeEvent_(
  5754. shaka.util.FakeEvent.EventName.RateChange);
  5755. this.dispatchEvent(event);
  5756. }
  5757. /**
  5758. * Try updating the state history. If the player has not finished
  5759. * initializing, this will be a no-op.
  5760. *
  5761. * @private
  5762. */
  5763. updateStateHistory_() {
  5764. // If we have not finish initializing, this will be a no-op.
  5765. if (!this.stats_) {
  5766. return;
  5767. }
  5768. if (!this.bufferObserver_) {
  5769. return;
  5770. }
  5771. const State = shaka.media.BufferingObserver.State;
  5772. const history = this.stats_.getStateHistory();
  5773. let updateState = 'playing';
  5774. if (this.bufferObserver_.getState() == State.STARVING) {
  5775. updateState = 'buffering';
  5776. } else if (this.video_.paused) {
  5777. updateState = 'paused';
  5778. } else if (this.video_.ended) {
  5779. updateState = 'ended';
  5780. }
  5781. const stateChanged = history.update(updateState);
  5782. if (stateChanged) {
  5783. const eventName = shaka.util.FakeEvent.EventName.StateChanged;
  5784. const data = (new Map()).set('newstate', updateState);
  5785. this.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
  5786. }
  5787. }
  5788. /**
  5789. * Callback for liveSync and vodDynamicPlaybackRate
  5790. *
  5791. * @private
  5792. */
  5793. onTimeUpdate_() {
  5794. const playbackRate = this.video_.playbackRate;
  5795. const isLive = this.isLive();
  5796. if (this.config_.streaming.vodDynamicPlaybackRate && !isLive) {
  5797. const minPlaybackRate =
  5798. this.config_.streaming.vodDynamicPlaybackRateLowBufferRate;
  5799. const bufferFullness = this.getBufferFullness();
  5800. const bufferThreshold =
  5801. this.config_.streaming.vodDynamicPlaybackRateBufferRatio;
  5802. if (bufferFullness <= bufferThreshold) {
  5803. if (playbackRate != minPlaybackRate) {
  5804. shaka.log.debug('Buffer fullness ratio (' + bufferFullness + ') ' +
  5805. 'is less than the vodDynamicPlaybackRateBufferRatio (' +
  5806. bufferThreshold + '). Updating playbackRate to ' + minPlaybackRate);
  5807. this.trickPlay(minPlaybackRate);
  5808. }
  5809. } else if (bufferFullness == 1) {
  5810. if (playbackRate !== this.playRateController_.getDefaultRate()) {
  5811. shaka.log.debug('Buffer is full. Cancel trick play.');
  5812. this.cancelTrickPlay();
  5813. }
  5814. }
  5815. }
  5816. // If the live stream has reached its end, do not sync.
  5817. if (!isLive) {
  5818. return;
  5819. }
  5820. const seekRange = this.seekRange();
  5821. if (!Number.isFinite(seekRange.end)) {
  5822. return;
  5823. }
  5824. const currentTime = this.video_.currentTime;
  5825. if (currentTime < seekRange.start) {
  5826. // Bad stream?
  5827. return;
  5828. }
  5829. let targetLatency;
  5830. let maxLatency;
  5831. let maxPlaybackRate;
  5832. let minLatency;
  5833. let minPlaybackRate;
  5834. const targetLatencyTolerance =
  5835. this.config_.streaming.liveSync.targetLatencyTolerance;
  5836. const dynamicTargetLatency =
  5837. this.config_.streaming.liveSync.dynamicTargetLatency.enabled;
  5838. const stabilityThreshold =
  5839. this.config_.streaming.liveSync.dynamicTargetLatency.stabilityThreshold;
  5840. if (this.config_.streaming.liveSync &&
  5841. this.config_.streaming.liveSync.enabled) {
  5842. targetLatency = this.config_.streaming.liveSync.targetLatency;
  5843. maxLatency = targetLatency + targetLatencyTolerance;
  5844. minLatency = Math.max(0, targetLatency - targetLatencyTolerance);
  5845. maxPlaybackRate = this.config_.streaming.liveSync.maxPlaybackRate;
  5846. minPlaybackRate = this.config_.streaming.liveSync.minPlaybackRate;
  5847. } else {
  5848. // serviceDescription must override if it is defined in the MPD and
  5849. // liveSync configuration is not set.
  5850. if (this.manifest_ && this.manifest_.serviceDescription) {
  5851. targetLatency = this.manifest_.serviceDescription.targetLatency;
  5852. if (this.manifest_.serviceDescription.targetLatency != null) {
  5853. maxLatency = this.manifest_.serviceDescription.targetLatency +
  5854. targetLatencyTolerance;
  5855. } else if (this.manifest_.serviceDescription.maxLatency != null) {
  5856. maxLatency = this.manifest_.serviceDescription.maxLatency;
  5857. }
  5858. if (this.manifest_.serviceDescription.targetLatency != null) {
  5859. minLatency = Math.max(0,
  5860. this.manifest_.serviceDescription.targetLatency -
  5861. targetLatencyTolerance);
  5862. } else if (this.manifest_.serviceDescription.minLatency != null) {
  5863. minLatency = this.manifest_.serviceDescription.minLatency;
  5864. }
  5865. maxPlaybackRate =
  5866. this.manifest_.serviceDescription.maxPlaybackRate ||
  5867. this.config_.streaming.liveSync.maxPlaybackRate;
  5868. minPlaybackRate =
  5869. this.manifest_.serviceDescription.minPlaybackRate ||
  5870. this.config_.streaming.liveSync.minPlaybackRate;
  5871. }
  5872. }
  5873. if (!this.currentTargetLatency_ && typeof targetLatency === 'number') {
  5874. this.currentTargetLatency_ = targetLatency;
  5875. }
  5876. const maxAttempts =
  5877. this.config_.streaming.liveSync.dynamicTargetLatency.maxAttempts;
  5878. if (dynamicTargetLatency && this.targetLatencyReached_ &&
  5879. this.currentTargetLatency_ !== null &&
  5880. typeof targetLatency === 'number' &&
  5881. this.rebufferingCount_ < maxAttempts &&
  5882. (Date.now() - this.targetLatencyReached_) > stabilityThreshold * 1000) {
  5883. const dynamicMinLatency =
  5884. this.config_.streaming.liveSync.dynamicTargetLatency.minLatency;
  5885. const latencyIncrement = (targetLatency - dynamicMinLatency) / 2;
  5886. this.currentTargetLatency_ = Math.max(
  5887. this.currentTargetLatency_ - latencyIncrement,
  5888. // current target latency should be within the tolerance of the min
  5889. // latency to not overshoot it
  5890. dynamicMinLatency + targetLatencyTolerance);
  5891. this.targetLatencyReached_ = Date.now();
  5892. }
  5893. if (dynamicTargetLatency && this.currentTargetLatency_ !== null) {
  5894. maxLatency = this.currentTargetLatency_ + targetLatencyTolerance;
  5895. minLatency = this.currentTargetLatency_ - targetLatencyTolerance;
  5896. }
  5897. const latency = seekRange.end - this.video_.currentTime;
  5898. let offset = 0;
  5899. // In src= mode, the seek range isn't updated frequently enough, so we need
  5900. // to fudge the latency number with an offset. The playback rate is used
  5901. // as an offset, since that is the amount we catch up 1 second of
  5902. // accelerated playback.
  5903. if (this.loadMode_ == shaka.Player.LoadMode.SRC_EQUALS) {
  5904. const buffered = this.video_.buffered;
  5905. if (buffered.length > 0) {
  5906. const bufferedEnd = buffered.end(buffered.length - 1);
  5907. offset = Math.max(maxPlaybackRate, bufferedEnd - seekRange.end);
  5908. }
  5909. }
  5910. const panicMode = this.config_.streaming.liveSync.panicMode;
  5911. const panicThreshold =
  5912. this.config_.streaming.liveSync.panicThreshold * 1000;
  5913. const timeSinceLastRebuffer =
  5914. Date.now() - this.bufferObserver_.getLastRebufferTime();
  5915. if (panicMode && !minPlaybackRate) {
  5916. minPlaybackRate = this.config_.streaming.liveSync.minPlaybackRate;
  5917. }
  5918. if (panicMode && minPlaybackRate &&
  5919. timeSinceLastRebuffer <= panicThreshold) {
  5920. if (playbackRate != minPlaybackRate) {
  5921. shaka.log.debug('Time since last rebuffer (' +
  5922. timeSinceLastRebuffer + 's) ' +
  5923. 'is less than the live sync panicThreshold (' + panicThreshold +
  5924. 's). Updating playbackRate to ' + minPlaybackRate);
  5925. this.trickPlay(minPlaybackRate);
  5926. }
  5927. } else if (maxLatency && maxPlaybackRate &&
  5928. (latency - offset) > maxLatency) {
  5929. if (playbackRate != maxPlaybackRate) {
  5930. shaka.log.debug('Latency (' + latency + 's) is greater than ' +
  5931. 'live sync maxLatency (' + maxLatency + 's). ' +
  5932. 'Updating playbackRate to ' + maxPlaybackRate);
  5933. this.trickPlay(maxPlaybackRate);
  5934. }
  5935. this.targetLatencyReached_ = null;
  5936. } else if (minLatency && minPlaybackRate &&
  5937. (latency - offset) < minLatency) {
  5938. if (playbackRate != minPlaybackRate) {
  5939. shaka.log.debug('Latency (' + latency + 's) is smaller than ' +
  5940. 'live sync minLatency (' + minLatency + 's). ' +
  5941. 'Updating playbackRate to ' + minPlaybackRate);
  5942. this.trickPlay(minPlaybackRate);
  5943. }
  5944. this.targetLatencyReached_ = null;
  5945. } else if (playbackRate !== this.playRateController_.getDefaultRate()) {
  5946. this.cancelTrickPlay();
  5947. this.targetLatencyReached_ = Date.now();
  5948. }
  5949. }
  5950. /**
  5951. * Callback for video progress events
  5952. *
  5953. * @private
  5954. */
  5955. onVideoProgress_() {
  5956. if (!this.video_) {
  5957. return;
  5958. }
  5959. let hasNewCompletionPercent = false;
  5960. const completionRatio = this.video_.currentTime / this.video_.duration;
  5961. if (!isNaN(completionRatio)) {
  5962. const percent = Math.round(100 * completionRatio);
  5963. if (isNaN(this.completionPercent_)) {
  5964. this.completionPercent_ = percent;
  5965. hasNewCompletionPercent = true;
  5966. } else {
  5967. const newCompletionPercent = Math.max(this.completionPercent_, percent);
  5968. if (this.completionPercent_ != newCompletionPercent) {
  5969. this.completionPercent_ = newCompletionPercent;
  5970. hasNewCompletionPercent = true;
  5971. }
  5972. }
  5973. }
  5974. if (hasNewCompletionPercent) {
  5975. let event;
  5976. if (this.completionPercent_ == 0) {
  5977. event = shaka.Player.makeEvent_(shaka.util.FakeEvent.EventName.Started);
  5978. } else if (this.completionPercent_ == 25) {
  5979. event = shaka.Player.makeEvent_(
  5980. shaka.util.FakeEvent.EventName.FirstQuartile);
  5981. } else if (this.completionPercent_ == 50) {
  5982. event = shaka.Player.makeEvent_(
  5983. shaka.util.FakeEvent.EventName.Midpoint);
  5984. } else if (this.completionPercent_ == 75) {
  5985. event = shaka.Player.makeEvent_(
  5986. shaka.util.FakeEvent.EventName.ThirdQuartile);
  5987. } else if (this.completionPercent_ == 100) {
  5988. event = shaka.Player.makeEvent_(
  5989. shaka.util.FakeEvent.EventName.Complete);
  5990. }
  5991. if (event) {
  5992. this.dispatchEvent(event);
  5993. }
  5994. }
  5995. }
  5996. /**
  5997. * Callback from Playhead.
  5998. *
  5999. * @private
  6000. */
  6001. onSeek_() {
  6002. if (this.playheadObservers_) {
  6003. this.playheadObservers_.notifyOfSeek();
  6004. }
  6005. if (this.streamingEngine_) {
  6006. this.streamingEngine_.seeked();
  6007. }
  6008. if (this.bufferObserver_) {
  6009. // If we seek into an unbuffered range, we should fire a 'buffering' event
  6010. // immediately. If StreamingEngine can buffer fast enough, we may not
  6011. // update our buffering tracking otherwise.
  6012. this.pollBufferState_();
  6013. }
  6014. }
  6015. /**
  6016. * Update AbrManager with variants while taking into account restrictions,
  6017. * preferences, and ABR.
  6018. *
  6019. * On error, this dispatches an error event and returns false.
  6020. *
  6021. * @return {boolean} True if successful.
  6022. * @private
  6023. */
  6024. updateAbrManagerVariants_() {
  6025. try {
  6026. goog.asserts.assert(this.manifest_, 'Manifest should exist by now!');
  6027. this.manifestFilterer_.checkRestrictedVariants(this.manifest_);
  6028. } catch (e) {
  6029. this.onError_(e);
  6030. return false;
  6031. }
  6032. const playableVariants = shaka.util.StreamUtils.getPlayableVariants(
  6033. this.manifest_.variants);
  6034. // Update the abr manager with newly filtered variants.
  6035. const adaptationSet = this.currentAdaptationSetCriteria_.create(
  6036. playableVariants);
  6037. this.abrManager_.setVariants(Array.from(adaptationSet.values()));
  6038. return true;
  6039. }
  6040. /**
  6041. * Chooses a variant from all possible variants while taking into account
  6042. * restrictions, preferences, and ABR.
  6043. *
  6044. * On error, this dispatches an error event and returns null.
  6045. *
  6046. * @param {boolean=} initialSelection
  6047. * @return {?shaka.extern.Variant}
  6048. * @private
  6049. */
  6050. chooseVariant_(initialSelection = false) {
  6051. if (this.updateAbrManagerVariants_()) {
  6052. return this.abrManager_.chooseVariant(initialSelection);
  6053. } else {
  6054. return null;
  6055. }
  6056. }
  6057. /**
  6058. * Checks to re-enable variants that were temporarily disabled due to network
  6059. * errors. If any variants are enabled this way, a new variant may be chosen
  6060. * for playback.
  6061. * @private
  6062. */
  6063. checkVariants_() {
  6064. goog.asserts.assert(this.manifest_, 'Should have manifest!');
  6065. const now = Date.now() / 1000;
  6066. let hasVariantUpdate = false;
  6067. /** @type {function(shaka.extern.Variant):string} */
  6068. const streamsAsString = (variant) => {
  6069. let str = '';
  6070. if (variant.video) {
  6071. str += 'video:' + variant.video.id;
  6072. }
  6073. if (variant.audio) {
  6074. str += str ? '&' : '';
  6075. str += 'audio:' + variant.audio.id;
  6076. }
  6077. return str;
  6078. };
  6079. let shouldStopTimer = true;
  6080. for (const variant of this.manifest_.variants) {
  6081. if (variant.disabledUntilTime > 0 && variant.disabledUntilTime <= now) {
  6082. variant.disabledUntilTime = 0;
  6083. hasVariantUpdate = true;
  6084. shaka.log.v2('Re-enabled variant with ' + streamsAsString(variant));
  6085. }
  6086. if (variant.disabledUntilTime > 0) {
  6087. shouldStopTimer = false;
  6088. }
  6089. }
  6090. if (shouldStopTimer) {
  6091. this.checkVariantsTimer_.stop();
  6092. }
  6093. if (hasVariantUpdate) {
  6094. // Reconsider re-enabled variant for ABR switching.
  6095. this.chooseVariantAndSwitch_(
  6096. /* clearBuffer= */ false, /* safeMargin= */ undefined,
  6097. /* force= */ false, /* fromAdaptation= */ false);
  6098. }
  6099. }
  6100. /**
  6101. * Choose a text stream from all possible text streams while taking into
  6102. * account user preference.
  6103. *
  6104. * @return {?shaka.extern.Stream}
  6105. * @private
  6106. */
  6107. chooseTextStream_() {
  6108. const subset = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
  6109. this.manifest_.textStreams,
  6110. this.currentTextLanguage_,
  6111. this.currentTextRole_,
  6112. this.currentTextForced_);
  6113. return subset[0] || null;
  6114. }
  6115. /**
  6116. * Chooses a new Variant. If the new variant differs from the old one, it
  6117. * adds the new one to the switch history and switches to it.
  6118. *
  6119. * Called after a config change, a key status event, or an explicit language
  6120. * change.
  6121. *
  6122. * @param {boolean=} clearBuffer Optional clear buffer or not when
  6123. * switch to new variant
  6124. * Defaults to true if not provided
  6125. * @param {number=} safeMargin Optional amount of buffer (in seconds) to
  6126. * retain when clearing the buffer.
  6127. * Defaults to 0 if not provided. Ignored if clearBuffer is false.
  6128. * @private
  6129. */
  6130. chooseVariantAndSwitch_(clearBuffer = true, safeMargin = 0, force = false,
  6131. fromAdaptation = true) {
  6132. goog.asserts.assert(this.config_, 'Must not be destroyed');
  6133. // Because we're running this after a config change (manual language
  6134. // change) or a key status event, it is always okay to clear the buffer
  6135. // here.
  6136. const chosenVariant = this.chooseVariant_();
  6137. if (chosenVariant) {
  6138. this.switchVariant_(chosenVariant, fromAdaptation,
  6139. clearBuffer, safeMargin, force);
  6140. }
  6141. }
  6142. /**
  6143. * @param {shaka.extern.Variant} variant
  6144. * @param {boolean} fromAdaptation
  6145. * @param {boolean} clearBuffer
  6146. * @param {number} safeMargin
  6147. * @param {boolean=} force
  6148. * @private
  6149. */
  6150. switchVariant_(variant, fromAdaptation, clearBuffer, safeMargin,
  6151. force = false) {
  6152. const currentVariant = this.streamingEngine_.getCurrentVariant();
  6153. if (variant == currentVariant) {
  6154. shaka.log.debug('Variant already selected.');
  6155. // If you want to clear the buffer, we force to reselect the same variant.
  6156. // We don't need to reset the timestampOffset since it's the same variant,
  6157. // so 'adaptation' isn't passed here.
  6158. if (clearBuffer) {
  6159. this.streamingEngine_.switchVariant(variant, clearBuffer, safeMargin,
  6160. /* force= */ true);
  6161. }
  6162. return;
  6163. }
  6164. // Add entries to the history.
  6165. this.addVariantToSwitchHistory_(variant, fromAdaptation);
  6166. this.streamingEngine_.switchVariant(
  6167. variant, clearBuffer, safeMargin, force,
  6168. /* adaptation= */ fromAdaptation);
  6169. let oldTrack = null;
  6170. if (currentVariant) {
  6171. oldTrack = shaka.util.StreamUtils.variantToTrack(currentVariant);
  6172. }
  6173. const newTrack = shaka.util.StreamUtils.variantToTrack(variant);
  6174. if (fromAdaptation) {
  6175. // Dispatch an 'adaptation' event
  6176. this.onAdaptation_(oldTrack, newTrack);
  6177. } else {
  6178. // Dispatch a 'variantchanged' event
  6179. this.onVariantChanged_(oldTrack, newTrack);
  6180. }
  6181. }
  6182. /**
  6183. * @param {AudioTrack} track
  6184. * @private
  6185. */
  6186. switchHtml5Track_(track) {
  6187. goog.asserts.assert(this.video_ && this.video_.audioTracks,
  6188. 'Video and video.audioTracks should not be null!');
  6189. const audioTracks = Array.from(this.video_.audioTracks);
  6190. const currentTrack = audioTracks.find((t) => t.enabled);
  6191. // This will reset the "enabled" of other tracks to false.
  6192. track.enabled = true;
  6193. if (!currentTrack) {
  6194. return;
  6195. }
  6196. // AirPlay does not reset the "enabled" of other tracks to false, so
  6197. // it must be changed by hand.
  6198. if (track.id !== currentTrack.id) {
  6199. currentTrack.enabled = false;
  6200. }
  6201. const oldTrack =
  6202. shaka.util.StreamUtils.html5AudioTrackToTrack(currentTrack);
  6203. const newTrack =
  6204. shaka.util.StreamUtils.html5AudioTrackToTrack(track);
  6205. this.onVariantChanged_(oldTrack, newTrack);
  6206. }
  6207. /**
  6208. * Decide during startup if text should be streamed/shown.
  6209. * @private
  6210. */
  6211. setInitialTextState_(initialVariant, initialTextStream) {
  6212. // Check if we should show text (based on difference between audio and text
  6213. // languages).
  6214. if (initialTextStream) {
  6215. if (initialVariant.audio && this.shouldInitiallyShowText_(
  6216. initialVariant.audio, initialTextStream)) {
  6217. this.isTextVisible_ = true;
  6218. }
  6219. if (this.isTextVisible_) {
  6220. // If the cached value says to show text, then update the text displayer
  6221. // since it defaults to not shown.
  6222. this.mediaSourceEngine_.getTextDisplayer().setTextVisibility(true);
  6223. goog.asserts.assert(this.shouldStreamText_(),
  6224. 'Should be streaming text');
  6225. }
  6226. this.onTextTrackVisibility_();
  6227. } else {
  6228. this.isTextVisible_ = false;
  6229. }
  6230. }
  6231. /**
  6232. * Check if we should show text on screen automatically.
  6233. *
  6234. * @param {shaka.extern.Stream} audioStream
  6235. * @param {shaka.extern.Stream} textStream
  6236. * @return {boolean}
  6237. * @private
  6238. */
  6239. shouldInitiallyShowText_(audioStream, textStream) {
  6240. const AutoShowText = shaka.config.AutoShowText;
  6241. if (this.config_.autoShowText == AutoShowText.NEVER) {
  6242. return false;
  6243. }
  6244. if (this.config_.autoShowText == AutoShowText.ALWAYS) {
  6245. return true;
  6246. }
  6247. const LanguageUtils = shaka.util.LanguageUtils;
  6248. /** @type {string} */
  6249. const preferredTextLocale =
  6250. LanguageUtils.normalize(this.config_.preferredTextLanguage);
  6251. /** @type {string} */
  6252. const textLocale = LanguageUtils.normalize(textStream.language);
  6253. if (this.config_.autoShowText == AutoShowText.IF_PREFERRED_TEXT_LANGUAGE) {
  6254. // Only the text language match matters.
  6255. return LanguageUtils.areLanguageCompatible(
  6256. textLocale,
  6257. preferredTextLocale);
  6258. }
  6259. if (this.config_.autoShowText == AutoShowText.IF_SUBTITLES_MAY_BE_NEEDED) {
  6260. /* The text should automatically be shown if the text is
  6261. * language-compatible with the user's text language preference, but not
  6262. * compatible with the audio. These are cases where we deduce that
  6263. * subtitles may be needed.
  6264. *
  6265. * For example:
  6266. * preferred | chosen | chosen |
  6267. * text | text | audio | show
  6268. * -----------------------------------
  6269. * en-CA | en | jp | true
  6270. * en | en-US | fr | true
  6271. * fr-CA | en-US | jp | false
  6272. * en-CA | en-US | en-US | false
  6273. *
  6274. */
  6275. /** @type {string} */
  6276. const audioLocale = LanguageUtils.normalize(audioStream.language);
  6277. return (
  6278. LanguageUtils.areLanguageCompatible(textLocale, preferredTextLocale) &&
  6279. !LanguageUtils.areLanguageCompatible(audioLocale, textLocale));
  6280. }
  6281. shaka.log.alwaysWarn('Invalid autoShowText setting!');
  6282. return false;
  6283. }
  6284. /**
  6285. * Callback from StreamingEngine.
  6286. *
  6287. * @private
  6288. */
  6289. onManifestUpdate_() {
  6290. if (this.parser_ && this.parser_.update) {
  6291. this.parser_.update();
  6292. }
  6293. }
  6294. /**
  6295. * Callback from StreamingEngine.
  6296. *
  6297. * @param {number} start
  6298. * @param {number} end
  6299. * @param {shaka.util.ManifestParserUtils.ContentType} contentType
  6300. * @param {boolean} isMuxed
  6301. *
  6302. * @private
  6303. */
  6304. onSegmentAppended_(start, end, contentType, isMuxed) {
  6305. // When we append a segment to media source (via streaming engine) we are
  6306. // changing what data we have buffered, so notify the playhead of the
  6307. // change.
  6308. if (this.playhead_) {
  6309. this.playhead_.notifyOfBufferingChange();
  6310. // Skip the initial buffer gap
  6311. const startTime = this.mediaSourceEngine_.bufferStart(contentType);
  6312. if (
  6313. !this.isLive() &&
  6314. // If not paused then GapJumpingController will handle this gap.
  6315. this.video_.paused &&
  6316. startTime != null &&
  6317. startTime > 0 &&
  6318. this.playhead_.getTime() < startTime
  6319. ) {
  6320. this.playhead_.setStartTime(startTime);
  6321. }
  6322. }
  6323. this.pollBufferState_();
  6324. // Dispatch an event for users to consume, too.
  6325. const data = new Map()
  6326. .set('start', start)
  6327. .set('end', end)
  6328. .set('contentType', contentType)
  6329. .set('isMuxed', isMuxed);
  6330. this.dispatchEvent(shaka.Player.makeEvent_(
  6331. shaka.util.FakeEvent.EventName.SegmentAppended, data));
  6332. }
  6333. /**
  6334. * Callback from AbrManager.
  6335. *
  6336. * @param {shaka.extern.Variant} variant
  6337. * @param {boolean=} clearBuffer
  6338. * @param {number=} safeMargin Optional amount of buffer (in seconds) to
  6339. * retain when clearing the buffer.
  6340. * Defaults to 0 if not provided. Ignored if clearBuffer is false.
  6341. * @private
  6342. */
  6343. switch_(variant, clearBuffer = false, safeMargin = 0) {
  6344. shaka.log.debug('switch_');
  6345. goog.asserts.assert(this.config_.abr.enabled,
  6346. 'AbrManager should not call switch while disabled!');
  6347. if (!this.manifest_) {
  6348. // It could come from a preload manager operation.
  6349. return;
  6350. }
  6351. if (!this.streamingEngine_) {
  6352. // There's no way to change it.
  6353. return;
  6354. }
  6355. if (variant == this.streamingEngine_.getCurrentVariant()) {
  6356. // This isn't a change.
  6357. return;
  6358. }
  6359. this.switchVariant_(variant, /* fromAdaptation= */ true,
  6360. clearBuffer, safeMargin);
  6361. }
  6362. /**
  6363. * Dispatches an 'adaptation' event.
  6364. * @param {?shaka.extern.Track} from
  6365. * @param {shaka.extern.Track} to
  6366. * @private
  6367. */
  6368. onAdaptation_(from, to) {
  6369. // Delay the 'adaptation' event so that StreamingEngine has time to absorb
  6370. // the changes before the user tries to query it.
  6371. const data = new Map()
  6372. .set('oldTrack', from)
  6373. .set('newTrack', to);
  6374. if (this.lcevcDec_) {
  6375. this.lcevcDec_.updateVariant(to, this.getManifestType());
  6376. }
  6377. const event = shaka.Player.makeEvent_(
  6378. shaka.util.FakeEvent.EventName.Adaptation, data);
  6379. this.delayDispatchEvent_(event);
  6380. }
  6381. /**
  6382. * Dispatches a 'trackschanged' event.
  6383. * @private
  6384. */
  6385. onTracksChanged_() {
  6386. // Delay the 'trackschanged' event so StreamingEngine has time to absorb the
  6387. // changes before the user tries to query it.
  6388. const event = shaka.Player.makeEvent_(
  6389. shaka.util.FakeEvent.EventName.TracksChanged);
  6390. this.delayDispatchEvent_(event);
  6391. }
  6392. /**
  6393. * Dispatches a 'variantchanged' event.
  6394. * @param {?shaka.extern.Track} from
  6395. * @param {shaka.extern.Track} to
  6396. * @private
  6397. */
  6398. onVariantChanged_(from, to) {
  6399. // Delay the 'variantchanged' event so StreamingEngine has time to absorb
  6400. // the changes before the user tries to query it.
  6401. const data = new Map()
  6402. .set('oldTrack', from)
  6403. .set('newTrack', to);
  6404. if (this.lcevcDec_) {
  6405. this.lcevcDec_.updateVariant(to, this.getManifestType());
  6406. }
  6407. const event = shaka.Player.makeEvent_(
  6408. shaka.util.FakeEvent.EventName.VariantChanged, data);
  6409. this.delayDispatchEvent_(event);
  6410. }
  6411. /**
  6412. * Dispatches a 'textchanged' event.
  6413. * @private
  6414. */
  6415. onTextChanged_() {
  6416. // Delay the 'textchanged' event so StreamingEngine time to absorb the
  6417. // changes before the user tries to query it.
  6418. const event = shaka.Player.makeEvent_(
  6419. shaka.util.FakeEvent.EventName.TextChanged);
  6420. this.delayDispatchEvent_(event);
  6421. }
  6422. /** @private */
  6423. onTextTrackVisibility_() {
  6424. const event = shaka.Player.makeEvent_(
  6425. shaka.util.FakeEvent.EventName.TextTrackVisibility);
  6426. this.delayDispatchEvent_(event);
  6427. }
  6428. /** @private */
  6429. onAbrStatusChanged_() {
  6430. // Restore disabled variants if abr get disabled
  6431. if (!this.config_.abr.enabled) {
  6432. this.restoreDisabledVariants_();
  6433. }
  6434. const data = (new Map()).set('newStatus', this.config_.abr.enabled);
  6435. this.delayDispatchEvent_(shaka.Player.makeEvent_(
  6436. shaka.util.FakeEvent.EventName.AbrStatusChanged, data));
  6437. }
  6438. /**
  6439. * @param {boolean} updateAbrManager
  6440. * @private
  6441. */
  6442. restoreDisabledVariants_(updateAbrManager=true) {
  6443. if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
  6444. return;
  6445. }
  6446. goog.asserts.assert(this.manifest_, 'Should have manifest!');
  6447. shaka.log.v2('Restoring all disabled streams...');
  6448. this.checkVariantsTimer_.stop();
  6449. for (const variant of this.manifest_.variants) {
  6450. variant.disabledUntilTime = 0;
  6451. }
  6452. if (updateAbrManager) {
  6453. this.updateAbrManagerVariants_();
  6454. }
  6455. }
  6456. /**
  6457. * Temporarily disable all variants containing |stream|
  6458. * @param {shaka.extern.Stream} stream
  6459. * @param {number} disableTime
  6460. * @return {boolean}
  6461. */
  6462. disableStream(stream, disableTime) {
  6463. if (!this.config_.abr.enabled ||
  6464. this.loadMode_ === shaka.Player.LoadMode.DESTROYED) {
  6465. return false;
  6466. }
  6467. if (!navigator.onLine) {
  6468. // Don't disable variants if we're completely offline, or else we end up
  6469. // rapidly restricting all of them.
  6470. return false;
  6471. }
  6472. // It only makes sense to disable a stream if we have an alternative else we
  6473. // end up disabling all variants.
  6474. const hasAltStream = this.manifest_.variants.some((variant) => {
  6475. const altStream = variant[stream.type];
  6476. if (altStream && altStream.id !== stream.id &&
  6477. !variant.disabledUntilTime) {
  6478. if (shaka.util.StreamUtils.isAudio(stream)) {
  6479. return stream.language === altStream.language;
  6480. }
  6481. return true;
  6482. }
  6483. return false;
  6484. });
  6485. if (hasAltStream) {
  6486. let didDisableStream = false;
  6487. for (const variant of this.manifest_.variants) {
  6488. const candidate = variant[stream.type];
  6489. if (candidate && candidate.id === stream.id) {
  6490. variant.disabledUntilTime = (Date.now() / 1000) + disableTime;
  6491. didDisableStream = true;
  6492. shaka.log.v2(
  6493. 'Disabled stream ' + stream.type + ':' + stream.id +
  6494. ' for ' + disableTime + ' seconds...');
  6495. }
  6496. }
  6497. goog.asserts.assert(didDisableStream, 'Must have disabled stream');
  6498. this.checkVariantsTimer_.tickEvery(1);
  6499. // Get the safeMargin to ensure a seamless playback
  6500. const {video} = this.getBufferedInfo();
  6501. const safeMargin =
  6502. video.reduce((size, {start, end}) => size + end - start, 0);
  6503. // Update abr manager variants and switch to recover playback
  6504. this.chooseVariantAndSwitch_(
  6505. /* clearBuffer= */ false, /* safeMargin= */ safeMargin,
  6506. /* force= */ true, /* fromAdaptation= */ false);
  6507. return true;
  6508. }
  6509. shaka.log.warning(
  6510. 'No alternate stream found for active ' + stream.type + ' stream. ' +
  6511. 'Will ignore request to disable stream...');
  6512. return false;
  6513. }
  6514. /**
  6515. * @param {!shaka.util.Error} error
  6516. * @private
  6517. */
  6518. async onError_(error) {
  6519. goog.asserts.assert(error instanceof shaka.util.Error, 'Wrong error type!');
  6520. // Errors dispatched after |destroy| is called are not meaningful and should
  6521. // be safe to ignore.
  6522. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) {
  6523. return;
  6524. }
  6525. if (error.severity === shaka.util.Error.Severity.RECOVERABLE) {
  6526. this.stats_.addNonFatalError();
  6527. }
  6528. let fireError = true;
  6529. if (this.fullyLoaded_ && this.manifest_ && this.streamingEngine_ &&
  6530. (error.code == shaka.util.Error.Code.VIDEO_ERROR ||
  6531. error.code == shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_FAILED ||
  6532. error.code == shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW ||
  6533. error.code == shaka.util.Error.Code.TRANSMUXING_FAILED)) {
  6534. try {
  6535. const ret = await this.streamingEngine_.resetMediaSource();
  6536. fireError = !ret;
  6537. } catch (e) {
  6538. fireError = true;
  6539. }
  6540. }
  6541. if (!fireError) {
  6542. return;
  6543. }
  6544. // Restore disabled variant if the player experienced a critical error.
  6545. if (error.severity === shaka.util.Error.Severity.CRITICAL) {
  6546. this.restoreDisabledVariants_(/* updateAbrManager= */ false);
  6547. }
  6548. const eventName = shaka.util.FakeEvent.EventName.Error;
  6549. const event = shaka.Player.makeEvent_(
  6550. eventName, (new Map()).set('detail', error));
  6551. this.dispatchEvent(event);
  6552. if (event.defaultPrevented) {
  6553. error.handled = true;
  6554. }
  6555. }
  6556. /**
  6557. * When we fire region events, we need to copy the information out of the
  6558. * region to break the connection with the player's internal data. We do the
  6559. * copy here because this is the transition point between the player and the
  6560. * app.
  6561. *
  6562. * @param {!shaka.util.FakeEvent.EventName} eventName
  6563. * @param {shaka.extern.TimelineRegionInfo} region
  6564. * @param {shaka.util.FakeEventTarget=} eventTarget
  6565. *
  6566. * @private
  6567. */
  6568. onRegionEvent_(eventName, region, eventTarget = this) {
  6569. // Always make a copy to avoid exposing our internal data to the app.
  6570. const clone = {
  6571. schemeIdUri: region.schemeIdUri,
  6572. value: region.value,
  6573. startTime: region.startTime,
  6574. endTime: region.endTime,
  6575. id: region.id,
  6576. eventElement: region.eventElement,
  6577. eventNode: region.eventNode,
  6578. };
  6579. const data = (new Map()).set('detail', clone);
  6580. eventTarget.dispatchEvent(shaka.Player.makeEvent_(eventName, data));
  6581. }
  6582. /**
  6583. * When notified of a media quality change we need to emit a
  6584. * MediaQualityChange event to the app.
  6585. *
  6586. * @param {shaka.extern.MediaQualityInfo} mediaQuality
  6587. * @param {number} position
  6588. * @param {boolean} audioTrackChanged This is to specify whether this should
  6589. * trigger a MediaQualityChangedEvent or an AudioTrackChangedEvent. Defaults
  6590. * to false to trigger MediaQualityChangedEvent.
  6591. *
  6592. * @private
  6593. */
  6594. onMediaQualityChange_(mediaQuality, position, audioTrackChanged = false) {
  6595. // Always make a copy to avoid exposing our internal data to the app.
  6596. const clone = {
  6597. bandwidth: mediaQuality.bandwidth,
  6598. audioSamplingRate: mediaQuality.audioSamplingRate,
  6599. codecs: mediaQuality.codecs,
  6600. contentType: mediaQuality.contentType,
  6601. frameRate: mediaQuality.frameRate,
  6602. height: mediaQuality.height,
  6603. mimeType: mediaQuality.mimeType,
  6604. channelsCount: mediaQuality.channelsCount,
  6605. pixelAspectRatio: mediaQuality.pixelAspectRatio,
  6606. width: mediaQuality.width,
  6607. label: mediaQuality.label,
  6608. roles: mediaQuality.roles,
  6609. language: mediaQuality.language,
  6610. };
  6611. const data = new Map()
  6612. .set('mediaQuality', clone)
  6613. .set('position', position);
  6614. this.dispatchEvent(shaka.Player.makeEvent_(
  6615. audioTrackChanged ?
  6616. shaka.util.FakeEvent.EventName.AudioTrackChanged :
  6617. shaka.util.FakeEvent.EventName.MediaQualityChanged,
  6618. data));
  6619. }
  6620. /**
  6621. * Turn the media element's error object into a Shaka Player error object.
  6622. *
  6623. * @return {shaka.util.Error}
  6624. * @private
  6625. */
  6626. videoErrorToShakaError_() {
  6627. goog.asserts.assert(this.video_.error,
  6628. 'Video error expected, but missing!');
  6629. if (!this.video_.error) {
  6630. return null;
  6631. }
  6632. const code = this.video_.error.code;
  6633. if (code == 1 /* MEDIA_ERR_ABORTED */) {
  6634. // Ignore this error code, which should only occur when navigating away or
  6635. // deliberately stopping playback of HTTP content.
  6636. return null;
  6637. }
  6638. // Extra error information from MS Edge:
  6639. let extended = this.video_.error.msExtendedCode;
  6640. if (extended) {
  6641. // Convert to unsigned:
  6642. if (extended < 0) {
  6643. extended += Math.pow(2, 32);
  6644. }
  6645. // Format as hex:
  6646. extended = extended.toString(16);
  6647. }
  6648. // Extra error information from Chrome:
  6649. const message = this.video_.error.message;
  6650. return new shaka.util.Error(
  6651. shaka.util.Error.Severity.CRITICAL,
  6652. shaka.util.Error.Category.MEDIA,
  6653. shaka.util.Error.Code.VIDEO_ERROR,
  6654. code, extended, message);
  6655. }
  6656. /**
  6657. * @param {!Event} event
  6658. * @private
  6659. */
  6660. onVideoError_(event) {
  6661. const error = this.videoErrorToShakaError_();
  6662. if (!error) {
  6663. return;
  6664. }
  6665. this.onError_(error);
  6666. }
  6667. /**
  6668. * @param {!Object.<string, string>} keyStatusMap A map of hex key IDs to
  6669. * statuses.
  6670. * @private
  6671. */
  6672. onKeyStatus_(keyStatusMap) {
  6673. goog.asserts.assert(this.streamingEngine_, 'Cannot be called in src= mode');
  6674. const event = shaka.Player.makeEvent_(
  6675. shaka.util.FakeEvent.EventName.KeyStatusChanged);
  6676. this.dispatchEvent(event);
  6677. let keyIds = Object.keys(keyStatusMap);
  6678. if (keyIds.length == 0) {
  6679. shaka.log.warning(
  6680. 'Got a key status event without any key statuses, so we don\'t ' +
  6681. 'know the real key statuses. If we don\'t have all the keys, ' +
  6682. 'you\'ll need to set restrictions so we don\'t select those tracks.');
  6683. }
  6684. // Non-standard version of global key status. Modify it to match standard
  6685. // behavior.
  6686. if (keyIds.length == 1 && keyIds[0] == '') {
  6687. keyIds = ['00'];
  6688. keyStatusMap = {'00': keyStatusMap['']};
  6689. }
  6690. // If EME is using a synthetic key ID, the only key ID is '00' (a single 0
  6691. // byte). In this case, it is only used to report global success/failure.
  6692. // See note about old platforms in: https://bit.ly/2tpez5Z
  6693. const isGlobalStatus = keyIds.length == 1 && keyIds[0] == '00';
  6694. if (isGlobalStatus) {
  6695. shaka.log.warning(
  6696. 'Got a synthetic key status event, so we don\'t know the real key ' +
  6697. 'statuses. If we don\'t have all the keys, you\'ll need to set ' +
  6698. 'restrictions so we don\'t select those tracks.');
  6699. }
  6700. const restrictedStatuses = shaka.media.ManifestFilterer.restrictedStatuses;
  6701. let tracksChanged = false;
  6702. goog.asserts.assert(this.drmEngine_, 'drmEngine should be non-null here.');
  6703. // Only filter tracks for keys if we have some key statuses to look at.
  6704. if (keyIds.length) {
  6705. for (const variant of this.manifest_.variants) {
  6706. const streams = shaka.util.StreamUtils.getVariantStreams(variant);
  6707. for (const stream of streams) {
  6708. const originalAllowed = variant.allowedByKeySystem;
  6709. // Only update if we have key IDs for the stream. If the keys aren't
  6710. // all present, then the track should be restricted.
  6711. if (stream.keyIds.size) {
  6712. variant.allowedByKeySystem = true;
  6713. for (const keyId of stream.keyIds) {
  6714. const keyStatus = keyStatusMap[isGlobalStatus ? '00' : keyId];
  6715. if (keyStatus || this.drmEngine_.hasManifestInitData()) {
  6716. variant.allowedByKeySystem = variant.allowedByKeySystem &&
  6717. !!keyStatus && !restrictedStatuses.includes(keyStatus);
  6718. }
  6719. }
  6720. }
  6721. if (originalAllowed != variant.allowedByKeySystem) {
  6722. tracksChanged = true;
  6723. }
  6724. } // for (const stream of streams)
  6725. } // for (const variant of this.manifest_.variants)
  6726. } // if (keyIds.size)
  6727. if (tracksChanged) {
  6728. this.onTracksChanged_();
  6729. const variantsUpdated = this.updateAbrManagerVariants_();
  6730. if (!variantsUpdated) {
  6731. return;
  6732. }
  6733. }
  6734. const currentVariant = this.streamingEngine_.getCurrentVariant();
  6735. if (currentVariant && !currentVariant.allowedByKeySystem) {
  6736. shaka.log.debug('Choosing new streams after key status changed');
  6737. this.chooseVariantAndSwitch_();
  6738. }
  6739. }
  6740. /**
  6741. * @return {boolean} true if we should stream text right now.
  6742. * @private
  6743. */
  6744. shouldStreamText_() {
  6745. return this.config_.streaming.alwaysStreamText || this.isTextTrackVisible();
  6746. }
  6747. /**
  6748. * Applies playRangeStart and playRangeEnd to the given timeline. This will
  6749. * only affect non-live content.
  6750. *
  6751. * @param {shaka.media.PresentationTimeline} timeline
  6752. * @param {number} playRangeStart
  6753. * @param {number} playRangeEnd
  6754. *
  6755. * @private
  6756. */
  6757. static applyPlayRange_(timeline, playRangeStart, playRangeEnd) {
  6758. if (playRangeStart > 0) {
  6759. if (timeline.isLive()) {
  6760. shaka.log.warning(
  6761. '|playRangeStart| has been configured for live content. ' +
  6762. 'Ignoring the setting.');
  6763. } else {
  6764. timeline.setUserSeekStart(playRangeStart);
  6765. }
  6766. }
  6767. // If the playback has been configured to end before the end of the
  6768. // presentation, update the duration unless it's live content.
  6769. const fullDuration = timeline.getDuration();
  6770. if (playRangeEnd < fullDuration) {
  6771. if (timeline.isLive()) {
  6772. shaka.log.warning(
  6773. '|playRangeEnd| has been configured for live content. ' +
  6774. 'Ignoring the setting.');
  6775. } else {
  6776. timeline.setDuration(playRangeEnd);
  6777. }
  6778. }
  6779. }
  6780. /**
  6781. * Fire an event, but wait a little bit so that the immediate execution can
  6782. * complete before the event is handled.
  6783. *
  6784. * @param {!shaka.util.FakeEvent} event
  6785. * @private
  6786. */
  6787. async delayDispatchEvent_(event) {
  6788. // Wait until the next interpreter cycle.
  6789. await Promise.resolve();
  6790. // Only dispatch the event if we are still alive.
  6791. if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) {
  6792. this.dispatchEvent(event);
  6793. }
  6794. }
  6795. /**
  6796. * Get the normalized languages for a group of tracks.
  6797. *
  6798. * @param {!Array.<?shaka.extern.Track>} tracks
  6799. * @return {!Set.<string>}
  6800. * @private
  6801. */
  6802. static getLanguagesFrom_(tracks) {
  6803. const languages = new Set();
  6804. for (const track of tracks) {
  6805. if (track.language) {
  6806. languages.add(shaka.util.LanguageUtils.normalize(track.language));
  6807. } else {
  6808. languages.add('und');
  6809. }
  6810. }
  6811. return languages;
  6812. }
  6813. /**
  6814. * Get all permutations of normalized languages and role for a group of
  6815. * tracks.
  6816. *
  6817. * @param {!Array.<?shaka.extern.Track>} tracks
  6818. * @return {!Array.<shaka.extern.LanguageRole>}
  6819. * @private
  6820. */
  6821. static getLanguageAndRolesFrom_(tracks) {
  6822. /** @type {!Map.<string, !Set>} */
  6823. const languageToRoles = new Map();
  6824. /** @type {!Map.<string, !Map.<string, string>>} */
  6825. const languageRoleToLabel = new Map();
  6826. for (const track of tracks) {
  6827. let language = 'und';
  6828. let roles = [];
  6829. if (track.language) {
  6830. language = shaka.util.LanguageUtils.normalize(track.language);
  6831. }
  6832. if (track.type == 'variant') {
  6833. roles = track.audioRoles;
  6834. } else {
  6835. roles = track.roles;
  6836. }
  6837. if (!roles || !roles.length) {
  6838. // We must have an empty role so that we will still get a language-role
  6839. // entry from our Map.
  6840. roles = [''];
  6841. }
  6842. if (!languageToRoles.has(language)) {
  6843. languageToRoles.set(language, new Set());
  6844. }
  6845. for (const role of roles) {
  6846. languageToRoles.get(language).add(role);
  6847. if (track.label) {
  6848. if (!languageRoleToLabel.has(language)) {
  6849. languageRoleToLabel.set(language, new Map());
  6850. }
  6851. languageRoleToLabel.get(language).set(role, track.label);
  6852. }
  6853. }
  6854. }
  6855. // Flatten our map to an array of language-role pairs.
  6856. const pairings = [];
  6857. languageToRoles.forEach((roles, language) => {
  6858. for (const role of roles) {
  6859. let label = null;
  6860. if (languageRoleToLabel.has(language) &&
  6861. languageRoleToLabel.get(language).has(role)) {
  6862. label = languageRoleToLabel.get(language).get(role);
  6863. }
  6864. pairings.push({language, role, label});
  6865. }
  6866. });
  6867. return pairings;
  6868. }
  6869. /**
  6870. * Assuming the player is playing content with media source, check if the
  6871. * player has buffered enough content to make it to the end of the
  6872. * presentation.
  6873. *
  6874. * @return {boolean}
  6875. * @private
  6876. */
  6877. isBufferedToEndMS_() {
  6878. goog.asserts.assert(
  6879. this.video_,
  6880. 'We need a video element to get buffering information');
  6881. goog.asserts.assert(
  6882. this.mediaSourceEngine_,
  6883. 'We need a media source engine to get buffering information');
  6884. goog.asserts.assert(
  6885. this.manifest_,
  6886. 'We need a manifest to get buffering information');
  6887. // This is a strong guarantee that we are buffered to the end, because it
  6888. // means the playhead is already at that end.
  6889. if (this.video_.ended) {
  6890. return true;
  6891. }
  6892. // This means that MediaSource has buffered the final segment in all
  6893. // SourceBuffers and is no longer accepting additional segments.
  6894. if (this.mediaSourceEngine_.ended()) {
  6895. return true;
  6896. }
  6897. // Live streams are "buffered to the end" when they have buffered to the
  6898. // live edge or beyond (into the region covered by the presentation delay).
  6899. if (this.manifest_.presentationTimeline.isLive()) {
  6900. const liveEdge =
  6901. this.manifest_.presentationTimeline.getSegmentAvailabilityEnd();
  6902. const bufferEnd =
  6903. shaka.media.TimeRangesUtils.bufferEnd(this.video_.buffered);
  6904. if (bufferEnd != null && bufferEnd >= liveEdge) {
  6905. return true;
  6906. }
  6907. }
  6908. return false;
  6909. }
  6910. /**
  6911. * Assuming the player is playing content with src=, check if the player has
  6912. * buffered enough content to make it to the end of the presentation.
  6913. *
  6914. * @return {boolean}
  6915. * @private
  6916. */
  6917. isBufferedToEndSrc_() {
  6918. goog.asserts.assert(
  6919. this.video_,
  6920. 'We need a video element to get buffering information');
  6921. // This is a strong guarantee that we are buffered to the end, because it
  6922. // means the playhead is already at that end.
  6923. if (this.video_.ended) {
  6924. return true;
  6925. }
  6926. // If we have buffered to the duration of the content, it means we will have
  6927. // enough content to buffer to the end of the presentation.
  6928. const bufferEnd =
  6929. shaka.media.TimeRangesUtils.bufferEnd(this.video_.buffered);
  6930. // Because Safari's native HLS reports slightly inaccurate values for
  6931. // bufferEnd here, we use a fudge factor. Without this, we can end up in a
  6932. // buffering state at the end of the stream. See issue #2117.
  6933. const fudge = 1; // 1000 ms
  6934. return bufferEnd != null && bufferEnd >= this.video_.duration - fudge;
  6935. }
  6936. /**
  6937. * Create an error for when we purposely interrupt a load operation.
  6938. *
  6939. * @return {!shaka.util.Error}
  6940. * @private
  6941. */
  6942. createAbortLoadError_() {
  6943. return new shaka.util.Error(
  6944. shaka.util.Error.Severity.CRITICAL,
  6945. shaka.util.Error.Category.PLAYER,
  6946. shaka.util.Error.Code.LOAD_INTERRUPTED);
  6947. }
  6948. };
  6949. /**
  6950. * In order to know what method of loading the player used for some content, we
  6951. * have this enum. It lets us know if content has not been loaded, loaded with
  6952. * media source, or loaded with src equals.
  6953. *
  6954. * This enum has a low resolution, because it is only meant to express the
  6955. * outer limits of the various states that the player is in. For example, when
  6956. * someone calls a public method on player, it should not matter if they have
  6957. * initialized drm engine, it should only matter if they finished loading
  6958. * content.
  6959. *
  6960. * @enum {number}
  6961. * @export
  6962. */
  6963. shaka.Player.LoadMode = {
  6964. 'DESTROYED': 0,
  6965. 'NOT_LOADED': 1,
  6966. 'MEDIA_SOURCE': 2,
  6967. 'SRC_EQUALS': 3,
  6968. };
  6969. /**
  6970. * The typical buffering threshold. When we have less than this buffered (in
  6971. * seconds), we enter a buffering state. This specific value is based on manual
  6972. * testing and evaluation across a variety of platforms.
  6973. *
  6974. * To make the buffering logic work in all cases, this "typical" threshold will
  6975. * be overridden if the rebufferingGoal configuration is too low.
  6976. *
  6977. * @const {number}
  6978. * @private
  6979. */
  6980. shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
  6981. /**
  6982. * @define {string} A version number taken from git at compile time.
  6983. * @export
  6984. */
  6985. // eslint-disable-next-line no-useless-concat
  6986. shaka.Player.version = 'v4.10.0' + '-uncompiled'; // x-release-please-version
  6987. // Initialize the deprecation system using the version string we just set
  6988. // on the player.
  6989. shaka.Deprecate.init(shaka.Player.version);
  6990. /** @private {!Object.<string, function():*>} */
  6991. shaka.Player.supportPlugins_ = {};
  6992. /** @private {?shaka.extern.IAdManager.Factory} */
  6993. shaka.Player.adManagerFactory_ = null;
  6994. /**
  6995. * @const {string}
  6996. */
  6997. shaka.Player.TextTrackLabel = 'Shaka Player TextTrack';