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