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