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