Source: lib/player.js

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