Source: lib/player.js

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