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