Source: lib/abr/simple_abr_manager.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.abr.SimpleAbrManager');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.abr.EwmaBandwidthEstimator');
  9. goog.require('shaka.log');
  10. goog.require('shaka.util.EventManager');
  11. goog.require('shaka.util.IReleasable');
  12. goog.require('shaka.util.StreamUtils');
  13. goog.require('shaka.util.Timer');
  14. goog.requireType('shaka.util.CmsdManager');
  15. /**
  16. * @summary
  17. * <p>
  18. * This defines the default ABR manager for the Player. An instance of this
  19. * class is used when no ABR manager is given.
  20. * </p>
  21. * <p>
  22. * The behavior of this class is to take throughput samples using
  23. * segmentDownloaded to estimate the current network bandwidth. Then it will
  24. * use that to choose the streams that best fit the current bandwidth. It will
  25. * always pick the highest bandwidth variant it thinks can be played.
  26. * </p>
  27. * <p>
  28. * After initial choices are made, this class will call switchCallback() when
  29. * there is a better choice. switchCallback() will not be called more than once
  30. * per ({@link shaka.abr.SimpleAbrManager.SWITCH_INTERVAL_MS}).
  31. * </p>
  32. *
  33. * @implements {shaka.extern.AbrManager}
  34. * @implements {shaka.util.IReleasable}
  35. * @export
  36. */
  37. shaka.abr.SimpleAbrManager = class {
  38. /** */
  39. constructor() {
  40. /** @private {?shaka.extern.AbrManager.SwitchCallback} */
  41. this.switch_ = null;
  42. /** @private {boolean} */
  43. this.enabled_ = false;
  44. /** @private {shaka.abr.EwmaBandwidthEstimator} */
  45. this.bandwidthEstimator_ = new shaka.abr.EwmaBandwidthEstimator();
  46. /** @private {!shaka.util.EventManager} */
  47. this.eventManager_ = new shaka.util.EventManager();
  48. // Some browsers implement the Network Information API, which allows
  49. // retrieving information about a user's network connection. We listen
  50. // to the change event to be able to make quick changes in case the type
  51. // of connectivity changes.
  52. if (navigator.connection && navigator.connection.addEventListener) {
  53. this.eventManager_.listen(
  54. /** @type {EventTarget} */(navigator.connection),
  55. 'change',
  56. () => {
  57. if (this.enabled_ && this.config_.useNetworkInformation) {
  58. this.bandwidthEstimator_ = new shaka.abr.EwmaBandwidthEstimator();
  59. if (this.config_) {
  60. this.bandwidthEstimator_.configure(this.config_.advanced);
  61. }
  62. const chosenVariant = this.chooseVariant();
  63. if (chosenVariant && navigator.onLine) {
  64. this.switch_(chosenVariant, this.config_.clearBufferSwitch,
  65. this.config_.safeMarginSwitch);
  66. }
  67. }
  68. });
  69. }
  70. /**
  71. * A filtered list of Variants to choose from.
  72. * @private {!Array<!shaka.extern.Variant>}
  73. */
  74. this.variants_ = [];
  75. /** @private {number} */
  76. this.playbackRate_ = 1;
  77. /** @private {boolean} */
  78. this.startupComplete_ = false;
  79. /**
  80. * The last wall-clock time, in milliseconds, when streams were chosen.
  81. *
  82. * @private {?number}
  83. */
  84. this.lastTimeChosenMs_ = null;
  85. /** @private {?shaka.extern.AbrConfiguration} */
  86. this.config_ = null;
  87. /** @private {HTMLMediaElement} */
  88. this.mediaElement_ = null;
  89. /** @private {ResizeObserver} */
  90. this.resizeObserver_ = null;
  91. /** @private {shaka.util.Timer} */
  92. this.resizeObserverTimer_ = new shaka.util.Timer(() => {
  93. if (this.enabled_ && (this.config_.restrictToElementSize ||
  94. this.config_.restrictToScreenSize)) {
  95. const chosenVariant = this.chooseVariant();
  96. if (chosenVariant) {
  97. this.switch_(chosenVariant, this.config_.clearBufferSwitch,
  98. this.config_.safeMarginSwitch);
  99. }
  100. }
  101. });
  102. /** @private {Window} */
  103. this.windowToCheck_ = window;
  104. if ('documentPictureInPicture' in window) {
  105. this.eventManager_.listen(
  106. window.documentPictureInPicture, 'enter', () => {
  107. this.windowToCheck_ = window.documentPictureInPicture.window;
  108. if (this.resizeObserverTimer_) {
  109. this.resizeObserverTimer_.tickNow();
  110. }
  111. this.eventManager_.listenOnce(
  112. this.windowToCheck_, 'pagehide', () => {
  113. this.windowToCheck_ = window;
  114. if (this.resizeObserverTimer_) {
  115. this.resizeObserverTimer_.tickNow();
  116. }
  117. });
  118. });
  119. }
  120. /** @private {PictureInPictureWindow} */
  121. this.pictureInPictureWindow_ = null;
  122. /** @private {?shaka.util.CmsdManager} */
  123. this.cmsdManager_ = null;
  124. }
  125. /**
  126. * @override
  127. * @export
  128. */
  129. stop() {
  130. this.switch_ = null;
  131. this.enabled_ = false;
  132. this.variants_ = [];
  133. this.playbackRate_ = 1;
  134. this.lastTimeChosenMs_ = null;
  135. this.mediaElement_ = null;
  136. if (this.resizeObserver_) {
  137. this.resizeObserver_.disconnect();
  138. this.resizeObserver_ = null;
  139. }
  140. this.resizeObserverTimer_.stop();
  141. this.pictureInPictureWindow_ = null;
  142. this.cmsdManager_ = null;
  143. // Don't reset |startupComplete_|: if we've left the startup interval, we
  144. // can start using bandwidth estimates right away after init() is called.
  145. }
  146. /**
  147. * @override
  148. * @export
  149. */
  150. release() {
  151. // stop() should already have been called for unload
  152. this.eventManager_.release();
  153. this.resizeObserverTimer_ = null;
  154. }
  155. /**
  156. * @override
  157. * @export
  158. */
  159. init(switchCallback) {
  160. this.switch_ = switchCallback;
  161. }
  162. /**
  163. * @return {shaka.extern.Variant}
  164. * @override
  165. * @export
  166. */
  167. chooseVariant() {
  168. let maxHeight = Infinity;
  169. let maxWidth = Infinity;
  170. if (this.config_.restrictToScreenSize) {
  171. const devicePixelRatio = this.config_.ignoreDevicePixelRatio ?
  172. 1 : this.windowToCheck_.devicePixelRatio;
  173. maxHeight = this.windowToCheck_.screen.height * devicePixelRatio;
  174. maxWidth = this.windowToCheck_.screen.width * devicePixelRatio;
  175. }
  176. if (this.resizeObserver_ && this.config_.restrictToElementSize) {
  177. const devicePixelRatio = this.config_.ignoreDevicePixelRatio ?
  178. 1 : this.windowToCheck_.devicePixelRatio;
  179. let height = this.mediaElement_.clientHeight;
  180. let width = this.mediaElement_.clientWidth;
  181. if (this.pictureInPictureWindow_ && document.pictureInPictureElement &&
  182. document.pictureInPictureElement == this.mediaElement_) {
  183. height = this.pictureInPictureWindow_.height;
  184. width = this.pictureInPictureWindow_.width;
  185. }
  186. maxHeight = Math.min(maxHeight, height * devicePixelRatio);
  187. maxWidth = Math.min(maxWidth, width * devicePixelRatio);
  188. }
  189. let normalVariants = this.variants_.filter((variant) => {
  190. return variant && !shaka.util.StreamUtils.isFastSwitching(variant);
  191. });
  192. if (!normalVariants.length) {
  193. normalVariants = this.variants_;
  194. }
  195. let variants = normalVariants;
  196. if (normalVariants.length != this.variants_.length) {
  197. variants = this.variants_.filter((variant) => {
  198. return variant && shaka.util.StreamUtils.isFastSwitching(variant);
  199. });
  200. }
  201. // Get sorted Variants.
  202. let sortedVariants = this.filterAndSortVariants_(
  203. this.config_.restrictions, variants,
  204. /* maxHeight= */ Infinity, /* maxWidth= */ Infinity);
  205. if (maxHeight != Infinity || maxWidth != Infinity) {
  206. const resolutions = this.getResolutionList_(sortedVariants);
  207. for (const resolution of resolutions) {
  208. if (resolution.height >= maxHeight && resolution.width >= maxWidth) {
  209. maxHeight = resolution.height;
  210. maxWidth = resolution.width;
  211. break;
  212. }
  213. }
  214. sortedVariants = this.filterAndSortVariants_(
  215. this.config_.restrictions, variants, maxHeight, maxWidth);
  216. }
  217. const currentBandwidth = this.getBandwidthEstimate();
  218. if (variants.length && !sortedVariants.length) {
  219. // If we couldn't meet the ABR restrictions, we should still play
  220. // something.
  221. // These restrictions are not "hard" restrictions in the way that
  222. // top-level or DRM-based restrictions are. Sort the variants without
  223. // restrictions and keep just the first (lowest-bandwidth) one.
  224. shaka.log.warning('No variants met the ABR restrictions. ' +
  225. 'Choosing a variant by lowest bandwidth.');
  226. sortedVariants = this.filterAndSortVariants_(
  227. /* restrictions= */ null, variants,
  228. /* maxHeight= */ Infinity, /* maxWidth= */ Infinity);
  229. sortedVariants = [sortedVariants[0]];
  230. }
  231. // Start by assuming that we will use the first Stream.
  232. let chosen = sortedVariants[0] || null;
  233. for (let i = 0; i < sortedVariants.length; i++) {
  234. const item = sortedVariants[i];
  235. const playbackRate =
  236. !isNaN(this.playbackRate_) ? Math.abs(this.playbackRate_) : 1;
  237. const itemBandwidth = playbackRate * item.bandwidth;
  238. const minBandwidth =
  239. itemBandwidth / this.config_.bandwidthDowngradeTarget;
  240. let next = {bandwidth: Infinity};
  241. for (let j = i + 1; j < sortedVariants.length; j++) {
  242. if (item.bandwidth != sortedVariants[j].bandwidth) {
  243. next = sortedVariants[j];
  244. break;
  245. }
  246. }
  247. const nextBandwidth = playbackRate * next.bandwidth;
  248. const maxBandwidth = nextBandwidth / this.config_.bandwidthUpgradeTarget;
  249. shaka.log.v2('Bandwidth ranges:',
  250. (itemBandwidth / 1e6).toFixed(3),
  251. (minBandwidth / 1e6).toFixed(3),
  252. (maxBandwidth / 1e6).toFixed(3));
  253. if (currentBandwidth >= minBandwidth &&
  254. currentBandwidth <= maxBandwidth &&
  255. (chosen.bandwidth != item.bandwidth ||
  256. this.isSameBandwidthAndHigherResolution_(chosen, item))) {
  257. chosen = item;
  258. }
  259. }
  260. this.lastTimeChosenMs_ = Date.now();
  261. return chosen;
  262. }
  263. /**
  264. * @override
  265. * @export
  266. */
  267. enable() {
  268. this.enabled_ = true;
  269. }
  270. /**
  271. * @override
  272. * @export
  273. */
  274. disable() {
  275. this.enabled_ = false;
  276. }
  277. /**
  278. * @param {number} deltaTimeMs The duration, in milliseconds, that the request
  279. * took to complete.
  280. * @param {number} numBytes The total number of bytes transferred.
  281. * @param {boolean} allowSwitch Indicate if the segment is allowed to switch
  282. * to another stream.
  283. * @param {shaka.extern.Request=} request
  284. * A reference to the request
  285. * @override
  286. * @export
  287. */
  288. segmentDownloaded(deltaTimeMs, numBytes, allowSwitch, request) {
  289. if (deltaTimeMs < this.config_.cacheLoadThreshold) {
  290. // The time indicates that it could be a cache response, so we should
  291. // ignore this value.
  292. return;
  293. }
  294. shaka.log.v2('Segment downloaded:',
  295. 'contentType=' + (request && request.contentType),
  296. 'deltaTimeMs=' + deltaTimeMs,
  297. 'numBytes=' + numBytes,
  298. 'lastTimeChosenMs=' + this.lastTimeChosenMs_,
  299. 'enabled=' + this.enabled_);
  300. goog.asserts.assert(deltaTimeMs >= 0, 'expected a non-negative duration');
  301. this.bandwidthEstimator_.sample(deltaTimeMs, numBytes);
  302. if (allowSwitch && (this.lastTimeChosenMs_ != null) && this.enabled_) {
  303. this.suggestStreams_();
  304. }
  305. }
  306. /**
  307. * @override
  308. * @export
  309. */
  310. trySuggestStreams() {
  311. if ((this.lastTimeChosenMs_ != null) && this.enabled_) {
  312. this.suggestStreams_();
  313. }
  314. }
  315. /**
  316. * @override
  317. * @export
  318. */
  319. getBandwidthEstimate() {
  320. const defaultBandwidthEstimate = this.getDefaultBandwidth_();
  321. if (navigator.connection && navigator.connection.downlink &&
  322. this.config_.useNetworkInformation &&
  323. this.config_.preferNetworkInformationBandwidth) {
  324. return defaultBandwidthEstimate;
  325. }
  326. const bandwidthEstimate = this.bandwidthEstimator_.getBandwidthEstimate(
  327. defaultBandwidthEstimate);
  328. if (this.cmsdManager_) {
  329. return this.cmsdManager_.getBandwidthEstimate(bandwidthEstimate);
  330. }
  331. return bandwidthEstimate;
  332. }
  333. /**
  334. * @override
  335. * @export
  336. */
  337. setVariants(variants) {
  338. this.variants_ = variants;
  339. }
  340. /**
  341. * @override
  342. * @export
  343. */
  344. playbackRateChanged(rate) {
  345. this.playbackRate_ = rate;
  346. }
  347. /**
  348. * @override
  349. * @export
  350. */
  351. setMediaElement(mediaElement) {
  352. this.mediaElement_ = mediaElement;
  353. if (this.resizeObserver_) {
  354. this.resizeObserver_.disconnect();
  355. this.resizeObserver_ = null;
  356. }
  357. const onResize = () => {
  358. const SimpleAbrManager = shaka.abr.SimpleAbrManager;
  359. // Batch up resize changes before checking them.
  360. this.resizeObserverTimer_.tickAfter(
  361. /* seconds= */ SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME);
  362. };
  363. if (this.mediaElement_ && 'ResizeObserver' in window) {
  364. this.resizeObserver_ = new ResizeObserver(onResize);
  365. this.resizeObserver_.observe(this.mediaElement_);
  366. }
  367. this.eventManager_.listen(mediaElement, 'enterpictureinpicture', (e) => {
  368. const event = /** @type {PictureInPictureEvent} */(e);
  369. if (event.pictureInPictureWindow) {
  370. this.pictureInPictureWindow_ = event.pictureInPictureWindow;
  371. this.eventManager_.listen(
  372. this.pictureInPictureWindow_, 'resize', onResize);
  373. }
  374. });
  375. this.eventManager_.listen(mediaElement, 'leavepictureinpicture', () => {
  376. if (this.pictureInPictureWindow_) {
  377. this.eventManager_.unlisten(
  378. this.pictureInPictureWindow_, 'resize', onResize);
  379. }
  380. this.pictureInPictureWindow_ = null;
  381. });
  382. }
  383. /**
  384. * @override
  385. * @export
  386. */
  387. setCmsdManager(cmsdManager) {
  388. this.cmsdManager_ = cmsdManager;
  389. }
  390. /**
  391. * @override
  392. * @export
  393. */
  394. configure(config) {
  395. this.config_ = config;
  396. if (this.bandwidthEstimator_ && this.config_) {
  397. this.bandwidthEstimator_.configure(this.config_.advanced);
  398. }
  399. }
  400. /**
  401. * Calls switch_() with the variant chosen by chooseVariant().
  402. *
  403. * @private
  404. */
  405. suggestStreams_() {
  406. shaka.log.v2('Suggesting Streams...');
  407. goog.asserts.assert(this.lastTimeChosenMs_ != null,
  408. 'lastTimeChosenMs_ should not be null');
  409. if (!this.startupComplete_) {
  410. // Check if we've got enough data yet.
  411. if (!this.bandwidthEstimator_.hasGoodEstimate()) {
  412. shaka.log.v2('Still waiting for a good estimate...');
  413. return;
  414. }
  415. this.startupComplete_ = true;
  416. this.lastTimeChosenMs_ -=
  417. (this.config_.switchInterval - this.config_.minTimeToSwitch) * 1000;
  418. }
  419. // Check if we've left the switch interval.
  420. const now = Date.now();
  421. const delta = now - this.lastTimeChosenMs_;
  422. if (delta < this.config_.switchInterval * 1000) {
  423. shaka.log.v2('Still within switch interval...');
  424. return;
  425. }
  426. const chosenVariant = this.chooseVariant();
  427. const bandwidthEstimate = this.getBandwidthEstimate();
  428. const currentBandwidthKbps = Math.round(bandwidthEstimate / 1000.0);
  429. if (chosenVariant) {
  430. shaka.log.debug(
  431. 'Calling switch_(), bandwidth=' + currentBandwidthKbps + ' kbps');
  432. // If any of these chosen streams are already chosen, Player will filter
  433. // them out before passing the choices on to StreamingEngine.
  434. this.switch_(chosenVariant, this.config_.clearBufferSwitch,
  435. this.config_.safeMarginSwitch);
  436. }
  437. }
  438. /**
  439. * @private
  440. */
  441. getDefaultBandwidth_() {
  442. let defaultBandwidthEstimate = this.config_.defaultBandwidthEstimate;
  443. // Some browsers implement the Network Information API, which allows
  444. // retrieving information about a user's network connection. Tizen 3 has
  445. // NetworkInformation, but not the downlink attribute.
  446. if (navigator.connection && navigator.connection.downlink &&
  447. this.config_.useNetworkInformation) {
  448. // If it's available, get the bandwidth estimate from the browser (in
  449. // megabits per second) and use it as defaultBandwidthEstimate.
  450. defaultBandwidthEstimate = navigator.connection.downlink * 1e6;
  451. }
  452. return defaultBandwidthEstimate;
  453. }
  454. /**
  455. * @param {?shaka.extern.Restrictions} restrictions
  456. * @param {!Array<shaka.extern.Variant>} variants
  457. * @param {!number} maxHeight
  458. * @param {!number} maxWidth
  459. * @return {!Array<shaka.extern.Variant>} variants filtered according to
  460. * |restrictions| and sorted in ascending order of bandwidth.
  461. * @private
  462. */
  463. filterAndSortVariants_(restrictions, variants, maxHeight, maxWidth) {
  464. if (this.cmsdManager_) {
  465. const maxBitrate = this.cmsdManager_.getMaxBitrate();
  466. if (maxBitrate) {
  467. variants = variants.filter((variant) => {
  468. if (!variant.bandwidth || !maxBitrate) {
  469. return true;
  470. }
  471. return variant.bandwidth <= maxBitrate;
  472. });
  473. }
  474. }
  475. if (restrictions) {
  476. variants = variants.filter((variant) => {
  477. // This was already checked in another scope, but the compiler doesn't
  478. // seem to understand that.
  479. goog.asserts.assert(restrictions, 'Restrictions should exist!');
  480. return shaka.util.StreamUtils.meetsRestrictions(
  481. variant, restrictions,
  482. /* maxHwRes= */ {width: maxWidth, height: maxHeight});
  483. });
  484. }
  485. return variants.sort((v1, v2) => {
  486. return v1.bandwidth - v2.bandwidth;
  487. });
  488. }
  489. /**
  490. * @param {!Array<shaka.extern.Variant>} variants
  491. * @return {!Array<{height: number, width: number}>}
  492. * @private
  493. */
  494. getResolutionList_(variants) {
  495. const resolutions = [];
  496. for (const variant of variants) {
  497. const video = variant.video;
  498. if (!video || !video.height || !video.width) {
  499. continue;
  500. }
  501. resolutions.push({
  502. height: video.height,
  503. width: video.width,
  504. });
  505. }
  506. return resolutions.sort((v1, v2) => {
  507. return v1.width - v2.width;
  508. });
  509. }
  510. /**
  511. * @param {shaka.extern.Variant} chosenVariant
  512. * @param {shaka.extern.Variant} newVariant
  513. * @return {boolean}
  514. * @private
  515. */
  516. isSameBandwidthAndHigherResolution_(chosenVariant, newVariant) {
  517. if (chosenVariant.bandwidth != newVariant.bandwidth) {
  518. return false;
  519. }
  520. if (!chosenVariant.video || !newVariant.video) {
  521. return false;
  522. }
  523. return chosenVariant.video.width < newVariant.video.width ||
  524. chosenVariant.video.height < newVariant.video.height;
  525. }
  526. };
  527. /**
  528. * The amount of time, in seconds, we wait to batch up rapid resize changes.
  529. * This allows us to avoid multiple resize events in most cases.
  530. * @type {number}
  531. */
  532. shaka.abr.SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME = 1;