Source: lib/player.js

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