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