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