Source: lib/player.js

  1. /**
  2. * @license
  3. * Copyright 2016 Google Inc.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. goog.provide('shaka.Player');
  18. goog.require('goog.asserts');
  19. goog.require('shaka.abr.SimpleAbrManager');
  20. goog.require('shaka.log');
  21. goog.require('shaka.media.DrmEngine');
  22. goog.require('shaka.media.ManifestParser');
  23. goog.require('shaka.media.MediaSourceEngine');
  24. goog.require('shaka.media.Playhead');
  25. goog.require('shaka.media.PlayheadObserver');
  26. goog.require('shaka.media.SegmentReference');
  27. goog.require('shaka.media.StreamingEngine');
  28. goog.require('shaka.net.NetworkingEngine');
  29. goog.require('shaka.text.SimpleTextDisplayer');
  30. goog.require('shaka.util.ArrayUtils');
  31. goog.require('shaka.util.ConfigUtils');
  32. goog.require('shaka.util.Error');
  33. goog.require('shaka.util.EventManager');
  34. goog.require('shaka.util.FakeEvent');
  35. goog.require('shaka.util.FakeEventTarget');
  36. goog.require('shaka.util.Functional');
  37. goog.require('shaka.util.IDestroyable');
  38. goog.require('shaka.util.ManifestParserUtils');
  39. goog.require('shaka.util.MapUtils');
  40. goog.require('shaka.util.PublicPromise');
  41. goog.require('shaka.util.StreamUtils');
  42. /**
  43. * Construct a Player.
  44. *
  45. * @param {HTMLMediaElement=} mediaElem If provided, this is equivalent to
  46. * calling attach(mediaElem, true) immediately after construction.
  47. * @param {function(shaka.Player)=} opt_dependencyInjector Optional callback
  48. * which is called to inject mocks into the Player. Used for testing.
  49. *
  50. * @constructor
  51. * @struct
  52. * @implements {shaka.util.IDestroyable}
  53. * @extends {shaka.util.FakeEventTarget}
  54. * @export
  55. */
  56. shaka.Player = function(mediaElem, opt_dependencyInjector) {
  57. shaka.util.FakeEventTarget.call(this);
  58. /** @private {boolean} */
  59. this.destroyed_ = false;
  60. /** @private {HTMLMediaElement} */
  61. this.video_ = null;
  62. /**
  63. * Only holds the visibility setting until a textDisplayer_ is created.
  64. * @private {boolean}
  65. */
  66. this.textVisibility_ = false;
  67. /** @private {shakaExtern.TextDisplayer} */
  68. this.textDisplayer_ = null;
  69. /** @private {shaka.util.EventManager} */
  70. this.eventManager_ = new shaka.util.EventManager();
  71. /** @private {shaka.net.NetworkingEngine} */
  72. this.networkingEngine_ = null;
  73. /** @private {shaka.media.DrmEngine} */
  74. this.drmEngine_ = null;
  75. /** @private {shaka.media.MediaSourceEngine} */
  76. this.mediaSourceEngine_ = null;
  77. /** @private {shaka.media.Playhead} */
  78. this.playhead_ = null;
  79. /** @private {shaka.media.PlayheadObserver} */
  80. this.playheadObserver_ = null;
  81. /** @private {shaka.media.StreamingEngine} */
  82. this.streamingEngine_ = null;
  83. /** @private {shakaExtern.ManifestParser} */
  84. this.parser_ = null;
  85. /** @private {?shakaExtern.Manifest} */
  86. this.manifest_ = null;
  87. /** @private {?string} */
  88. this.manifestUri_ = null;
  89. /** @private {shakaExtern.AbrManager} */
  90. this.abrManager_ = null;
  91. /**
  92. * The factory that was used to create the abrManager_ instance.
  93. * @private {?shakaExtern.AbrManager.Factory}
  94. */
  95. this.abrManagerFactory_ = null;
  96. /**
  97. * Contains an ID for use with creating streams. The manifest parser should
  98. * start with small IDs, so this starts with a large one.
  99. * @private {number}
  100. */
  101. this.nextExternalStreamId_ = 1e9;
  102. /** @private {!Array.<number>} */
  103. this.loadingTextStreamIds_ = [];
  104. /** @private {boolean} */
  105. this.buffering_ = false;
  106. /** @private {boolean} */
  107. this.switchingPeriods_ = true;
  108. /** @private {?function()} */
  109. this.onCancelLoad_ = null;
  110. /** @private {Promise} */
  111. this.unloadChain_ = null;
  112. /** @private {?shakaExtern.Variant} */
  113. this.deferredVariant_ = null;
  114. /** @private {boolean} */
  115. this.deferredVariantClearBuffer_ = false;
  116. /** @private {?shakaExtern.Stream} */
  117. this.deferredTextStream_ = null;
  118. /** @private {!Array.<shakaExtern.TimelineRegionInfo>} */
  119. this.pendingTimelineRegions_ = [];
  120. /**
  121. * A map of Period number to a map of content type to stream id.
  122. * @private {!Object.<number, !Object.<string, number>>}
  123. */
  124. this.activeStreamsByPeriod_ = {};
  125. /** @private {?shakaExtern.PlayerConfiguration} */
  126. this.config_ = this.defaultConfig_();
  127. /** @private {{width: number, height: number}} */
  128. this.maxHwRes_ = {width: Infinity, height: Infinity};
  129. /** @private {shakaExtern.Stats} */
  130. this.stats_ = this.getCleanStats_();
  131. /** @private {number} */
  132. this.lastTimeStatsUpdateTimestamp_ = 0;
  133. /** @private {string} */
  134. this.currentAudioLanguage_ = this.config_.preferredAudioLanguage;
  135. /** @private {string} */
  136. this.currentTextLanguage_ = this.config_.preferredTextLanguage;
  137. /** @private {string} */
  138. this.currentVariantRole_ = this.config_.preferredVariantRole;
  139. /** @private {string} */
  140. this.currentTextRole_ = this.config_.preferredTextRole;
  141. /** @private {number} */
  142. this.currentAudioChannelCount_ = this.config_.preferredAudioChannelCount;
  143. if (opt_dependencyInjector) {
  144. opt_dependencyInjector(this);
  145. }
  146. this.networkingEngine_ = this.createNetworkingEngine();
  147. if (mediaElem) {
  148. this.attach(mediaElem, true /* initializeMediaSource */);
  149. }
  150. // If the browser comes back online after being offline, then try to play
  151. // again.
  152. this.eventManager_.listen(window, 'online', () => {
  153. this.retryStreaming();
  154. });
  155. };
  156. goog.inherits(shaka.Player, shaka.util.FakeEventTarget);
  157. /**
  158. * @return {!Promise}
  159. * @private
  160. */
  161. shaka.Player.prototype.cancelLoad_ = function() {
  162. if (!this.onCancelLoad_) {
  163. return Promise.resolve();
  164. }
  165. let stopParser = Promise.resolve();
  166. if (this.parser_) {
  167. // Stop the parser manually, to ensure that any network calls it may be
  168. // making are stopped in a timely fashion.
  169. // This happens in parallel with cancelling the load chain.
  170. // Otherwise, destroying will wait for any failing network calls to run
  171. // out of retries.
  172. stopParser = this.parser_.stop();
  173. this.parser_ = null;
  174. }
  175. return Promise.all([stopParser, this.onCancelLoad_()]);
  176. };
  177. /**
  178. * After destruction, a Player object cannot be used again.
  179. *
  180. * @override
  181. * @export
  182. */
  183. shaka.Player.prototype.destroy = async function() {
  184. // First, detach from the media element. This implies unloading content
  185. // and canceling pending loads.
  186. await this.detach();
  187. // Then, destroy other components and clear fields.
  188. this.destroyed_ = true;
  189. let p = Promise.all([
  190. this.eventManager_ ? this.eventManager_.destroy() : null,
  191. this.networkingEngine_ ? this.networkingEngine_.destroy() : null
  192. ]);
  193. this.textVisibility_ = false;
  194. this.eventManager_ = null;
  195. this.abrManager_ = null;
  196. this.abrManagerFactory_ = null;
  197. this.networkingEngine_ = null;
  198. this.config_ = null;
  199. await p;
  200. };
  201. /**
  202. * @define {string} A version number taken from git at compile time.
  203. * @export
  204. */
  205. shaka.Player.version = 'v2.4.7-uncompiled';
  206. /**
  207. * @event shaka.Player.ErrorEvent
  208. * @description Fired when a playback error occurs.
  209. * @property {string} type
  210. * 'error'
  211. * @property {!shaka.util.Error} detail
  212. * An object which contains details on the error. The error's 'category' and
  213. * 'code' properties will identify the specific error that occurred. In an
  214. * uncompiled build, you can also use the 'message' and 'stack' properties
  215. * to debug.
  216. * @exportDoc
  217. */
  218. /**
  219. * @event shaka.Player.EmsgEvent
  220. * @description Fired when a non-typical emsg is found in a segment.
  221. * @property {string} type
  222. * 'emsg'
  223. * @property {shakaExtern.EmsgInfo} detail
  224. * An object which contains the content of the emsg box.
  225. * @exportDoc
  226. */
  227. /**
  228. * @event shaka.Player.DrmSessionUpdateEvent
  229. * @description Fired when the CDM has accepted the license response.
  230. * @property {string} type
  231. * 'drmsessionupdate'
  232. * @exportDoc
  233. */
  234. /**
  235. * @event shaka.Player.TimelineRegionAddedEvent
  236. * @description Fired when a media timeline region is added.
  237. * @property {string} type
  238. * 'timelineregionadded'
  239. * @property {shakaExtern.TimelineRegionInfo} detail
  240. * An object which contains a description of the region.
  241. * @exportDoc
  242. */
  243. /**
  244. * @event shaka.Player.TimelineRegionEnterEvent
  245. * @description Fired when the playhead enters a timeline region.
  246. * @property {string} type
  247. * 'timelineregionenter'
  248. * @property {shakaExtern.TimelineRegionInfo} detail
  249. * An object which contains a description of the region.
  250. * @exportDoc
  251. */
  252. /**
  253. * @event shaka.Player.TimelineRegionExitEvent
  254. * @description Fired when the playhead exits a timeline region.
  255. * @property {string} type
  256. * 'timelineregionexit'
  257. * @property {shakaExtern.TimelineRegionInfo} detail
  258. * An object which contains a description of the region.
  259. * @exportDoc
  260. */
  261. /**
  262. * @event shaka.Player.BufferingEvent
  263. * @description Fired when the player's buffering state changes.
  264. * @property {string} type
  265. * 'buffering'
  266. * @property {boolean} buffering
  267. * True when the Player enters the buffering state.
  268. * False when the Player leaves the buffering state.
  269. * @exportDoc
  270. */
  271. /**
  272. * @event shaka.Player.LoadingEvent
  273. * @description Fired when the player begins loading.
  274. * Used by the Cast receiver to determine idle state.
  275. * @property {string} type
  276. * 'loading'
  277. * @exportDoc
  278. */
  279. /**
  280. * @event shaka.Player.UnloadingEvent
  281. * @description Fired when the player unloads or fails to load.
  282. * Used by the Cast receiver to determine idle state.
  283. * @property {string} type
  284. * 'unloading'
  285. * @exportDoc
  286. */
  287. /**
  288. * @event shaka.Player.TextTrackVisibilityEvent
  289. * @description Fired when text track visibility changes.
  290. * @property {string} type
  291. * 'texttrackvisibility'
  292. * @exportDoc
  293. */
  294. /**
  295. * @event shaka.Player.TracksChangedEvent
  296. * @description Fired when the list of tracks changes. For example, this will
  297. * happen when changing periods or when track restrictions change.
  298. * @property {string} type
  299. * 'trackschanged'
  300. * @exportDoc
  301. */
  302. /**
  303. * @event shaka.Player.AdaptationEvent
  304. * @description Fired when an automatic adaptation causes the active tracks
  305. * to change. Does not fire when the application calls selectVariantTrack()
  306. * selectTextTrack(), selectAudioLanguage() or selectTextLanguage().
  307. * @property {string} type
  308. * 'adaptation'
  309. * @exportDoc
  310. */
  311. /**
  312. * @event shaka.Player.ExpirationUpdatedEvent
  313. * @description Fired when there is a change in the expiration times of an
  314. * EME session.
  315. * @property {string} type
  316. * 'expirationupdated'
  317. * @exportDoc
  318. */
  319. /**
  320. * @event shaka.Player.LargeGapEvent
  321. * @description Fired when the playhead enters a large gap. If
  322. * |config.streaming.jumpLargeGaps| is set, the default action of this event
  323. * is to jump the gap; this can be prevented by calling preventDefault() on
  324. * the event object.
  325. * @property {string} type
  326. * 'largegap'
  327. * @property {number} currentTime
  328. * The current time of the playhead.
  329. * @property {number} gapSize
  330. * The size of the gap, in seconds.
  331. * @exportDoc
  332. */
  333. /**
  334. * @event shaka.Player.StreamingEvent
  335. * @description Fired after the manifest has been parsed and track information
  336. * is available, but before streams have been chosen and before any segments
  337. * have been fetched. You may use this event to configure the player based on
  338. * information found in the manifest.
  339. * @property {string} type
  340. * 'streaming'
  341. * @exportDoc
  342. */
  343. /** @private {!Object.<string, function():*>} */
  344. shaka.Player.supportPlugins_ = {};
  345. /**
  346. * Registers a plugin callback that will be called with support(). The
  347. * callback will return the value that will be stored in the return value from
  348. * support().
  349. *
  350. * @param {string} name
  351. * @param {function():*} callback
  352. * @export
  353. */
  354. shaka.Player.registerSupportPlugin = function(name, callback) {
  355. shaka.Player.supportPlugins_[name] = callback;
  356. };
  357. /**
  358. * Return whether the browser provides basic support. If this returns false,
  359. * Shaka Player cannot be used at all. In this case, do not construct a Player
  360. * instance and do not use the library.
  361. *
  362. * @return {boolean}
  363. * @export
  364. */
  365. shaka.Player.isBrowserSupported = function() {
  366. // Basic features needed for the library to be usable.
  367. let basic = !!window.Promise && !!window.Uint8Array &&
  368. !!Array.prototype.forEach;
  369. return basic &&
  370. shaka.media.MediaSourceEngine.isBrowserSupported() &&
  371. shaka.media.DrmEngine.isBrowserSupported();
  372. };
  373. /**
  374. * Probes the browser to determine what features are supported. This makes a
  375. * number of requests to EME/MSE/etc which may result in user prompts. This
  376. * should only be used for diagnostics.
  377. *
  378. * NOTE: This may show a request to the user for permission.
  379. *
  380. * @see https://goo.gl/ovYLvl
  381. * @return {!Promise.<shakaExtern.SupportType>}
  382. * @export
  383. */
  384. shaka.Player.probeSupport = function() {
  385. goog.asserts.assert(shaka.Player.isBrowserSupported(),
  386. 'Must have basic support');
  387. return shaka.media.DrmEngine.probeSupport().then(function(drm) {
  388. let manifest = shaka.media.ManifestParser.probeSupport();
  389. let media = shaka.media.MediaSourceEngine.probeSupport();
  390. let ret = {
  391. manifest: manifest,
  392. media: media,
  393. drm: drm
  394. };
  395. let plugins = shaka.Player.supportPlugins_;
  396. for (let name in plugins) {
  397. ret[name] = plugins[name]();
  398. }
  399. return ret;
  400. });
  401. };
  402. /**
  403. * Attach the Player to a media element (audio or video tag).
  404. *
  405. * If the Player is already attached to a media element, the previous element
  406. * will first be detached.
  407. *
  408. * After calling attach, the media element is owned by the Player and should not
  409. * be used for other purposes until detach or destroy() are called.
  410. *
  411. * @param {!HTMLMediaElement} mediaElem
  412. * @param {boolean=} initializeMediaSource If true, start initializing
  413. * MediaSource right away. This can improve load() latency for
  414. * MediaSource-based playbacks. Defaults to true.
  415. *
  416. * @return {!Promise} If initializeMediaSource is false, the Promise is resolved
  417. * as soon as the Player has released any previous media element and taken
  418. * ownership of the new one. If initializeMediaSource is true, the Promise
  419. * resolves after MediaSource has been subsequently initialized on the new
  420. * media element.
  421. * @export
  422. */
  423. shaka.Player.prototype.attach =
  424. async function(mediaElem, initializeMediaSource) {
  425. if (initializeMediaSource === undefined) {
  426. initializeMediaSource = true;
  427. }
  428. if (this.video_) {
  429. await this.detach();
  430. }
  431. this.video_ = mediaElem;
  432. goog.asserts.assert(mediaElem, 'Cannot attach to a null media element!');
  433. // Listen for video errors.
  434. this.eventManager_.listen(this.video_, 'error',
  435. this.onVideoError_.bind(this));
  436. if (initializeMediaSource) {
  437. // Start the (potentially slow) process of opening MediaSource now.
  438. this.mediaSourceEngine_ = this.createMediaSourceEngine();
  439. await this.mediaSourceEngine_.open();
  440. }
  441. };
  442. /**
  443. * Detaches the Player from the media element (audio or video tag).
  444. *
  445. * After calling detach and waiting for the Promise to be resolved, the media
  446. * element is no longer owned by the Player and may be used for other purposes.
  447. *
  448. * @return {!Promise} Resolved when the Player has released any previous media
  449. * element.
  450. * @export
  451. */
  452. shaka.Player.prototype.detach = async function() {
  453. if (!this.video_) {
  454. return;
  455. }
  456. // Unload any loaded content.
  457. await this.unload(false);
  458. // Stop listening for video errors.
  459. this.eventManager_.unlisten(this.video_, 'error');
  460. this.video_ = null;
  461. };
  462. /**
  463. * Creates a manifest parser and loads the given manifest.
  464. *
  465. * @param {string} manifestUri
  466. * @param {shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
  467. * Optional manifest parser factory to override auto-detection or use an
  468. * unregistered parser.
  469. * @return {!Promise.<shakaExtern.Manifest>} Resolves with the manifest.
  470. * @private
  471. */
  472. shaka.Player.prototype.loadManifest_ = async function(
  473. manifestUri, opt_manifestParserFactory) {
  474. goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed');
  475. const factory = await shaka.media.ManifestParser.getFactory(
  476. manifestUri,
  477. this.networkingEngine_,
  478. this.config_.manifest.retryParameters,
  479. opt_manifestParserFactory);
  480. this.parser_ = new factory();
  481. this.parser_.configure(this.config_.manifest);
  482. goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed');
  483. let playerInterface = {
  484. networkingEngine: this.networkingEngine_,
  485. filterNewPeriod: this.filterNewPeriod_.bind(this),
  486. filterAllPeriods: this.filterAllPeriods_.bind(this),
  487. onTimelineRegionAdded: this.onTimelineRegionAdded_.bind(this),
  488. onEvent: this.onEvent_.bind(this),
  489. onError: this.onError_.bind(this)
  490. };
  491. return this.parser_.start(manifestUri, playerInterface);
  492. };
  493. /**
  494. * When there is a variant with video and audio, filter out all variants which
  495. * lack one or the other.
  496. * This is to avoid problems where we choose audio-only variants because they
  497. * have lower bandwidth, when there are variants with video available.
  498. *
  499. * @private
  500. */
  501. shaka.Player.prototype.filterManifestForAVVariants_ = function() {
  502. const isAVVariant = (variant) => {
  503. // Audio-video variants may include both streams separately or may be single
  504. // multiplexed streams with multiple codecs.
  505. return (variant.video && variant.audio) ||
  506. (variant.video && variant.video.codecs.includes(','));
  507. };
  508. const hasAVVariant = this.manifest_.periods.some((period) => {
  509. return period.variants.some(isAVVariant);
  510. });
  511. if (hasAVVariant) {
  512. shaka.log.debug('Found variant with audio and video content, ' +
  513. 'so filtering out audio-only content in all periods.');
  514. this.manifest_.periods.forEach((period) => {
  515. period.variants = period.variants.filter(isAVVariant);
  516. });
  517. }
  518. if (this.manifest_.periods.length == 0) {
  519. throw new shaka.util.Error(
  520. shaka.util.Error.Severity.CRITICAL,
  521. shaka.util.Error.Category.MANIFEST,
  522. shaka.util.Error.Code.NO_PERIODS);
  523. }
  524. };
  525. /**
  526. * Applys playRangeStart and playRangeEnd to this.manifest_.
  527. *
  528. * @private
  529. */
  530. shaka.Player.prototype.applyPlayRange_ = function() {
  531. let fullDuration = this.manifest_.presentationTimeline.getDuration();
  532. let playRangeEnd = this.config_.playRangeEnd;
  533. let playRangeStart = this.config_.playRangeStart;
  534. if (playRangeStart > 0) {
  535. if (this.isLive()) {
  536. shaka.log.warning('PlayerConfiguration.playRangeStart has been ' +
  537. 'configured for live content. Ignoring the setting.');
  538. } else {
  539. this.manifest_.presentationTimeline.setUserSeekStart(playRangeStart);
  540. }
  541. }
  542. // If the playback has been configured to end before the end of the
  543. // presentation, update the duration unless it's live content.
  544. if (playRangeEnd < fullDuration) {
  545. if (this.isLive()) {
  546. shaka.log.warning('PlayerConfiguration.playRangeEnd has been ' +
  547. 'configured for live content. Ignoring the setting.');
  548. } else {
  549. this.manifest_.presentationTimeline.setDuration(playRangeEnd);
  550. }
  551. }
  552. };
  553. /**
  554. * Load a manifest.
  555. *
  556. * @param {string} manifestUri
  557. * @param {number=} opt_startTime Optional start time, in seconds, to begin
  558. * playback.
  559. * Defaults to 0 for VOD and to the live edge for live.
  560. * Set a positive number to start with a certain offset the beginning.
  561. * Set a negative number to start with a certain offset from the end. This is
  562. * intended for use with live streams, to start at a fixed offset from the
  563. * live edge.
  564. * @param {shakaExtern.ManifestParser.Factory=} opt_manifestParserFactory
  565. * Optional manifest parser factory to override auto-detection or use an
  566. * unregistered parser.
  567. * @return {!Promise} Resolved when the manifest has been loaded and playback
  568. * has begun; rejected when an error occurs or the call was interrupted by
  569. * destroy(), unload() or another call to load().
  570. * @export
  571. */
  572. shaka.Player.prototype.load = async function(manifestUri, opt_startTime,
  573. opt_manifestParserFactory) {
  574. if (!this.video_) {
  575. throw new shaka.util.Error(
  576. shaka.util.Error.Severity.CRITICAL,
  577. shaka.util.Error.Category.PLAYER,
  578. shaka.util.Error.Code.NO_VIDEO_ELEMENT);
  579. }
  580. let cancelValue;
  581. /** @type {!shaka.util.PublicPromise} */
  582. const cancelPromise = new shaka.util.PublicPromise();
  583. const cancelCallback = function() {
  584. cancelValue = new shaka.util.Error(
  585. shaka.util.Error.Severity.CRITICAL,
  586. shaka.util.Error.Category.PLAYER,
  587. shaka.util.Error.Code.LOAD_INTERRUPTED);
  588. return cancelPromise;
  589. };
  590. this.dispatchEvent(new shaka.util.FakeEvent('loading'));
  591. let startTime = Date.now();
  592. try {
  593. const unloadPromise = this.unload();
  594. this.onCancelLoad_ = cancelCallback;
  595. await unloadPromise;
  596. // Not tracked in stats because it should be insignificant.
  597. // Logged in case it is not.
  598. shaka.log.debug('Unload latency:', (Date.now() - startTime) / 1000);
  599. this.stats_ = this.getCleanStats_();
  600. this.eventManager_.listen(this.video_, 'playing',
  601. this.updateState_.bind(this));
  602. this.eventManager_.listen(this.video_, 'pause',
  603. this.updateState_.bind(this));
  604. this.eventManager_.listen(this.video_, 'ended',
  605. this.updateState_.bind(this));
  606. const AbrManagerFactory = this.config_.abrFactory;
  607. if (!this.abrManager_ || this.abrManagerFactory_ != AbrManagerFactory) {
  608. this.abrManagerFactory_ = AbrManagerFactory;
  609. this.abrManager_ = new AbrManagerFactory();
  610. this.abrManager_.configure(this.config_.abr);
  611. }
  612. this.textDisplayer_ = new this.config_.textDisplayFactory();
  613. this.textDisplayer_.setTextVisibility(this.textVisibility_);
  614. if (cancelValue) throw cancelValue;
  615. this.manifest_ = await this.loadManifest_(
  616. manifestUri, opt_manifestParserFactory);
  617. this.manifestUri_ = manifestUri;
  618. if (cancelValue) throw cancelValue;
  619. this.filterManifestForAVVariants_();
  620. this.drmEngine_ = this.createDrmEngine();
  621. this.drmEngine_.configure(this.config_.drm);
  622. await this.drmEngine_.init(
  623. /** @type{shakaExtern.Manifest} */ (this.manifest_),
  624. /* isOffline */ false);
  625. if (cancelValue) throw cancelValue;
  626. // Re-filter the manifest after DRM has been initialized.
  627. this.filterAllPeriods_(this.manifest_.periods);
  628. this.lastTimeStatsUpdateTimestamp_ = Date.now() / 1000;
  629. // Copy preferred languages from the config again, in case the config was
  630. // changed between construction and playback.
  631. this.currentAudioLanguage_ = this.config_.preferredAudioLanguage;
  632. this.currentTextLanguage_ = this.config_.preferredTextLanguage;
  633. this.currentAudioChannelCount_ = this.config_.preferredAudioChannelCount;
  634. this.applyPlayRange_();
  635. await this.drmEngine_.attach(this.video_);
  636. if (cancelValue) throw cancelValue;
  637. this.abrManager_.init(this.switch_.bind(this));
  638. if (!this.mediaSourceEngine_) {
  639. this.mediaSourceEngine_ = this.createMediaSourceEngine();
  640. }
  641. this.mediaSourceEngine_.setTextDisplayer(this.textDisplayer_);
  642. this.playhead_ = this.createPlayhead(opt_startTime);
  643. this.playheadObserver_ = this.createPlayheadObserver();
  644. this.streamingEngine_ = this.createStreamingEngine();
  645. this.streamingEngine_.configure(this.config_.streaming);
  646. // If the content is multi-codec and the browser can play more than one of
  647. // them, choose codecs now before we initialize streaming.
  648. this.chooseCodecsAndFilterManifest_();
  649. this.dispatchEvent(new shaka.util.FakeEvent('streaming'));
  650. await this.streamingEngine_.init();
  651. if (cancelValue) throw cancelValue;
  652. if (this.config_.streaming.startAtSegmentBoundary) {
  653. let time = this.adjustStartTime_(this.playhead_.getTime());
  654. this.playhead_.setStartTime(time);
  655. }
  656. // Re-filter the manifest after streams have been chosen.
  657. this.manifest_.periods.forEach(this.filterNewPeriod_.bind(this));
  658. // Dispatch a 'trackschanged' event now that all initial filtering is done.
  659. this.onTracksChanged_();
  660. // Since the first streams just became active, send an adaptation event.
  661. this.onAdaptation_();
  662. // Now that we've filtered out variants that aren't compatible with the
  663. // active one, update abr manager with filtered variants for the current
  664. // period.
  665. let currentPeriod = this.streamingEngine_.getCurrentPeriod();
  666. let variants = shaka.util.StreamUtils.filterVariantsByConfig(
  667. currentPeriod.variants, this.currentAudioLanguage_,
  668. this.currentVariantRole_, this.currentAudioChannelCount_);
  669. this.abrManager_.setVariants(variants);
  670. let hasPrimary = currentPeriod.variants.some(function(variant) {
  671. return variant.primary;
  672. });
  673. if (!this.currentAudioLanguage_ && !hasPrimary) {
  674. shaka.log.warning('No preferred audio language set. We will choose an ' +
  675. 'arbitrary language initially');
  676. }
  677. this.pendingTimelineRegions_.forEach(
  678. this.playheadObserver_.addTimelineRegion.bind(this.playheadObserver_));
  679. this.pendingTimelineRegions_ = [];
  680. // Wait for the 'loadeddata' event to measure load() latency.
  681. this.eventManager_.listenOnce(this.video_, 'loadeddata', function() {
  682. // Compute latency in seconds (Date.now() gives ms):
  683. let latency = (Date.now() - startTime) / 1000;
  684. this.stats_.loadLatency = latency;
  685. shaka.log.debug('Load latency:', latency);
  686. }.bind(this));
  687. if (cancelValue) throw cancelValue;
  688. this.onCancelLoad_ = null;
  689. } catch (error) {
  690. goog.asserts.assert(error instanceof shaka.util.Error,
  691. 'Wrong error type!');
  692. shaka.log.debug('load() failed:', error,
  693. error ? error.message : null, error ? error.stack : null);
  694. // If we haven't started another load, clear the onCancelLoad_.
  695. cancelPromise.resolve();
  696. if (this.onCancelLoad_ == cancelCallback) {
  697. this.onCancelLoad_ = null;
  698. this.dispatchEvent(new shaka.util.FakeEvent('unloading'));
  699. }
  700. // If part of the load chain was aborted, that async call may have thrown.
  701. // In those cases, we want the cancelation error, not the thrown error.
  702. if (cancelValue) {
  703. return Promise.reject(cancelValue);
  704. }
  705. return Promise.reject(error);
  706. }
  707. };
  708. /**
  709. * In case of multiple usable codecs, choose one based on lowest average
  710. * bandwidth and filter out the rest.
  711. * @private
  712. */
  713. shaka.Player.prototype.chooseCodecsAndFilterManifest_ = function() {
  714. // Collect a list of variants for all periods.
  715. /** @type {!Array.<shakaExtern.Variant>} */
  716. let variants = this.manifest_.periods.reduce(
  717. (variants, period) => variants.concat(period.variants), []);
  718. // To start, consider a subset of variants based on audio channel preferences.
  719. // For some content (#1013), surround-sound variants will use a different
  720. // codec than stereo variants, so it is important to choose codecs **after**
  721. // considering the audio channel config.
  722. variants = shaka.util.StreamUtils.filterVariantsByAudioChannelCount(
  723. variants, this.config_.preferredAudioChannelCount);
  724. function variantCodecs(variant) {
  725. // Only consider the base of the codec string. For example, these should
  726. // both be considered the same codec: avc1.42c01e, avc1.4d401f
  727. let baseVideoCodec =
  728. variant.video ? variant.video.codecs.split('.')[0] : '';
  729. let baseAudioCodec =
  730. variant.audio ? variant.audio.codecs.split('.')[0] : '';
  731. return baseVideoCodec + '-' + baseAudioCodec;
  732. }
  733. // Now organize variants into buckets by codecs.
  734. /** @type {!Object.<string, !Array.<shakaExtern.Variant>>} */
  735. let variantsByCodecs = {};
  736. variants.forEach(function(variant) {
  737. let codecs = variantCodecs(variant);
  738. if (!(codecs in variantsByCodecs)) {
  739. variantsByCodecs[codecs] = [];
  740. }
  741. variantsByCodecs[codecs].push(variant);
  742. });
  743. // Compute the average bandwidth for each group of variants.
  744. // Choose the lowest-bandwidth codecs.
  745. let bestCodecs = null;
  746. let lowestAverageBandwidth = Infinity;
  747. shaka.util.MapUtils.forEach(variantsByCodecs, function(codecs, variants) {
  748. let sum = 0;
  749. let num = 0;
  750. variants.forEach(function(variant) {
  751. sum += variant.bandwidth || 0;
  752. ++num;
  753. });
  754. let averageBandwidth = sum / num;
  755. shaka.log.debug('codecs', codecs, 'avg bandwidth', averageBandwidth);
  756. if (averageBandwidth < lowestAverageBandwidth) {
  757. bestCodecs = codecs;
  758. lowestAverageBandwidth = averageBandwidth;
  759. }
  760. });
  761. goog.asserts.assert(bestCodecs != null, 'Should have chosen codecs!');
  762. goog.asserts.assert(!isNaN(lowestAverageBandwidth),
  763. 'Bandwidth should be a number!');
  764. // Filter out any variants that don't match, forcing AbrManager to choose from
  765. // the most efficient variants possible.
  766. this.manifest_.periods.forEach(function(period) {
  767. period.variants = period.variants.filter(function(variant) {
  768. let codecs = variantCodecs(variant);
  769. if (codecs == bestCodecs) return true;
  770. shaka.log.debug('Dropping Variant (better codec available)', variant);
  771. return false;
  772. });
  773. });
  774. };
  775. /**
  776. * Creates a new instance of DrmEngine. This can be replaced by tests to
  777. * create fake instances instead.
  778. *
  779. * @return {!shaka.media.DrmEngine}
  780. */
  781. shaka.Player.prototype.createDrmEngine = function() {
  782. goog.asserts.assert(this.networkingEngine_, 'Must not be destroyed');
  783. let playerInterface = {
  784. netEngine: this.networkingEngine_,
  785. onError: this.onError_.bind(this),
  786. onKeyStatus: this.onKeyStatus_.bind(this),
  787. onExpirationUpdated: this.onExpirationUpdated_.bind(this),
  788. onEvent: this.onEvent_.bind(this)
  789. };
  790. return new shaka.media.DrmEngine(playerInterface);
  791. };
  792. /**
  793. * Creates a new instance of NetworkingEngine. This can be replaced by tests
  794. * to create fake instances instead.
  795. *
  796. * @return {!shaka.net.NetworkingEngine}
  797. */
  798. shaka.Player.prototype.createNetworkingEngine = function() {
  799. return new shaka.net.NetworkingEngine(this.onSegmentDownloaded_.bind(this));
  800. };
  801. /**
  802. * Creates a new instance of Playhead. This can be replaced by tests to create
  803. * fake instances instead.
  804. *
  805. * @param {number=} opt_startTime
  806. * @return {!shaka.media.Playhead}
  807. */
  808. shaka.Player.prototype.createPlayhead = function(opt_startTime) {
  809. goog.asserts.assert(this.manifest_, 'Must have manifest');
  810. goog.asserts.assert(this.video_, 'Must have video');
  811. let startTime = opt_startTime == undefined ? null : opt_startTime;
  812. return new shaka.media.Playhead(
  813. this.video_, this.manifest_, this.config_.streaming,
  814. startTime, this.onSeek_.bind(this), this.onEvent_.bind(this));
  815. };
  816. /**
  817. * Creates a new instance of PlayheadOvserver. This can be replaced by tests to
  818. * create fake instances instead.
  819. *
  820. * @return {!shaka.media.PlayheadObserver}
  821. */
  822. shaka.Player.prototype.createPlayheadObserver = function() {
  823. goog.asserts.assert(this.manifest_, 'Must have manifest');
  824. return new shaka.media.PlayheadObserver(
  825. this.video_, this.mediaSourceEngine_, this.manifest_,
  826. this.config_.streaming, this.onBuffering_.bind(this),
  827. this.onEvent_.bind(this), this.onChangePeriod_.bind(this));
  828. };
  829. /**
  830. * Creates a new instance of MediaSourceEngine. This can be replaced by tests
  831. * to create fake instances instead.
  832. *
  833. * @return {!shaka.media.MediaSourceEngine}
  834. */
  835. shaka.Player.prototype.createMediaSourceEngine = function() {
  836. return new shaka.media.MediaSourceEngine(this.video_);
  837. };
  838. /**
  839. * Creates a new instance of StreamingEngine. This can be replaced by tests
  840. * to create fake instances instead.
  841. *
  842. * @return {!shaka.media.StreamingEngine}
  843. */
  844. shaka.Player.prototype.createStreamingEngine = function() {
  845. goog.asserts.assert(
  846. this.playhead_ && this.playheadObserver_ && this.mediaSourceEngine_ &&
  847. this.manifest_,
  848. 'Must not be destroyed');
  849. let playerInterface = {
  850. playhead: this.playhead_,
  851. mediaSourceEngine: this.mediaSourceEngine_,
  852. netEngine: this.networkingEngine_,
  853. onChooseStreams: this.onChooseStreams_.bind(this),
  854. onCanSwitch: this.canSwitch_.bind(this),
  855. onError: this.onError_.bind(this),
  856. onEvent: this.onEvent_.bind(this),
  857. onManifestUpdate: this.onManifestUpdate_.bind(this),
  858. onSegmentAppended: this.onSegmentAppended_.bind(this),
  859. filterNewPeriod: this.filterNewPeriod_.bind(this),
  860. filterAllPeriods: this.filterAllPeriods_.bind(this)
  861. };
  862. return new shaka.media.StreamingEngine(this.manifest_, playerInterface);
  863. };
  864. /**
  865. * Configure the Player instance.
  866. *
  867. * The config object passed in need not be complete. It will be merged with
  868. * the existing Player configuration.
  869. *
  870. * Config keys and types will be checked. If any problems with the config
  871. * object are found, errors will be reported through logs and this returns
  872. * false. If there are errors, valid config objects are still set.
  873. *
  874. * @param {string|!Object} config This should either be a field name or an
  875. * object following the form of {@link shakaExtern.PlayerConfiguration},
  876. * where you may omit any field you do not wish to change.
  877. * @param {*=} value This should be provided if the previous parameter
  878. * was a string field name.
  879. * @return {boolean} True if the passed config object was valid, false if there
  880. * were invalid entries.
  881. * @export
  882. */
  883. shaka.Player.prototype.configure = function(config, value) {
  884. goog.asserts.assert(this.config_, 'Config must not be null!');
  885. goog.asserts.assert(typeof(config) == 'object' || arguments.length == 2,
  886. 'String configs should have values!');
  887. // ('fieldName', value) format
  888. if (arguments.length == 2 && typeof(config) == 'string') {
  889. config = this.convertToConfigObject_(config, value);
  890. }
  891. goog.asserts.assert(typeof(config) == 'object', 'Should be an object!');
  892. let ret = shaka.util.ConfigUtils.mergeConfigObjects(
  893. this.config_, config, this.defaultConfig_(), this.configOverrides_(), '');
  894. this.applyConfig_();
  895. return ret;
  896. };
  897. /**
  898. * Convert config from ('fieldName', value) format to a partial
  899. * shakaExtern.PlayerConfiguration object.
  900. * E. g. from ('manifest.retryParameters.maxAttempts', 1) to
  901. * { manifest: { retryParameters: { maxAttempts: 1 }}}.
  902. *
  903. * @param {string} fieldName
  904. * @param {*} value
  905. * @return {!Object}
  906. * @private
  907. */
  908. shaka.Player.prototype.convertToConfigObject_ = function(fieldName, value) {
  909. let configObject = {};
  910. let last = configObject;
  911. let searchIndex = 0;
  912. let nameStart = 0;
  913. while (true) { // eslint-disable-line no-constant-condition
  914. let idx = fieldName.indexOf('.', searchIndex);
  915. if (idx < 0) {
  916. break;
  917. }
  918. if (idx == 0 || fieldName[idx - 1] != '\\') {
  919. let part = fieldName.substring(nameStart, idx).replace(/\\\./g, '.');
  920. last[part] = {};
  921. last = last[part];
  922. nameStart = idx + 1;
  923. }
  924. searchIndex = idx + 1;
  925. }
  926. last[fieldName.substring(nameStart).replace(/\\\./g, '.')] = value;
  927. return configObject;
  928. };
  929. /**
  930. * Apply config changes.
  931. * @private
  932. */
  933. shaka.Player.prototype.applyConfig_ = function() {
  934. if (this.parser_) {
  935. this.parser_.configure(this.config_.manifest);
  936. }
  937. if (this.drmEngine_) {
  938. this.drmEngine_.configure(this.config_.drm);
  939. }
  940. if (this.streamingEngine_) {
  941. this.streamingEngine_.configure(this.config_.streaming);
  942. // Need to apply the restrictions to every period.
  943. try {
  944. // this.filterNewPeriod_() may throw.
  945. this.manifest_.periods.forEach(this.filterNewPeriod_.bind(this));
  946. } catch (error) {
  947. this.onError_(error);
  948. }
  949. // If the stream we are playing is restricted, we need to switch.
  950. let activeAudio = this.streamingEngine_.getActiveAudio();
  951. let activeVideo = this.streamingEngine_.getActiveVideo();
  952. let period = this.streamingEngine_.getCurrentPeriod();
  953. let activeVariant = shaka.util.StreamUtils.getVariantByStreams(
  954. activeAudio, activeVideo, period.variants);
  955. if (!activeVariant || !activeVariant.allowedByApplication ||
  956. !activeVariant.allowedByKeySystem) {
  957. shaka.log.debug('Choosing new streams after changing configuration');
  958. this.chooseStreamsAndSwitch_(period);
  959. }
  960. }
  961. if (this.abrManager_) {
  962. this.abrManager_.configure(this.config_.abr);
  963. // Simply enable/disable ABR with each call, since multiple calls to these
  964. // methods have no effect.
  965. if (this.config_.abr.enabled && !this.switchingPeriods_) {
  966. this.abrManager_.enable();
  967. } else {
  968. this.abrManager_.disable();
  969. }
  970. }
  971. };
  972. /**
  973. * Return a copy of the current configuration. Modifications of the returned
  974. * value will not affect the Player's active configuration. You must call
  975. * player.configure() to make changes.
  976. *
  977. * @return {shakaExtern.PlayerConfiguration}
  978. * @export
  979. */
  980. shaka.Player.prototype.getConfiguration = function() {
  981. goog.asserts.assert(this.config_, 'Config must not be null!');
  982. let ret = this.defaultConfig_();
  983. shaka.util.ConfigUtils.mergeConfigObjects(
  984. ret, this.config_, this.defaultConfig_(), this.configOverrides_(), '');
  985. return ret;
  986. };
  987. /**
  988. * Reset configuration to default.
  989. * @export
  990. */
  991. shaka.Player.prototype.resetConfiguration = function() {
  992. // Don't call mergeConfigObjects_(), since that would not reset open-ended
  993. // dictionaries like drm.servers.
  994. this.config_ = this.defaultConfig_();
  995. this.applyConfig_();
  996. };
  997. /**
  998. * @return {HTMLMediaElement} A reference to the HTML Media Element passed
  999. * to the constructor or to attach().
  1000. * @export
  1001. */
  1002. shaka.Player.prototype.getMediaElement = function() {
  1003. return this.video_;
  1004. };
  1005. /**
  1006. * @return {shaka.net.NetworkingEngine} A reference to the Player's networking
  1007. * engine. Applications may use this to make requests through Shaka's
  1008. * networking plugins.
  1009. * @export
  1010. */
  1011. shaka.Player.prototype.getNetworkingEngine = function() {
  1012. return this.networkingEngine_;
  1013. };
  1014. /**
  1015. * @return {?string} If a manifest is loaded, returns the manifest URI given in
  1016. * the last call to load(). Otherwise, returns null.
  1017. * @export
  1018. */
  1019. shaka.Player.prototype.getManifestUri = function() {
  1020. return this.manifestUri_;
  1021. };
  1022. /**
  1023. * @return {boolean} True if the current stream is live. False otherwise.
  1024. * @export
  1025. */
  1026. shaka.Player.prototype.isLive = function() {
  1027. return this.manifest_ ?
  1028. this.manifest_.presentationTimeline.isLive() :
  1029. false;
  1030. };
  1031. /**
  1032. * @return {boolean} True if the current stream is in-progress VOD.
  1033. * False otherwise.
  1034. * @export
  1035. */
  1036. shaka.Player.prototype.isInProgress = function() {
  1037. return this.manifest_ ?
  1038. this.manifest_.presentationTimeline.isInProgress() :
  1039. false;
  1040. };
  1041. /**
  1042. * @return {boolean} True for audio-only content. False otherwise.
  1043. * @export
  1044. */
  1045. shaka.Player.prototype.isAudioOnly = function() {
  1046. if (!this.manifest_ || !this.manifest_.periods.length) {
  1047. return false;
  1048. }
  1049. let variants = this.manifest_.periods[0].variants;
  1050. if (!variants.length) {
  1051. return false;
  1052. }
  1053. // Note that if there are some audio-only variants and some audio-video
  1054. // variants, the audio-only variants are removed during filtering.
  1055. // Therefore if the first variant has no video, that's sufficient to say it
  1056. // is audio-only content.
  1057. return !variants[0].video;
  1058. };
  1059. /**
  1060. * Get the seekable range for the current stream.
  1061. * @return {{start: number, end: number}}
  1062. * @export
  1063. */
  1064. shaka.Player.prototype.seekRange = function() {
  1065. let start = 0;
  1066. let end = 0;
  1067. if (this.manifest_) {
  1068. let timeline = this.manifest_.presentationTimeline;
  1069. start = timeline.getSeekRangeStart();
  1070. end = timeline.getSeekRangeEnd();
  1071. }
  1072. return {'start': start, 'end': end};
  1073. };
  1074. /**
  1075. * Get the key system currently being used by EME. This returns the empty
  1076. * string if not using EME.
  1077. *
  1078. * @return {string}
  1079. * @export
  1080. */
  1081. shaka.Player.prototype.keySystem = function() {
  1082. return this.drmEngine_ ? this.drmEngine_.keySystem() : '';
  1083. };
  1084. /**
  1085. * Get the DrmInfo used to initialize EME. This returns null when not using
  1086. * EME.
  1087. *
  1088. * @return {?shakaExtern.DrmInfo}
  1089. * @export
  1090. */
  1091. shaka.Player.prototype.drmInfo = function() {
  1092. return this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
  1093. };
  1094. /**
  1095. * The next known expiration time for any EME session. If the sessions never
  1096. * expire, or there are no EME sessions, this returns Infinity.
  1097. *
  1098. * @return {number}
  1099. * @export
  1100. */
  1101. shaka.Player.prototype.getExpiration = function() {
  1102. return this.drmEngine_ ? this.drmEngine_.getExpiration() : Infinity;
  1103. };
  1104. /**
  1105. * @return {boolean} True if the Player is in a buffering state.
  1106. * @export
  1107. */
  1108. shaka.Player.prototype.isBuffering = function() {
  1109. return this.buffering_;
  1110. };
  1111. /**
  1112. * Unload the current manifest and make the Player available for re-use.
  1113. *
  1114. * @param {boolean=} reinitializeMediaSource If true, start reinitializing
  1115. * MediaSource right away. This can improve load() latency for
  1116. * MediaSource-based playbacks. Defaults to true.
  1117. * @return {!Promise} If reinitializeMediaSource is false, the Promise is
  1118. * resolved as soon as streaming has stopped and the previous content, if any,
  1119. * has been unloaded. If reinitializeMediaSource is true or undefined, the
  1120. * Promise resolves after MediaSource has been subsequently reinitialized.
  1121. * @export
  1122. */
  1123. shaka.Player.prototype.unload = async function(reinitializeMediaSource) {
  1124. if (this.destroyed_) {
  1125. return;
  1126. }
  1127. if (reinitializeMediaSource === undefined) {
  1128. reinitializeMediaSource = true;
  1129. }
  1130. this.dispatchEvent(new shaka.util.FakeEvent('unloading'));
  1131. await this.cancelLoad_();
  1132. // If there is an existing unload operation, use that.
  1133. if (!this.unloadChain_) {
  1134. this.unloadChain_ = this.destroyStreaming_().then(() => {
  1135. // Force an exit from the buffering state.
  1136. this.onBuffering_(false);
  1137. this.unloadChain_ = null;
  1138. });
  1139. }
  1140. await this.unloadChain_;
  1141. if (reinitializeMediaSource) {
  1142. // Start the (potentially slow) process of opening MediaSource now.
  1143. this.mediaSourceEngine_ = this.createMediaSourceEngine();
  1144. await this.mediaSourceEngine_.open();
  1145. }
  1146. };
  1147. /**
  1148. * Gets the current effective playback rate. If using trick play, it will
  1149. * return the current trick play rate; otherwise, it will return the video
  1150. * playback rate.
  1151. * @return {number}
  1152. * @export
  1153. */
  1154. shaka.Player.prototype.getPlaybackRate = function() {
  1155. return this.playhead_ ? this.playhead_.getPlaybackRate() : 0;
  1156. };
  1157. /**
  1158. * Skip through the content without playing. Simulated using repeated seeks.
  1159. *
  1160. * Trick play will be canceled automatically if the playhead hits the beginning
  1161. * or end of the seekable range for the content.
  1162. *
  1163. * @param {number} rate The playback rate to simulate. For example, a rate of
  1164. * 2.5 would result in 2.5 seconds of content being skipped every second.
  1165. * To trick-play backward, use a negative rate.
  1166. * @export
  1167. */
  1168. shaka.Player.prototype.trickPlay = function(rate) {
  1169. shaka.log.debug('Trick play rate', rate);
  1170. if (this.playhead_) {
  1171. this.playhead_.setPlaybackRate(rate);
  1172. }
  1173. if (this.streamingEngine_) {
  1174. this.streamingEngine_.setTrickPlay(rate != 1);
  1175. }
  1176. };
  1177. /**
  1178. * Cancel trick-play.
  1179. * @export
  1180. */
  1181. shaka.Player.prototype.cancelTrickPlay = function() {
  1182. shaka.log.debug('Trick play canceled');
  1183. if (this.playhead_) {
  1184. this.playhead_.setPlaybackRate(1);
  1185. }
  1186. if (this.streamingEngine_) {
  1187. this.streamingEngine_.setTrickPlay(false);
  1188. }
  1189. };
  1190. /**
  1191. * Return a list of variant tracks available for the current
  1192. * Period. If there are multiple Periods, then you must seek to the Period
  1193. * before being able to switch.
  1194. *
  1195. * @return {!Array.<shakaExtern.Track>}
  1196. * @export
  1197. */
  1198. shaka.Player.prototype.getVariantTracks = function() {
  1199. if (!this.manifest_ || !this.playhead_) {
  1200. return [];
  1201. }
  1202. this.assertCorrectActiveStreams_();
  1203. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1204. let currentPeriod = shaka.util.StreamUtils.findPeriodContainingTime(
  1205. this.manifest_, this.playhead_.getTime());
  1206. let activeStreams = this.activeStreamsByPeriod_[currentPeriod] || {};
  1207. return shaka.util.StreamUtils.getVariantTracks(
  1208. this.manifest_.periods[currentPeriod],
  1209. activeStreams[ContentType.AUDIO],
  1210. activeStreams[ContentType.VIDEO]);
  1211. };
  1212. /**
  1213. * Return a list of text tracks available for the current
  1214. * Period. If there are multiple Periods, then you must seek to the Period
  1215. * before being able to switch.
  1216. *
  1217. * @return {!Array.<shakaExtern.Track>}
  1218. * @export
  1219. */
  1220. shaka.Player.prototype.getTextTracks = function() {
  1221. if (!this.manifest_ || !this.playhead_) {
  1222. return [];
  1223. }
  1224. this.assertCorrectActiveStreams_();
  1225. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1226. let currentPeriod = shaka.util.StreamUtils.findPeriodContainingTime(
  1227. this.manifest_, this.playhead_.getTime());
  1228. let activeStreams = this.activeStreamsByPeriod_[currentPeriod] || {};
  1229. if (!activeStreams[ContentType.TEXT]) {
  1230. // This is a workaround for the demo page to be able to display the
  1231. // list of text tracks. If no text track is currently active, pick
  1232. // the one that's going to be streamed when captions are enabled
  1233. // and mark it as active.
  1234. let textStreams = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
  1235. this.manifest_.periods[currentPeriod].textStreams,
  1236. this.currentTextLanguage_,
  1237. this.currentTextRole_);
  1238. if (textStreams.length) {
  1239. activeStreams[ContentType.TEXT] = textStreams[0].id;
  1240. }
  1241. }
  1242. return shaka.util.StreamUtils
  1243. .getTextTracks(
  1244. this.manifest_.periods[currentPeriod],
  1245. activeStreams[ContentType.TEXT])
  1246. .filter(function(track) {
  1247. // Don't show any tracks that are being loaded still.
  1248. return this.loadingTextStreamIds_.indexOf(track.id) < 0;
  1249. }.bind(this));
  1250. };
  1251. /**
  1252. * Select a specific text track. Note that AdaptationEvents are not
  1253. * fired for manual track selections.
  1254. *
  1255. * @param {shakaExtern.Track} track
  1256. * @export
  1257. */
  1258. shaka.Player.prototype.selectTextTrack = function(track) {
  1259. if (!this.streamingEngine_) {
  1260. return;
  1261. }
  1262. const StreamUtils = shaka.util.StreamUtils;
  1263. let period = this.streamingEngine_.getCurrentPeriod();
  1264. let stream = StreamUtils.findTextStreamForTrack(period, track);
  1265. if (!stream) {
  1266. shaka.log.error('Unable to find the track with id "' + track.id +
  1267. '"; did we change Periods?');
  1268. return;
  1269. }
  1270. this.mediaSourceEngine_.setUseEmbeddedText(false);
  1271. // Add entries to the history.
  1272. this.addTextStreamToSwitchHistory_(stream, /* fromAdaptation */ false);
  1273. this.switchTextStream_(stream);
  1274. // Workaround for https://github.com/google/shaka-player/issues/1299
  1275. // When track is selected, back-propogate the language to
  1276. // currentTextLanguage_.
  1277. this.currentTextLanguage_ = stream.language;
  1278. };
  1279. /**
  1280. * Use the embedded text for the current stream, if present.
  1281. *
  1282. * CEA 608/708 captions data is embedded inside the video stream.
  1283. *
  1284. * @export
  1285. */
  1286. shaka.Player.prototype.selectEmbeddedTextTrack = function() {
  1287. this.mediaSourceEngine_.setUseEmbeddedText(true);
  1288. this.streamingEngine_.unloadTextStream();
  1289. };
  1290. /**
  1291. * @return {boolean} True if we are using any embedded text tracks present.
  1292. * @export
  1293. */
  1294. shaka.Player.prototype.usingEmbeddedTextTrack = function() {
  1295. return this.mediaSourceEngine_ ?
  1296. this.mediaSourceEngine_.getUseEmbeddedText() : false;
  1297. };
  1298. /**
  1299. * Select a specific track. Note that AdaptationEvents are not fired for manual
  1300. * track selections.
  1301. *
  1302. * @param {shakaExtern.Track} track
  1303. * @param {boolean=} opt_clearBuffer
  1304. * @export
  1305. */
  1306. shaka.Player.prototype.selectVariantTrack = function(track, opt_clearBuffer) {
  1307. if (!this.streamingEngine_) {
  1308. return;
  1309. }
  1310. if (this.config_.abr.enabled) {
  1311. shaka.log.alwaysWarn('Changing tracks while abr manager is enabled will ' +
  1312. 'likely result in the selected track being ' +
  1313. 'overriden. Consider disabling abr before calling ' +
  1314. 'selectVariantTrack().');
  1315. }
  1316. const StreamUtils = shaka.util.StreamUtils;
  1317. let period = this.streamingEngine_.getCurrentPeriod();
  1318. let variant = StreamUtils.findVariantForTrack(period, track);
  1319. if (!variant) {
  1320. shaka.log.error('Unable to locate track with id "' + track.id + '".');
  1321. return;
  1322. }
  1323. // Double check that the track is allowed to be played.
  1324. // The track list should only contain playable variants,
  1325. // but if restrictions change and selectVariantTrack()
  1326. // is called before the track list is updated, we could
  1327. // get a now-restricted variant.
  1328. let variantIsPlayable = StreamUtils.isPlayable(variant);
  1329. if (!variantIsPlayable) {
  1330. shaka.log.error('Unable to switch to track with id "' + track.id +
  1331. '" because it is restricted.');
  1332. return;
  1333. }
  1334. // Add entries to the history.
  1335. this.addVariantToSwitchHistory_(variant, /* fromAdaptation */ false);
  1336. this.switchVariant_(variant, opt_clearBuffer);
  1337. // Workaround for https://github.com/google/shaka-player/issues/1299
  1338. // When track is selected, back-propogate the language to
  1339. // currentAudioLanguage_.
  1340. this.currentAudioLanguage_ = variant.language;
  1341. if (variant.audio && variant.audio.channelsCount) {
  1342. this.currentAudioChannelCount_ = variant.audio.channelsCount;
  1343. }
  1344. // Update AbrManager variants to match these new settings.
  1345. let variants = shaka.util.StreamUtils.filterVariantsByConfig(
  1346. period.variants, this.currentAudioLanguage_, this.currentVariantRole_,
  1347. this.currentAudioChannelCount_);
  1348. this.abrManager_.setVariants(variants);
  1349. };
  1350. /**
  1351. * Return a list of audio language-role combinations available for the current
  1352. * Period.
  1353. *
  1354. * @return {!Array.<shakaExtern.LanguageRole>}
  1355. * @export
  1356. */
  1357. shaka.Player.prototype.getAudioLanguagesAndRoles = function() {
  1358. if (!this.streamingEngine_) {
  1359. return [];
  1360. }
  1361. const StreamUtils = shaka.util.StreamUtils;
  1362. let period = this.streamingEngine_.getCurrentPeriod();
  1363. let variants = StreamUtils.getPlayableVariants(period.variants);
  1364. let audioStreams = variants.map(function(variant) {
  1365. return variant.audio;
  1366. }).filter(shaka.util.Functional.isNotDuplicate);
  1367. return this.getLanguagesAndRoles_(audioStreams);
  1368. };
  1369. /**
  1370. * Return a list of text language-role combinations available for the current
  1371. * Period.
  1372. *
  1373. * @return {!Array.<shakaExtern.LanguageRole>}
  1374. * @export
  1375. */
  1376. shaka.Player.prototype.getTextLanguagesAndRoles = function() {
  1377. if (!this.streamingEngine_) {
  1378. return [];
  1379. }
  1380. let period = this.streamingEngine_.getCurrentPeriod();
  1381. return this.getLanguagesAndRoles_(period.textStreams);
  1382. };
  1383. /**
  1384. * Return a list of audio languages available for the current Period.
  1385. *
  1386. * @return {!Array.<string>}
  1387. * @export
  1388. */
  1389. shaka.Player.prototype.getAudioLanguages = function() {
  1390. if (!this.streamingEngine_) {
  1391. return [];
  1392. }
  1393. const StreamUtils = shaka.util.StreamUtils;
  1394. let period = this.streamingEngine_.getCurrentPeriod();
  1395. let variants = StreamUtils.getPlayableVariants(period.variants);
  1396. return variants.map(function(variant) {
  1397. return variant.language;
  1398. }).filter(shaka.util.Functional.isNotDuplicate);
  1399. };
  1400. /**
  1401. * Return a list of text languages available for the current Period.
  1402. *
  1403. * @return {!Array.<string>}
  1404. * @export
  1405. */
  1406. shaka.Player.prototype.getTextLanguages = function() {
  1407. if (!this.streamingEngine_) {
  1408. return [];
  1409. }
  1410. let period = this.streamingEngine_.getCurrentPeriod();
  1411. return period.textStreams.map(function(stream) {
  1412. return stream.language;
  1413. }).filter(shaka.util.Functional.isNotDuplicate);
  1414. };
  1415. /**
  1416. * Given a list of streams, return a list of language-role combinations
  1417. * available for them.
  1418. *
  1419. * @param {!Array.<?shakaExtern.Stream>} streams
  1420. * @return {!Array.<shakaExtern.LanguageRole>}
  1421. * @private
  1422. */
  1423. shaka.Player.prototype.getLanguagesAndRoles_ = function(streams) {
  1424. /** @const {string} */
  1425. const noLanguage = 'und';
  1426. /** @const {string} */
  1427. const noRole = '';
  1428. /** @type {!Array.<shakaExtern.LanguageRole>} */
  1429. let roleLangCombinations = [];
  1430. streams.forEach(function(stream) {
  1431. if (!stream) {
  1432. // Video-only variant
  1433. roleLangCombinations.push({language: noLanguage, role: noRole});
  1434. } else {
  1435. let language = stream.language;
  1436. if (stream.roles.length) {
  1437. stream.roles.forEach(function(role) {
  1438. roleLangCombinations.push({language: language, role: role});
  1439. });
  1440. } else {
  1441. // No roles, just add language by itself
  1442. roleLangCombinations.push({language: language, role: noRole});
  1443. }
  1444. }
  1445. });
  1446. return shaka.util.ArrayUtils.removeDuplicates(
  1447. roleLangCombinations,
  1448. function(a, b) {
  1449. return a.language == b.language && a.role == b.role;
  1450. });
  1451. };
  1452. /**
  1453. * Sets currentAudioLanguage and currentVariantRole to the selected
  1454. * language and role, and chooses a new variant if need be.
  1455. *
  1456. * @param {string} language
  1457. * @param {string=} opt_role
  1458. * @export
  1459. */
  1460. shaka.Player.prototype.selectAudioLanguage =
  1461. function(language, opt_role) {
  1462. if (!this.streamingEngine_) return;
  1463. let period = this.streamingEngine_.getCurrentPeriod();
  1464. this.currentAudioLanguage_ = language;
  1465. this.currentVariantRole_ = opt_role || '';
  1466. // TODO: Refactor to only change audio and not affect text.
  1467. this.chooseStreamsAndSwitch_(period);
  1468. };
  1469. /**
  1470. * Sets currentTextLanguage and currentTextRole to the selected
  1471. * language and role, and chooses a new text stream if need be.
  1472. *
  1473. * @param {string} language
  1474. * @param {string=} opt_role
  1475. * @export
  1476. */
  1477. shaka.Player.prototype.selectTextLanguage =
  1478. function(language, opt_role) {
  1479. if (!this.streamingEngine_) return;
  1480. let period = this.streamingEngine_.getCurrentPeriod();
  1481. this.currentTextLanguage_ = language;
  1482. this.currentTextRole_ = opt_role || '';
  1483. // TODO: Refactor to only change text and not affect audio.
  1484. this.chooseStreamsAndSwitch_(period);
  1485. };
  1486. /**
  1487. * @return {boolean} True if the current text track is visible.
  1488. * @export
  1489. */
  1490. shaka.Player.prototype.isTextTrackVisible = function() {
  1491. if (this.textDisplayer_) {
  1492. return this.textDisplayer_.isTextVisible();
  1493. } else {
  1494. return this.textVisibility_;
  1495. }
  1496. };
  1497. /**
  1498. * Set the visibility of the current text track, if any.
  1499. *
  1500. * @param {boolean} on
  1501. * @export
  1502. */
  1503. shaka.Player.prototype.setTextTrackVisibility = function(on) {
  1504. if (this.textDisplayer_) {
  1505. this.textDisplayer_.setTextVisibility(on);
  1506. }
  1507. this.textVisibility_ = on;
  1508. this.onTextTrackVisibility_();
  1509. // If we always stream text, don't do anything special to StreamingEngine.
  1510. if (this.config_.streaming.alwaysStreamText) return;
  1511. // Load text stream when the user chooses to show the caption, and pause
  1512. // loading text stream when the user chooses to hide the caption.
  1513. if (!this.streamingEngine_) return;
  1514. const StreamUtils = shaka.util.StreamUtils;
  1515. if (on) {
  1516. let period = this.streamingEngine_.getCurrentPeriod();
  1517. let textStreams = StreamUtils.filterStreamsByLanguageAndRole(
  1518. period.textStreams,
  1519. this.currentTextLanguage_,
  1520. this.currentTextRole_);
  1521. let stream = textStreams[0];
  1522. if (stream) {
  1523. this.streamingEngine_.loadNewTextStream(stream);
  1524. }
  1525. } else {
  1526. this.streamingEngine_.unloadTextStream();
  1527. }
  1528. };
  1529. /**
  1530. * Returns current playhead time as a Date.
  1531. *
  1532. * @return {Date}
  1533. * @export
  1534. */
  1535. shaka.Player.prototype.getPlayheadTimeAsDate = function() {
  1536. if (!this.manifest_) return null;
  1537. goog.asserts.assert(this.isLive(),
  1538. 'getPlayheadTimeAsDate should be called on a live stream!');
  1539. let time =
  1540. this.manifest_.presentationTimeline.getPresentationStartTime() * 1000 +
  1541. this.video_.currentTime * 1000;
  1542. return new Date(time);
  1543. };
  1544. /**
  1545. * Returns the presentation start time as a Date.
  1546. *
  1547. * @return {Date}
  1548. * @export
  1549. */
  1550. shaka.Player.prototype.getPresentationStartTimeAsDate = function() {
  1551. if (!this.manifest_) return null;
  1552. goog.asserts.assert(this.isLive(),
  1553. 'getPresentationStartTimeAsDate should be called on a live stream!');
  1554. let time =
  1555. this.manifest_.presentationTimeline.getPresentationStartTime() * 1000;
  1556. return new Date(time);
  1557. };
  1558. /**
  1559. * Return the information about the current buffered ranges.
  1560. *
  1561. * @return {shakaExtern.BufferedInfo}
  1562. * @export
  1563. */
  1564. shaka.Player.prototype.getBufferedInfo = function() {
  1565. if (!this.mediaSourceEngine_) {
  1566. return {
  1567. total: [],
  1568. audio: [],
  1569. video: [],
  1570. text: []
  1571. };
  1572. }
  1573. return this.mediaSourceEngine_.getBufferedInfo();
  1574. };
  1575. /**
  1576. * Return playback and adaptation stats.
  1577. *
  1578. * @return {shakaExtern.Stats}
  1579. * @export
  1580. */
  1581. shaka.Player.prototype.getStats = function() {
  1582. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1583. this.updateTimeStats_();
  1584. this.updateState_();
  1585. let video = null;
  1586. let variant = null;
  1587. let videoElem = /** @type {!HTMLVideoElement} */ (this.video_);
  1588. let videoInfo = videoElem && videoElem.getVideoPlaybackQuality ?
  1589. videoElem.getVideoPlaybackQuality() : {};
  1590. if (this.playhead_ && this.manifest_) {
  1591. let periodIdx = shaka.util.StreamUtils.findPeriodContainingTime(
  1592. this.manifest_, this.playhead_.getTime());
  1593. let period = this.manifest_.periods[periodIdx];
  1594. let activeStreams = this.activeStreamsByPeriod_[periodIdx];
  1595. if (activeStreams) {
  1596. variant = shaka.util.StreamUtils.getVariantByStreamIds(
  1597. activeStreams[ContentType.AUDIO],
  1598. activeStreams[ContentType.VIDEO],
  1599. period.variants);
  1600. // |variant| can possibly be null, if the period contains a text stream
  1601. // but no video or audio streams.
  1602. video = variant ? variant.video : null;
  1603. }
  1604. }
  1605. if (!video) video = {};
  1606. if (!variant) variant = {};
  1607. // Clone the internal object so our state cannot be tampered with.
  1608. const cloneObject = shaka.util.ConfigUtils.cloneObject;
  1609. return {
  1610. // Not tracked in this.stats_:
  1611. width: video.width || 0,
  1612. height: video.height || 0,
  1613. streamBandwidth: variant.bandwidth || 0,
  1614. decodedFrames: Number(videoInfo.totalVideoFrames),
  1615. droppedFrames: Number(videoInfo.droppedVideoFrames),
  1616. estimatedBandwidth: this.abrManager_ ?
  1617. this.abrManager_.getBandwidthEstimate() : NaN,
  1618. loadLatency: this.stats_.loadLatency,
  1619. playTime: this.stats_.playTime,
  1620. bufferingTime: this.stats_.bufferingTime,
  1621. // Deep-clone the objects as well as the arrays that contain them:
  1622. switchHistory: cloneObject(this.stats_.switchHistory),
  1623. stateHistory: cloneObject(this.stats_.stateHistory)
  1624. };
  1625. };
  1626. /**
  1627. * Adds the given text track to the current Period. load() must resolve before
  1628. * calling. The current Period or the presentation must have a duration. This
  1629. * returns a Promise that will resolve with the track that was created, when
  1630. * that track can be switched to.
  1631. *
  1632. * @param {string} uri
  1633. * @param {string} language
  1634. * @param {string} kind
  1635. * @param {string} mime
  1636. * @param {string=} opt_codec
  1637. * @param {string=} opt_label
  1638. * @return {!Promise.<shakaExtern.Track>}
  1639. * @export
  1640. */
  1641. shaka.Player.prototype.addTextTrack = function(
  1642. uri, language, kind, mime, opt_codec, opt_label) {
  1643. if (!this.streamingEngine_) {
  1644. shaka.log.error(
  1645. 'Must call load() and wait for it to resolve before adding text ' +
  1646. 'tracks.');
  1647. return Promise.reject();
  1648. }
  1649. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  1650. // Get the Period duration.
  1651. let period = this.streamingEngine_.getCurrentPeriod();
  1652. /** @type {number} */
  1653. const periodIndex = this.manifest_.periods.indexOf(period);
  1654. /** @type {number} */
  1655. const nextPeriodIndex = periodIndex + 1;
  1656. /** @type {number} */
  1657. const nextPeriodStart = nextPeriodIndex >= this.manifest_.periods.length ?
  1658. this.manifest_.presentationTimeline.getDuration() :
  1659. this.manifest_.periods[nextPeriodIndex].startTime;
  1660. /** @type {number} */
  1661. const periodDuration = nextPeriodStart - period.startTime;
  1662. if (periodDuration == Infinity) {
  1663. return Promise.reject(new shaka.util.Error(
  1664. shaka.util.Error.Severity.RECOVERABLE,
  1665. shaka.util.Error.Category.MANIFEST,
  1666. shaka.util.Error.Code.CANNOT_ADD_EXTERNAL_TEXT_TO_LIVE_STREAM));
  1667. }
  1668. /** @type {shakaExtern.Stream} */
  1669. let stream = {
  1670. id: this.nextExternalStreamId_++,
  1671. createSegmentIndex: Promise.resolve.bind(Promise),
  1672. findSegmentPosition: function(time) { return 1; },
  1673. getSegmentReference: function(ref) {
  1674. if (ref != 1) return null;
  1675. return new shaka.media.SegmentReference(
  1676. 1, 0, periodDuration, function() { return [uri]; }, 0, null);
  1677. },
  1678. initSegmentReference: null,
  1679. presentationTimeOffset: 0,
  1680. mimeType: mime,
  1681. codecs: opt_codec || '',
  1682. kind: kind,
  1683. encrypted: false,
  1684. keyId: null,
  1685. language: language,
  1686. label: opt_label || null,
  1687. type: ContentType.TEXT,
  1688. primary: false,
  1689. trickModeVideo: null,
  1690. containsEmsgBoxes: false,
  1691. roles: [],
  1692. channelsCount: null
  1693. };
  1694. // Add the stream to the loading list to ensure it isn't switched to while it
  1695. // is initializing.
  1696. this.loadingTextStreamIds_.push(stream.id);
  1697. period.textStreams.push(stream);
  1698. // Don't create the media state until subtitles are actually enabled
  1699. return this.streamingEngine_.loadNewTextStream(stream).then(function() {
  1700. if (this.destroyed_) return;
  1701. let curPeriodIdx = this.manifest_.periods.indexOf(period);
  1702. let activeText = this.streamingEngine_.getActiveText();
  1703. if (activeText) {
  1704. // If this was the first text stream, StreamingEngine will start streaming
  1705. // it in loadNewTextStream. To reflect this, update the active stream.
  1706. this.activeStreamsByPeriod_[curPeriodIdx][ContentType.TEXT] =
  1707. activeText.id;
  1708. }
  1709. // Remove the stream from the loading list.
  1710. this.loadingTextStreamIds_.splice(
  1711. this.loadingTextStreamIds_.indexOf(stream.id), 1);
  1712. shaka.log.debug('Choosing new streams after adding a text stream');
  1713. this.chooseStreamsAndSwitch_(period);
  1714. this.onTracksChanged_();
  1715. return {
  1716. id: stream.id,
  1717. active: false,
  1718. type: ContentType.TEXT,
  1719. bandwidth: 0,
  1720. language: language,
  1721. label: opt_label || null,
  1722. kind: kind,
  1723. width: null,
  1724. height: null
  1725. };
  1726. }.bind(this));
  1727. };
  1728. /**
  1729. * Set the maximum resolution that the platform's hardware can handle.
  1730. * This will be called automatically by shaka.cast.CastReceiver to enforce
  1731. * limitations of the Chromecast hardware.
  1732. *
  1733. * @param {number} width
  1734. * @param {number} height
  1735. * @export
  1736. */
  1737. shaka.Player.prototype.setMaxHardwareResolution = function(width, height) {
  1738. this.maxHwRes_.width = width;
  1739. this.maxHwRes_.height = height;
  1740. };
  1741. /**
  1742. * Retry streaming after a failure. Does nothing if not in a failure state.
  1743. * @return {boolean} False if unable to retry.
  1744. * @export
  1745. */
  1746. shaka.Player.prototype.retryStreaming = function() {
  1747. return this.streamingEngine_ ? this.streamingEngine_.retry() : false;
  1748. };
  1749. /**
  1750. * Return the manifest information if it's loaded. Otherwise, return null.
  1751. * @return {?shakaExtern.Manifest}
  1752. * @export
  1753. */
  1754. shaka.Player.prototype.getManifest = function() {
  1755. return this.manifest_;
  1756. };
  1757. /**
  1758. * @param {shakaExtern.Variant} variant
  1759. * @param {boolean} fromAdaptation
  1760. * @private
  1761. */
  1762. shaka.Player.prototype.addVariantToSwitchHistory_ = function(
  1763. variant, fromAdaptation) {
  1764. if (variant.video) {
  1765. this.updateActiveStreams_(variant.video);
  1766. }
  1767. if (variant.audio) {
  1768. this.updateActiveStreams_(variant.audio);
  1769. }
  1770. // TODO: Get StreamingEngine to track variants and create getActiveVariant()
  1771. let activePeriod = this.streamingEngine_.getActivePeriod();
  1772. let activeVariant = shaka.util.StreamUtils.getVariantByStreams(
  1773. this.streamingEngine_.getActiveAudio(),
  1774. this.streamingEngine_.getActiveVideo(),
  1775. activePeriod ? activePeriod.variants : []);
  1776. // Only log the switch if the variant changes. For the initial decision,
  1777. // activeVariant is null and variant != activeVariant.
  1778. // This allows us to avoid onAdaptation_() when nothing has changed.
  1779. if (variant != activeVariant) {
  1780. this.stats_.switchHistory.push({
  1781. timestamp: Date.now() / 1000,
  1782. id: variant.id,
  1783. type: 'variant',
  1784. fromAdaptation: fromAdaptation,
  1785. bandwidth: variant.bandwidth
  1786. });
  1787. }
  1788. };
  1789. /**
  1790. * @param {shakaExtern.Stream} textStream
  1791. * @param {boolean} fromAdaptation
  1792. * @private
  1793. */
  1794. shaka.Player.prototype.addTextStreamToSwitchHistory_ =
  1795. function(textStream, fromAdaptation) {
  1796. this.updateActiveStreams_(textStream);
  1797. this.stats_.switchHistory.push({
  1798. timestamp: Date.now() / 1000,
  1799. id: textStream.id,
  1800. type: 'text',
  1801. fromAdaptation: fromAdaptation,
  1802. bandwidth: null
  1803. });
  1804. };
  1805. /**
  1806. * @param {!shakaExtern.Stream} stream
  1807. * @private
  1808. */
  1809. shaka.Player.prototype.updateActiveStreams_ = function(stream) {
  1810. goog.asserts.assert(this.manifest_, 'Must not be destroyed');
  1811. let periodIndex =
  1812. shaka.util.StreamUtils.findPeriodContainingStream(this.manifest_, stream);
  1813. if (!this.activeStreamsByPeriod_[periodIndex]) {
  1814. this.activeStreamsByPeriod_[periodIndex] = {};
  1815. }
  1816. this.activeStreamsByPeriod_[periodIndex][stream.type] = stream.id;
  1817. };
  1818. /**
  1819. * Destroy members responsible for streaming.
  1820. *
  1821. * @return {!Promise}
  1822. * @private
  1823. */
  1824. shaka.Player.prototype.destroyStreaming_ = function() {
  1825. if (this.eventManager_) {
  1826. this.eventManager_.unlisten(this.video_, 'loadeddata');
  1827. this.eventManager_.unlisten(this.video_, 'playing');
  1828. this.eventManager_.unlisten(this.video_, 'pause');
  1829. this.eventManager_.unlisten(this.video_, 'ended');
  1830. }
  1831. const drmEngine = this.drmEngine_;
  1832. let p = Promise.all([
  1833. this.abrManager_ ? this.abrManager_.stop() : null,
  1834. this.mediaSourceEngine_ ? this.mediaSourceEngine_.destroy() : null,
  1835. this.playhead_ ? this.playhead_.destroy() : null,
  1836. this.playheadObserver_ ? this.playheadObserver_.destroy() : null,
  1837. this.streamingEngine_ ? this.streamingEngine_.destroy() : null,
  1838. this.parser_ ? this.parser_.stop() : null,
  1839. this.textDisplayer_ ? this.textDisplayer_.destroy() : null,
  1840. ]).then(() => {
  1841. // MediaSourceEngine must be destroyed before DrmEngine, so that DrmEngine
  1842. // can detach MediaKeys from the media element.
  1843. return drmEngine ? drmEngine.destroy() : null;
  1844. });
  1845. this.switchingPeriods_ = true;
  1846. this.drmEngine_ = null;
  1847. this.mediaSourceEngine_ = null;
  1848. this.playhead_ = null;
  1849. this.playheadObserver_ = null;
  1850. this.streamingEngine_ = null;
  1851. this.parser_ = null;
  1852. this.textDisplayer_ = null;
  1853. this.manifest_ = null;
  1854. this.manifestUri_ = null;
  1855. this.pendingTimelineRegions_ = [];
  1856. this.activeStreamsByPeriod_ = {};
  1857. this.stats_ = this.getCleanStats_();
  1858. return p;
  1859. };
  1860. /**
  1861. * @return {!Object}
  1862. * @private
  1863. */
  1864. shaka.Player.prototype.configOverrides_ = function() {
  1865. return {
  1866. '.drm.servers': '',
  1867. '.drm.clearKeys': '',
  1868. '.drm.advanced': {
  1869. distinctiveIdentifierRequired: false,
  1870. persistentStateRequired: false,
  1871. videoRobustness: '',
  1872. audioRobustness: '',
  1873. serverCertificate: new Uint8Array(0)
  1874. }
  1875. };
  1876. };
  1877. /**
  1878. * @return {shakaExtern.PlayerConfiguration}
  1879. * @private
  1880. */
  1881. shaka.Player.prototype.defaultConfig_ = function() {
  1882. // This is a relatively safe default, since 3G cell connections
  1883. // are faster than this. For slower connections, such as 2G,
  1884. // the default estimate may be too high.
  1885. let bandwidthEstimate = 500e3; // 500kbps
  1886. let abrMaxHeight = Infinity;
  1887. // Some browsers implement the Network Information API, which allows
  1888. // retrieving information about a user's network connection.
  1889. //
  1890. // We are excluding connection.type == undefined to avoid getting bogus data
  1891. // on platforms where the implementation is incomplete. Currently, desktop
  1892. // Chrome 64 returns connection type undefined and a bogus downlink value.
  1893. if (navigator.connection && navigator.connection.type) {
  1894. // If it's available, get the bandwidth estimate from the browser (in
  1895. // megabits per second) and use it as defaultBandwidthEstimate.
  1896. bandwidthEstimate = navigator.connection.downlink * 1e6;
  1897. // TODO: Move this into AbrManager, where changes to the estimate can be
  1898. // observed and absorbed.
  1899. // If the user has checked a box in the browser to ask it to use less data,
  1900. // the browser will expose this intent via connection.saveData. When that
  1901. // is true, we will default the max ABR height to 360p. Apps can override
  1902. // this if they wish.
  1903. //
  1904. // The decision to use 360p was somewhat arbitrary. We needed a default
  1905. // limit, and rather than restrict to a certain bandwidth, we decided to
  1906. // restrict resolution. This will implicitly restrict bandwidth and
  1907. // therefore save data. We (Shaka+Chrome) judged that:
  1908. // - HD would be inappropriate
  1909. // - If a user is asking their browser to save data, 360p it reasonable
  1910. // - 360p would not look terrible on small mobile device screen
  1911. // We also found that:
  1912. // - YouTube's website on mobile defaults to 360p (as of 2018)
  1913. // - iPhone 6, in portrait mode, has a physical resolution big enough
  1914. // for 360p widescreen, but a little smaller than 480p widescreen
  1915. // (https://goo.gl/5trzW5)
  1916. // If the content's lowest resolution is above 360p, AbrManager will use
  1917. // the lowest resolution.
  1918. if (navigator.connection.saveData) {
  1919. abrMaxHeight = 360;
  1920. }
  1921. }
  1922. // Because this.video_ may not be set when the config is built, the default
  1923. // TextDisplay factory must capture a reference to "this" as "self" to use at
  1924. // the time we call the factory. Bind can't be used here because we call the
  1925. // factory with "new", effectively removing any binding to "this".
  1926. const self = this;
  1927. const defaultTextDisplayFactory = function() {
  1928. return new shaka.text.SimpleTextDisplayer(self.video_);
  1929. };
  1930. return {
  1931. drm: {
  1932. retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
  1933. // These will all be verified by special cases in mergeConfigObjects_():
  1934. servers: {}, // key is arbitrary key system ID, value must be string
  1935. clearKeys: {}, // key is arbitrary key system ID, value must be string
  1936. advanced: {}, // key is arbitrary key system ID, value is a record type
  1937. delayLicenseRequestUntilPlayed: false
  1938. },
  1939. manifest: {
  1940. retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
  1941. availabilityWindowOverride: NaN,
  1942. dash: {
  1943. customScheme: function(node) {
  1944. // Reference node to keep closure from removing it.
  1945. // If the argument is removed, it breaks our function length check
  1946. // in mergeConfigObjects_().
  1947. // TODO: Find a better solution if possible.
  1948. // NOTE: Chrome App Content Security Policy prohibits usage of new
  1949. // Function()
  1950. if (node) return null;
  1951. },
  1952. clockSyncUri: '',
  1953. ignoreDrmInfo: false,
  1954. xlinkFailGracefully: false,
  1955. defaultPresentationDelay: 10
  1956. }
  1957. },
  1958. streaming: {
  1959. retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
  1960. failureCallback:
  1961. this.defaultStreamingFailureCallback_.bind(this),
  1962. rebufferingGoal: 2,
  1963. bufferingGoal: 10,
  1964. bufferBehind: 30,
  1965. ignoreTextStreamFailures: false,
  1966. alwaysStreamText: false,
  1967. startAtSegmentBoundary: false,
  1968. smallGapLimit: 0.5,
  1969. jumpLargeGaps: false,
  1970. durationBackoff: 1,
  1971. forceTransmuxTS: false
  1972. },
  1973. abrFactory: shaka.abr.SimpleAbrManager,
  1974. textDisplayFactory: defaultTextDisplayFactory,
  1975. abr: {
  1976. enabled: true,
  1977. defaultBandwidthEstimate: bandwidthEstimate,
  1978. switchInterval: 8,
  1979. bandwidthUpgradeTarget: 0.85,
  1980. bandwidthDowngradeTarget: 0.95,
  1981. restrictions: {
  1982. minWidth: 0,
  1983. maxWidth: Infinity,
  1984. minHeight: 0,
  1985. maxHeight: abrMaxHeight,
  1986. minPixels: 0,
  1987. maxPixels: Infinity,
  1988. minBandwidth: 0,
  1989. maxBandwidth: Infinity
  1990. }
  1991. },
  1992. preferredAudioLanguage: '',
  1993. preferredTextLanguage: '',
  1994. preferredVariantRole: '',
  1995. preferredTextRole: '',
  1996. preferredAudioChannelCount: 2,
  1997. restrictions: {
  1998. minWidth: 0,
  1999. maxWidth: Infinity,
  2000. minHeight: 0,
  2001. maxHeight: Infinity,
  2002. minPixels: 0,
  2003. maxPixels: Infinity,
  2004. minBandwidth: 0,
  2005. maxBandwidth: Infinity
  2006. },
  2007. playRangeStart: 0,
  2008. playRangeEnd: Infinity
  2009. };
  2010. };
  2011. /**
  2012. * @param {!shaka.util.Error} error
  2013. * @private
  2014. */
  2015. shaka.Player.prototype.defaultStreamingFailureCallback_ = function(error) {
  2016. let retryErrorCodes = [
  2017. shaka.util.Error.Code.BAD_HTTP_STATUS,
  2018. shaka.util.Error.Code.HTTP_ERROR,
  2019. shaka.util.Error.Code.TIMEOUT
  2020. ];
  2021. if (this.isLive() && retryErrorCodes.indexOf(error.code) >= 0) {
  2022. error.severity = shaka.util.Error.Severity.RECOVERABLE;
  2023. shaka.log.warning('Live streaming error. Retrying automatically...');
  2024. this.retryStreaming();
  2025. }
  2026. };
  2027. /**
  2028. * @return {shakaExtern.Stats}
  2029. * @private
  2030. */
  2031. shaka.Player.prototype.getCleanStats_ = function() {
  2032. return {
  2033. // These are not tracked in the private stats structure and are only here to
  2034. // satisfy the compiler.
  2035. width: NaN,
  2036. height: NaN,
  2037. streamBandwidth: NaN,
  2038. decodedFrames: NaN,
  2039. droppedFrames: NaN,
  2040. estimatedBandwidth: NaN,
  2041. // These are tracked in the private stats structure to avoid the need for
  2042. // many private member variables.
  2043. loadLatency: NaN,
  2044. playTime: 0,
  2045. bufferingTime: 0,
  2046. switchHistory: [],
  2047. stateHistory: []
  2048. };
  2049. };
  2050. /**
  2051. * Filters a list of periods.
  2052. * @param {!Array.<!shakaExtern.Period>} periods
  2053. * @private
  2054. */
  2055. shaka.Player.prototype.filterAllPeriods_ = function(periods) {
  2056. goog.asserts.assert(this.video_, 'Must not be destroyed');
  2057. const ArrayUtils = shaka.util.ArrayUtils;
  2058. const StreamUtils = shaka.util.StreamUtils;
  2059. /** @type {?shakaExtern.Stream} */
  2060. let activeAudio =
  2061. this.streamingEngine_ ? this.streamingEngine_.getActiveAudio() : null;
  2062. /** @type {?shakaExtern.Stream} */
  2063. let activeVideo =
  2064. this.streamingEngine_ ? this.streamingEngine_.getActiveVideo() : null;
  2065. let filterPeriod = StreamUtils.filterNewPeriod.bind(
  2066. null, this.drmEngine_, activeAudio, activeVideo);
  2067. periods.forEach(filterPeriod);
  2068. let validPeriodsCount = ArrayUtils.count(periods, function(period) {
  2069. return period.variants.some(StreamUtils.isPlayable);
  2070. });
  2071. // If none of the periods are playable, throw CONTENT_UNSUPPORTED_BY_BROWSER.
  2072. if (validPeriodsCount == 0) {
  2073. throw new shaka.util.Error(
  2074. shaka.util.Error.Severity.CRITICAL,
  2075. shaka.util.Error.Category.MANIFEST,
  2076. shaka.util.Error.Code.CONTENT_UNSUPPORTED_BY_BROWSER);
  2077. }
  2078. // If only some of the periods are playable, throw UNPLAYABLE_PERIOD.
  2079. if (validPeriodsCount < periods.length) {
  2080. throw new shaka.util.Error(
  2081. shaka.util.Error.Severity.CRITICAL,
  2082. shaka.util.Error.Category.MANIFEST,
  2083. shaka.util.Error.Code.UNPLAYABLE_PERIOD);
  2084. }
  2085. periods.forEach(function(period) {
  2086. let tracksChanged = shaka.util.StreamUtils.applyRestrictions(
  2087. period, this.config_.restrictions, this.maxHwRes_);
  2088. if (tracksChanged && this.streamingEngine_ &&
  2089. this.streamingEngine_.getCurrentPeriod() == period) {
  2090. this.onTracksChanged_();
  2091. }
  2092. let hasValidVariant = period.variants.some(StreamUtils.isPlayable);
  2093. if (!hasValidVariant) {
  2094. throw new shaka.util.Error(
  2095. shaka.util.Error.Severity.CRITICAL,
  2096. shaka.util.Error.Category.MANIFEST,
  2097. shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET);
  2098. }
  2099. }.bind(this));
  2100. };
  2101. /**
  2102. * Filters a new period.
  2103. * @param {shakaExtern.Period} period
  2104. * @private
  2105. */
  2106. shaka.Player.prototype.filterNewPeriod_ = function(period) {
  2107. goog.asserts.assert(this.video_, 'Must not be destroyed');
  2108. const StreamUtils = shaka.util.StreamUtils;
  2109. /** @type {?shakaExtern.Stream} */
  2110. let activeAudio =
  2111. this.streamingEngine_ ? this.streamingEngine_.getActiveAudio() : null;
  2112. /** @type {?shakaExtern.Stream} */
  2113. let activeVideo =
  2114. this.streamingEngine_ ? this.streamingEngine_.getActiveVideo() : null;
  2115. StreamUtils.filterNewPeriod(
  2116. this.drmEngine_, activeAudio, activeVideo, period);
  2117. /** @type {!Array.<shakaExtern.Variant>} */
  2118. let variants = period.variants;
  2119. // Check for playable variants before restrictions, so that we can give a
  2120. // special error when there were tracks but they were all filtered.
  2121. let hasPlayableVariant = variants.some(StreamUtils.isPlayable);
  2122. let tracksChanged = shaka.util.StreamUtils.applyRestrictions(
  2123. period, this.config_.restrictions, this.maxHwRes_);
  2124. if (tracksChanged && this.streamingEngine_ &&
  2125. this.streamingEngine_.getCurrentPeriod() == period) {
  2126. this.onTracksChanged_();
  2127. }
  2128. // Check for playable variants again. If the first check found variants, but
  2129. // not the second, then all variants are restricted.
  2130. let hasAvailableVariant = variants.some(StreamUtils.isPlayable);
  2131. if (!hasPlayableVariant) {
  2132. throw new shaka.util.Error(
  2133. shaka.util.Error.Severity.CRITICAL,
  2134. shaka.util.Error.Category.MANIFEST,
  2135. shaka.util.Error.Code.UNPLAYABLE_PERIOD);
  2136. }
  2137. if (!hasAvailableVariant) {
  2138. throw new shaka.util.Error(
  2139. shaka.util.Error.Severity.CRITICAL,
  2140. shaka.util.Error.Category.MANIFEST,
  2141. shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET);
  2142. }
  2143. // For new Periods, we may need to create new sessions for any new init data.
  2144. const curDrmInfo = this.drmEngine_ ? this.drmEngine_.getDrmInfo() : null;
  2145. if (curDrmInfo) {
  2146. for (const variant of variants) {
  2147. for (const drmInfo of variant.drmInfos) {
  2148. // Ignore any data for different key systems.
  2149. if (drmInfo.keySystem == curDrmInfo.keySystem) {
  2150. for (const initData of (drmInfo.initData || [])) {
  2151. this.drmEngine_.newInitData(
  2152. initData.initDataType, initData.initData);
  2153. }
  2154. }
  2155. }
  2156. }
  2157. }
  2158. };
  2159. /**
  2160. * Switches to the given variant, deferring if needed.
  2161. * @param {shakaExtern.Variant} variant
  2162. * @param {boolean=} opt_clearBuffer
  2163. * @private
  2164. */
  2165. shaka.Player.prototype.switchVariant_ =
  2166. function(variant, opt_clearBuffer) {
  2167. if (this.switchingPeriods_) {
  2168. // Store this action for later.
  2169. this.deferredVariant_ = variant;
  2170. this.deferredVariantClearBuffer_ = opt_clearBuffer || false;
  2171. } else {
  2172. // Act now.
  2173. this.streamingEngine_.switchVariant(variant, opt_clearBuffer || false);
  2174. }
  2175. };
  2176. /**
  2177. * Switches to the given text stream, deferring if needed.
  2178. * @param {shakaExtern.Stream} textStream
  2179. * @private
  2180. */
  2181. shaka.Player.prototype.switchTextStream_ = function(textStream) {
  2182. if (this.switchingPeriods_) {
  2183. // Store this action for later.
  2184. this.deferredTextStream_ = textStream;
  2185. } else {
  2186. // Act now.
  2187. this.streamingEngine_.switchTextStream(textStream);
  2188. }
  2189. };
  2190. /**
  2191. * Verifies that the active streams according to the player match those in
  2192. * StreamingEngine.
  2193. * @private
  2194. */
  2195. shaka.Player.prototype.assertCorrectActiveStreams_ = function() {
  2196. if (!this.streamingEngine_ || !this.manifest_ || !goog.DEBUG) return;
  2197. const StreamUtils = shaka.util.StreamUtils;
  2198. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  2199. let activeAudio = this.streamingEngine_.getActiveAudio();
  2200. let activeVideo = this.streamingEngine_.getActiveVideo();
  2201. let activeText = this.streamingEngine_.getActiveText();
  2202. /** @type {?shakaExtern.Stream} */
  2203. let mainStream = activeVideo || activeAudio;
  2204. if (!mainStream) {
  2205. return;
  2206. }
  2207. let streamingPeriodIndex =
  2208. StreamUtils.findPeriodContainingStream(this.manifest_, mainStream);
  2209. let currentPeriodIndex =
  2210. this.manifest_.periods.indexOf(this.streamingEngine_.getCurrentPeriod());
  2211. if (streamingPeriodIndex < 0 || streamingPeriodIndex != currentPeriodIndex) {
  2212. return;
  2213. }
  2214. let playerActive = this.activeStreamsByPeriod_[currentPeriodIndex] || {};
  2215. let activeStreams = [activeAudio, activeVideo, activeText];
  2216. activeStreams.forEach(function(stream) {
  2217. if (stream) {
  2218. let id = stream.id;
  2219. let type = stream.type;
  2220. if (type == ContentType.TEXT && this.deferredTextStream_) {
  2221. id = this.deferredTextStream_.id;
  2222. }
  2223. if (type == ContentType.AUDIO && this.deferredVariant_) {
  2224. id = this.deferredVariant_.audio.id;
  2225. }
  2226. if (type == ContentType.VIDEO && this.deferredVariant_) {
  2227. id = this.deferredVariant_.video.id;
  2228. }
  2229. goog.asserts.assert(
  2230. id == playerActive[type], 'Inconsistent active stream');
  2231. }
  2232. }.bind(this));
  2233. };
  2234. /** @private */
  2235. shaka.Player.prototype.updateTimeStats_ = function() {
  2236. // Only count while we're loaded.
  2237. if (!this.manifest_) {
  2238. return;
  2239. }
  2240. let now = Date.now() / 1000;
  2241. if (this.buffering_) {
  2242. this.stats_.bufferingTime += (now - this.lastTimeStatsUpdateTimestamp_);
  2243. } else {
  2244. this.stats_.playTime += (now - this.lastTimeStatsUpdateTimestamp_);
  2245. }
  2246. this.lastTimeStatsUpdateTimestamp_ = now;
  2247. };
  2248. /**
  2249. * @param {number} time
  2250. * @return {number}
  2251. * @private
  2252. */
  2253. shaka.Player.prototype.adjustStartTime_ = function(time) {
  2254. /** @type {?shakaExtern.Stream} */
  2255. let activeAudio = this.streamingEngine_.getActiveAudio();
  2256. /** @type {?shakaExtern.Stream} */
  2257. let activeVideo = this.streamingEngine_.getActiveVideo();
  2258. /** @type {shakaExtern.Period} */
  2259. let period = this.streamingEngine_.getCurrentPeriod();
  2260. // This method is called after StreamingEngine.init resolves, which means that
  2261. // all the active streams have had createSegmentIndex called.
  2262. function getAdjustedTime(stream, time) {
  2263. if (!stream) return null;
  2264. let idx = stream.findSegmentPosition(time - period.startTime);
  2265. if (idx == null) return null;
  2266. let ref = stream.getSegmentReference(idx);
  2267. if (!ref) return null;
  2268. let refTime = ref.startTime + period.startTime;
  2269. goog.asserts.assert(refTime <= time, 'Segment should start before time');
  2270. return refTime;
  2271. }
  2272. let audioStartTime = getAdjustedTime(activeAudio, time);
  2273. let videoStartTime = getAdjustedTime(activeVideo, time);
  2274. // If we have both video and audio times, pick the larger one. If we picked
  2275. // the smaller one, that one will download an entire segment to buffer the
  2276. // difference.
  2277. if (videoStartTime != null && audioStartTime != null) {
  2278. return Math.max(videoStartTime, audioStartTime);
  2279. } else if (videoStartTime != null) {
  2280. return videoStartTime;
  2281. } else if (audioStartTime != null) {
  2282. return audioStartTime;
  2283. } else {
  2284. return time;
  2285. }
  2286. };
  2287. /**
  2288. * Callback from NetworkingEngine.
  2289. *
  2290. * @param {number} deltaTimeMs
  2291. * @param {number} numBytes
  2292. * @private
  2293. */
  2294. shaka.Player.prototype.onSegmentDownloaded_ = function(deltaTimeMs, numBytes) {
  2295. if (this.abrManager_) {
  2296. // Abr manager might not exist during offline storage.
  2297. this.abrManager_.segmentDownloaded(deltaTimeMs, numBytes);
  2298. }
  2299. };
  2300. /**
  2301. * Callback from PlayheadObserver.
  2302. *
  2303. * @param {boolean} buffering
  2304. * @private
  2305. */
  2306. shaka.Player.prototype.onBuffering_ = function(buffering) {
  2307. // Before setting |buffering_|, update the time spent in the previous state.
  2308. this.updateTimeStats_();
  2309. this.buffering_ = buffering;
  2310. this.updateState_();
  2311. if (this.playhead_) {
  2312. this.playhead_.setBuffering(buffering);
  2313. }
  2314. let event = new shaka.util.FakeEvent('buffering', {'buffering': buffering});
  2315. this.dispatchEvent(event);
  2316. };
  2317. /**
  2318. * Callback from PlayheadObserver.
  2319. * @private
  2320. */
  2321. shaka.Player.prototype.onChangePeriod_ = function() {
  2322. this.onTracksChanged_();
  2323. };
  2324. /**
  2325. * Called from potential initiators of state changes, or before returning stats
  2326. * to the user.
  2327. *
  2328. * This method decides if state has actually changed, updates the last entry,
  2329. * and adds a new one if needed.
  2330. *
  2331. * @private
  2332. */
  2333. shaka.Player.prototype.updateState_ = function() {
  2334. if (this.destroyed_) return;
  2335. let newState;
  2336. if (this.buffering_) {
  2337. newState = 'buffering';
  2338. } else if (this.video_.ended) {
  2339. newState = 'ended';
  2340. } else if (this.video_.paused) {
  2341. newState = 'paused';
  2342. } else {
  2343. newState = 'playing';
  2344. }
  2345. let now = Date.now() / 1000;
  2346. if (this.stats_.stateHistory.length) {
  2347. let lastIndex = this.stats_.stateHistory.length - 1;
  2348. let lastEntry = this.stats_.stateHistory[lastIndex];
  2349. lastEntry.duration = now - lastEntry.timestamp;
  2350. if (newState == lastEntry.state) {
  2351. // The state has not changed, so do not add anything to the history.
  2352. return;
  2353. }
  2354. }
  2355. this.stats_.stateHistory.push({
  2356. timestamp: now,
  2357. state: newState,
  2358. duration: 0
  2359. });
  2360. };
  2361. /**
  2362. * Callback from Playhead.
  2363. *
  2364. * @private
  2365. */
  2366. shaka.Player.prototype.onSeek_ = function() {
  2367. if (this.playheadObserver_) {
  2368. this.playheadObserver_.seeked();
  2369. }
  2370. if (this.streamingEngine_) {
  2371. this.streamingEngine_.seeked();
  2372. }
  2373. };
  2374. /**
  2375. * Chooses a variant through the ABR manager.
  2376. * On error, this dispatches an error event and returns null.
  2377. *
  2378. * @param {!Array.<shakaExtern.Variant>} variants
  2379. * @return {?shakaExtern.Variant}
  2380. * @private
  2381. */
  2382. shaka.Player.prototype.chooseVariant_ = function(variants) {
  2383. goog.asserts.assert(this.config_, 'Must not be destroyed');
  2384. if (!variants || !variants.length) {
  2385. this.onError_(new shaka.util.Error(
  2386. shaka.util.Error.Severity.CRITICAL,
  2387. shaka.util.Error.Category.MANIFEST,
  2388. shaka.util.Error.Code.RESTRICTIONS_CANNOT_BE_MET));
  2389. return null;
  2390. }
  2391. // Update the abr manager with newly filtered variants.
  2392. this.abrManager_.setVariants(variants);
  2393. return this.abrManager_.chooseVariant();
  2394. };
  2395. /**
  2396. * Chooses streams from the given Period and switches to them.
  2397. * Called after a config change, a new text stream, a key status event, or an
  2398. * explicit language change.
  2399. *
  2400. * @param {!shakaExtern.Period} period
  2401. * @private
  2402. */
  2403. shaka.Player.prototype.chooseStreamsAndSwitch_ = function(period) {
  2404. goog.asserts.assert(this.config_, 'Must not be destroyed');
  2405. let variants = shaka.util.StreamUtils.filterVariantsByConfig(
  2406. period.variants, this.currentAudioLanguage_, this.currentVariantRole_,
  2407. this.currentAudioChannelCount_);
  2408. let textStreams = shaka.util.StreamUtils.filterStreamsByLanguageAndRole(
  2409. period.textStreams, this.currentTextLanguage_, this.currentTextRole_);
  2410. // Because we're running this after a config change (manual language change),
  2411. // a new text stream, or a key status event, and because switching to an
  2412. // active stream is a no-op, it is always okay to clear the buffer here.
  2413. let chosenVariant = this.chooseVariant_(variants);
  2414. if (chosenVariant) {
  2415. this.addVariantToSwitchHistory_(chosenVariant, /* fromAdaptation */ true);
  2416. this.switchVariant_(chosenVariant, true);
  2417. }
  2418. // Only switch text if we should be streaming text right now.
  2419. let chosenText = textStreams[0];
  2420. if (chosenText && this.streamText_()) {
  2421. this.addTextStreamToSwitchHistory_(chosenText, /* fromAdaptation */ true);
  2422. this.switchTextStream_(chosenText);
  2423. }
  2424. // Send an adaptation event so that the UI can show the new language/tracks.
  2425. this.onAdaptation_();
  2426. };
  2427. /**
  2428. * Callback from StreamingEngine, invoked when a period starts.
  2429. *
  2430. * @param {!shakaExtern.Period} period
  2431. * @return {shaka.media.StreamingEngine.ChosenStreams} An object containing the
  2432. * chosen variant and text stream.
  2433. * @private
  2434. */
  2435. shaka.Player.prototype.onChooseStreams_ = function(period) {
  2436. shaka.log.debug('onChooseStreams_', period);
  2437. goog.asserts.assert(this.config_, 'Must not be destroyed');
  2438. const ContentType = shaka.util.ManifestParserUtils.ContentType;
  2439. const StreamUtils = shaka.util.StreamUtils;
  2440. // We are switching Periods, so the AbrManager will be disabled. But if we
  2441. // want to abr.enabled, we do not want to call AbrManager.enable before
  2442. // canSwitch_ is called.
  2443. this.switchingPeriods_ = true;
  2444. this.abrManager_.disable();
  2445. shaka.log.debug('Choosing new streams after period changed');
  2446. // Create empty object first and initialize the fields through
  2447. // [] to allow field names to be expressions.
  2448. // TODO: This feedback system for language matches could be cleaned up.
  2449. let languageMatches = {};
  2450. languageMatches[ContentType.AUDIO] = false;
  2451. languageMatches[ContentType.TEXT] = false;
  2452. let variants = StreamUtils.filterVariantsByConfig(
  2453. period.variants,
  2454. this.currentAudioLanguage_,
  2455. this.currentVariantRole_,
  2456. this.currentAudioChannelCount_,
  2457. languageMatches);
  2458. let textStreams = StreamUtils.filterStreamsByLanguageAndRole(
  2459. period.textStreams, this.currentTextLanguage_, this.currentTextRole_,
  2460. languageMatches);
  2461. shaka.log.v2('onChooseStreams_, variants and text streams: ',
  2462. variants, textStreams);
  2463. let chosenVariant = this.chooseVariant_(variants);
  2464. let chosenTextStream = textStreams[0] || null;
  2465. shaka.log.v2('onChooseStreams_, chosen=', chosenVariant, chosenTextStream);
  2466. // This assertion satisfies a compiler nullability check below.
  2467. goog.asserts.assert(this.manifest_, 'Manifest should exist!');
  2468. // Ignore deferred variant or text streams only if we are starting a new
  2469. // period. In this case, any deferred switches were from an older period, so
  2470. // they do not apply. We can still have deferred switches from the current
  2471. // period in the case of an early call to select*Track while we are setting up
  2472. // the first period. This can happen with the 'streaming' event.
  2473. if (this.deferredVariant_) {
  2474. const deferredPeriodIndex = StreamUtils.findPeriodContainingVariant(
  2475. this.manifest_, this.deferredVariant_);
  2476. const deferredPeriod = this.manifest_.periods[deferredPeriodIndex];
  2477. if (deferredPeriod == period) {
  2478. chosenVariant = this.deferredVariant_;
  2479. }
  2480. this.deferredVariant_ = null;
  2481. }
  2482. if (this.deferredTextStream_) {
  2483. const deferredPeriodIndex = StreamUtils.findPeriodContainingStream(
  2484. this.manifest_, this.deferredTextStream_);
  2485. const deferredPeriod = this.manifest_.periods[deferredPeriodIndex];
  2486. if (deferredPeriod == period) {
  2487. chosenTextStream = this.deferredTextStream_;
  2488. }
  2489. this.deferredTextStream_ = null;
  2490. }
  2491. if (chosenVariant) {
  2492. this.addVariantToSwitchHistory_(chosenVariant, /* fromAdaptation */ true);
  2493. }
  2494. if (chosenTextStream) {
  2495. this.addTextStreamToSwitchHistory_(
  2496. chosenTextStream, /* fromAdaptation */ true);
  2497. // If audio and text tracks have different languages, and the text track
  2498. // matches the user's preference, then show the captions. Only do this
  2499. // when we are choosing the initial tracks during startup.
  2500. let startingUp = !this.streamingEngine_.getActivePeriod();
  2501. if (startingUp) {
  2502. if (chosenVariant && chosenVariant.audio &&
  2503. languageMatches[ContentType.TEXT] &&
  2504. chosenTextStream.language != chosenVariant.audio.language) {
  2505. this.textDisplayer_.setTextVisibility(true);
  2506. this.onTextTrackVisibility_();
  2507. }
  2508. }
  2509. }
  2510. // Don't fire a tracks-changed event since we aren't inside the new Period
  2511. // yet.
  2512. // Don't initialize with a text stream unless we should be streaming text.
  2513. if (this.streamText_()) {
  2514. return {variant: chosenVariant, text: chosenTextStream};
  2515. } else {
  2516. return {variant: chosenVariant, text: null};
  2517. }
  2518. };
  2519. /**
  2520. * Callback from StreamingEngine, invoked when the period is set up.
  2521. *
  2522. * @private
  2523. */
  2524. shaka.Player.prototype.canSwitch_ = function() {
  2525. shaka.log.debug('canSwitch_');
  2526. goog.asserts.assert(this.config_, 'Must not be destroyed');
  2527. this.switchingPeriods_ = false;
  2528. if (this.config_.abr.enabled) {
  2529. this.abrManager_.enable();
  2530. }
  2531. // If we still have deferred switches, switch now.
  2532. if (this.deferredVariant_) {
  2533. this.streamingEngine_.switchVariant(
  2534. this.deferredVariant_, this.deferredVariantClearBuffer_);
  2535. this.deferredVariant_ = null;
  2536. }
  2537. if (this.deferredTextStream_) {
  2538. this.streamingEngine_.switchTextStream(this.deferredTextStream_);
  2539. this.deferredTextStream_ = null;
  2540. }
  2541. };
  2542. /**
  2543. * Callback from StreamingEngine.
  2544. *
  2545. * @private
  2546. */
  2547. shaka.Player.prototype.onManifestUpdate_ = function() {
  2548. if (this.parser_ && this.parser_.update) {
  2549. this.parser_.update();
  2550. }
  2551. };
  2552. /**
  2553. * Callback from StreamingEngine.
  2554. *
  2555. * @private
  2556. */
  2557. shaka.Player.prototype.onSegmentAppended_ = function() {
  2558. if (this.playhead_) {
  2559. this.playhead_.onSegmentAppended();
  2560. }
  2561. };
  2562. /**
  2563. * Callback from AbrManager.
  2564. *
  2565. * @param {shakaExtern.Variant} variant
  2566. * @param {boolean=} opt_clearBuffer
  2567. * @private
  2568. */
  2569. shaka.Player.prototype.switch_ = function(variant, opt_clearBuffer) {
  2570. shaka.log.debug('switch_');
  2571. goog.asserts.assert(this.config_.abr.enabled,
  2572. 'AbrManager should not call switch while disabled!');
  2573. goog.asserts.assert(!this.switchingPeriods_,
  2574. 'AbrManager should not call switch while transitioning between Periods!');
  2575. this.addVariantToSwitchHistory_(variant, /* fromAdaptation */ true);
  2576. if (!this.streamingEngine_) {
  2577. // There's no way to change it.
  2578. return;
  2579. }
  2580. this.streamingEngine_.switchVariant(variant, opt_clearBuffer || false);
  2581. this.onAdaptation_();
  2582. };
  2583. /**
  2584. * Dispatches an 'adaptation' event.
  2585. * @private
  2586. */
  2587. shaka.Player.prototype.onAdaptation_ = function() {
  2588. // In the next frame, dispatch an 'adaptation' event.
  2589. // This gives StreamingEngine time to absorb the changes before the user
  2590. // tries to query them.
  2591. Promise.resolve().then(function() {
  2592. if (this.destroyed_) return;
  2593. let event = new shaka.util.FakeEvent('adaptation');
  2594. this.dispatchEvent(event);
  2595. }.bind(this));
  2596. };
  2597. /**
  2598. * Dispatches a 'trackschanged' event.
  2599. * @private
  2600. */
  2601. shaka.Player.prototype.onTracksChanged_ = function() {
  2602. // In the next frame, dispatch a 'trackschanged' event.
  2603. // This gives StreamingEngine time to absorb the changes before the user
  2604. // tries to query them.
  2605. Promise.resolve().then(function() {
  2606. if (this.destroyed_) return;
  2607. let event = new shaka.util.FakeEvent('trackschanged');
  2608. this.dispatchEvent(event);
  2609. }.bind(this));
  2610. };
  2611. /** @private */
  2612. shaka.Player.prototype.onTextTrackVisibility_ = function() {
  2613. let event = new shaka.util.FakeEvent('texttrackvisibility');
  2614. this.dispatchEvent(event);
  2615. };
  2616. /**
  2617. * @param {!shaka.util.Error} error
  2618. * @private
  2619. */
  2620. shaka.Player.prototype.onError_ = function(error) {
  2621. // Errors dispatched after destroy is called are irrelevant.
  2622. if (this.destroyed_) return;
  2623. goog.asserts.assert(error instanceof shaka.util.Error, 'Wrong error type!');
  2624. let event = new shaka.util.FakeEvent('error', {'detail': error});
  2625. this.dispatchEvent(event);
  2626. if (event.defaultPrevented) {
  2627. error.handled = true;
  2628. }
  2629. };
  2630. /**
  2631. * @param {shakaExtern.TimelineRegionInfo} region
  2632. * @private
  2633. */
  2634. shaka.Player.prototype.onTimelineRegionAdded_ = function(region) {
  2635. if (this.playheadObserver_) {
  2636. this.playheadObserver_.addTimelineRegion(region);
  2637. } else {
  2638. this.pendingTimelineRegions_.push(region);
  2639. }
  2640. };
  2641. /**
  2642. * @param {!Event} event
  2643. * @private
  2644. */
  2645. shaka.Player.prototype.onEvent_ = function(event) {
  2646. this.dispatchEvent(event);
  2647. };
  2648. /**
  2649. * @param {!Event} event
  2650. * @private
  2651. */
  2652. shaka.Player.prototype.onVideoError_ = function(event) {
  2653. if (!this.video_.error) return;
  2654. let code = this.video_.error.code;
  2655. if (code == 1 /* MEDIA_ERR_ABORTED */) {
  2656. // Ignore this error code, which should only occur when navigating away or
  2657. // deliberately stopping playback of HTTP content.
  2658. return;
  2659. }
  2660. // Extra error information from MS Edge and IE11:
  2661. let extended = this.video_.error.msExtendedCode;
  2662. if (extended) {
  2663. // Convert to unsigned:
  2664. if (extended < 0) {
  2665. extended += Math.pow(2, 32);
  2666. }
  2667. // Format as hex:
  2668. extended = extended.toString(16);
  2669. }
  2670. // Extra error information from Chrome:
  2671. let message = this.video_.error.message;
  2672. this.onError_(new shaka.util.Error(
  2673. shaka.util.Error.Severity.CRITICAL,
  2674. shaka.util.Error.Category.MEDIA,
  2675. shaka.util.Error.Code.VIDEO_ERROR,
  2676. code, extended, message));
  2677. };
  2678. /**
  2679. * @param {!Object.<string, string>} keyStatusMap A map of hex key IDs to
  2680. * statuses.
  2681. * @private
  2682. */
  2683. shaka.Player.prototype.onKeyStatus_ = function(keyStatusMap) {
  2684. goog.asserts.assert(this.streamingEngine_, 'Should have been initialized.');
  2685. // 'usable', 'released', 'output-downscaled', 'status-pending' are statuses
  2686. // of the usable keys.
  2687. // 'expired' status is being handled separately in DrmEngine.
  2688. let restrictedStatuses = ['output-restricted', 'internal-error'];
  2689. let period = this.streamingEngine_.getCurrentPeriod();
  2690. let tracksChanged = false;
  2691. let keyIds = Object.keys(keyStatusMap);
  2692. if (keyIds.length == 0) {
  2693. shaka.log.warning(
  2694. 'Got a key status event without any key statuses, so we don\'t know ' +
  2695. 'the real key statuses. If we don\'t have all the keys, you\'ll need ' +
  2696. 'to set restrictions so we don\'t select those tracks.');
  2697. }
  2698. // If EME is using a synthetic key ID, the only key ID is '00' (a single 0
  2699. // byte). In this case, it is only used to report global success/failure.
  2700. // See note about old platforms in: https://goo.gl/KtQMja
  2701. let isGlobalStatus = keyIds.length == 1 && keyIds[0] == '00';
  2702. if (isGlobalStatus) {
  2703. shaka.log.warning(
  2704. 'Got a synthetic key status event, so we don\'t know the real key ' +
  2705. 'statuses. If we don\'t have all the keys, you\'ll need to set ' +
  2706. 'restrictions so we don\'t select those tracks.');
  2707. }
  2708. // Only filter tracks for keys if we have some key statuses to look at.
  2709. if (keyIds.length) {
  2710. period.variants.forEach(function(variant) {
  2711. let streams = [];
  2712. if (variant.audio) streams.push(variant.audio);
  2713. if (variant.video) streams.push(variant.video);
  2714. streams.forEach(function(stream) {
  2715. let originalAllowed = variant.allowedByKeySystem;
  2716. // Only update if we have a key ID for the stream.
  2717. // If the key isn't present, then we don't have that key and the track
  2718. // should be restricted.
  2719. if (stream.keyId) {
  2720. let keyStatus = keyStatusMap[isGlobalStatus ? '00' : stream.keyId];
  2721. variant.allowedByKeySystem =
  2722. !!keyStatus && restrictedStatuses.indexOf(keyStatus) < 0;
  2723. }
  2724. if (originalAllowed != variant.allowedByKeySystem) {
  2725. tracksChanged = true;
  2726. }
  2727. }); // streams.forEach
  2728. }); // period.variants.forEach
  2729. } // if (keyIds.length)
  2730. // TODO: Get StreamingEngine to track variants and create getActiveVariant()
  2731. let activeAudio = this.streamingEngine_.getActiveAudio();
  2732. let activeVideo = this.streamingEngine_.getActiveVideo();
  2733. let activeVariant = shaka.util.StreamUtils.getVariantByStreams(
  2734. activeAudio, activeVideo, period.variants);
  2735. if (activeVariant && !activeVariant.allowedByKeySystem) {
  2736. shaka.log.debug('Choosing new streams after key status changed');
  2737. this.chooseStreamsAndSwitch_(period);
  2738. }
  2739. if (tracksChanged) {
  2740. this.onTracksChanged_();
  2741. // Update AbrManager about any restricted/un-restricted variants.
  2742. let variants = shaka.util.StreamUtils.filterVariantsByConfig(
  2743. period.variants,
  2744. this.currentAudioLanguage_,
  2745. this.currentVariantRole_,
  2746. this.currentAudioChannelCount_);
  2747. this.abrManager_.setVariants(variants);
  2748. }
  2749. };
  2750. /**
  2751. * Callback from DrmEngine
  2752. * @param {string} keyId
  2753. * @param {number} expiration
  2754. * @private
  2755. */
  2756. shaka.Player.prototype.onExpirationUpdated_ = function(keyId, expiration) {
  2757. if (this.parser_ && this.parser_.onExpirationUpdated) {
  2758. this.parser_.onExpirationUpdated(keyId, expiration);
  2759. }
  2760. let event = new shaka.util.FakeEvent('expirationupdated');
  2761. this.dispatchEvent(event);
  2762. };
  2763. /**
  2764. * @return {boolean} true if we should stream text right now.
  2765. * @private
  2766. */
  2767. shaka.Player.prototype.streamText_ = function() {
  2768. return this.config_.streaming.alwaysStreamText || this.isTextTrackVisible();
  2769. };