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.config_.restrictToElementSize) {
  94. const chosenVariant = this.chooseVariant();
  95. if (chosenVariant) {
  96. this.switch_(chosenVariant, this.config_.clearBufferSwitch,
  97. this.config_.safeMarginSwitch);
  98. }
  99. }
  100. });
  101. /** @private {?shaka.util.CmsdManager} */
  102. this.cmsdManager_ = null;
  103. }
  104. /**
  105. * @override
  106. * @export
  107. */
  108. stop() {
  109. this.switch_ = null;
  110. this.enabled_ = false;
  111. this.variants_ = [];
  112. this.playbackRate_ = 1;
  113. this.lastTimeChosenMs_ = null;
  114. this.mediaElement_ = null;
  115. if (this.resizeObserver_) {
  116. this.resizeObserver_.disconnect();
  117. this.resizeObserver_ = null;
  118. }
  119. this.resizeObserverTimer_.stop();
  120. this.cmsdManager_ = null;
  121. // Don't reset |startupComplete_|: if we've left the startup interval, we
  122. // can start using bandwidth estimates right away after init() is called.
  123. }
  124. /**
  125. * @override
  126. * @export
  127. */
  128. release() {
  129. // stop() should already have been called for unload
  130. this.eventManager_.release();
  131. this.resizeObserverTimer_ = null;
  132. }
  133. /**
  134. * @override
  135. * @export
  136. */
  137. init(switchCallback) {
  138. this.switch_ = switchCallback;
  139. }
  140. /**
  141. * @param {boolean=} preferFastSwitching
  142. * @return {shaka.extern.Variant}
  143. * @override
  144. * @export
  145. */
  146. chooseVariant(preferFastSwitching = false) {
  147. let maxHeight = Infinity;
  148. let maxWidth = Infinity;
  149. if (this.config_.restrictToScreenSize) {
  150. const devicePixelRatio =
  151. this.config_.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio;
  152. maxHeight = window.screen.height * devicePixelRatio;
  153. maxWidth = window.screen.width * devicePixelRatio;
  154. }
  155. if (this.resizeObserver_ && this.config_.restrictToElementSize) {
  156. const devicePixelRatio =
  157. this.config_.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio;
  158. maxHeight = Math.min(
  159. maxHeight, this.mediaElement_.clientHeight * devicePixelRatio);
  160. maxWidth = Math.min(
  161. maxWidth, this.mediaElement_.clientWidth * devicePixelRatio);
  162. }
  163. let normalVariants = this.variants_.filter((variant) => {
  164. return variant && !shaka.util.StreamUtils.isFastSwitching(variant);
  165. });
  166. if (!normalVariants.length) {
  167. normalVariants = this.variants_;
  168. }
  169. let variants = normalVariants;
  170. if (preferFastSwitching &&
  171. normalVariants.length != this.variants_.length) {
  172. variants = this.variants_.filter((variant) => {
  173. return variant && shaka.util.StreamUtils.isFastSwitching(variant);
  174. });
  175. }
  176. // Get sorted Variants.
  177. let sortedVariants = this.filterAndSortVariants_(
  178. this.config_.restrictions, variants,
  179. /* maxHeight= */ Infinity, /* maxWidth= */ Infinity);
  180. if (maxHeight != Infinity || maxWidth != Infinity) {
  181. const resolutions = this.getResolutionList_(sortedVariants);
  182. for (const resolution of resolutions) {
  183. if (resolution.height >= maxHeight && resolution.width >= maxWidth) {
  184. maxHeight = resolution.height;
  185. maxWidth = resolution.width;
  186. break;
  187. }
  188. }
  189. sortedVariants = this.filterAndSortVariants_(
  190. this.config_.restrictions, variants, maxHeight, maxWidth);
  191. }
  192. const currentBandwidth = this.getBandwidthEstimate();
  193. if (variants.length && !sortedVariants.length) {
  194. // If we couldn't meet the ABR restrictions, we should still play
  195. // something.
  196. // These restrictions are not "hard" restrictions in the way that
  197. // top-level or DRM-based restrictions are. Sort the variants without
  198. // restrictions and keep just the first (lowest-bandwidth) one.
  199. shaka.log.warning('No variants met the ABR restrictions. ' +
  200. 'Choosing a variant by lowest bandwidth.');
  201. sortedVariants = this.filterAndSortVariants_(
  202. /* restrictions= */ null, variants,
  203. /* maxHeight= */ Infinity, /* maxWidth= */ Infinity);
  204. sortedVariants = [sortedVariants[0]];
  205. }
  206. // Start by assuming that we will use the first Stream.
  207. let chosen = sortedVariants[0] || null;
  208. for (let i = 0; i < sortedVariants.length; i++) {
  209. const item = sortedVariants[i];
  210. const playbackRate =
  211. !isNaN(this.playbackRate_) ? Math.abs(this.playbackRate_) : 1;
  212. const itemBandwidth = playbackRate * item.bandwidth;
  213. const minBandwidth =
  214. itemBandwidth / this.config_.bandwidthDowngradeTarget;
  215. let next = {bandwidth: Infinity};
  216. for (let j = i + 1; j < sortedVariants.length; j++) {
  217. if (item.bandwidth != sortedVariants[j].bandwidth) {
  218. next = sortedVariants[j];
  219. break;
  220. }
  221. }
  222. const nextBandwidth = playbackRate * next.bandwidth;
  223. const maxBandwidth = nextBandwidth / this.config_.bandwidthUpgradeTarget;
  224. shaka.log.v2('Bandwidth ranges:',
  225. (itemBandwidth / 1e6).toFixed(3),
  226. (minBandwidth / 1e6).toFixed(3),
  227. (maxBandwidth / 1e6).toFixed(3));
  228. if (currentBandwidth >= minBandwidth &&
  229. currentBandwidth <= maxBandwidth &&
  230. (chosen.bandwidth != item.bandwidth ||
  231. this.isSameBandwidthAndHigherResolution_(chosen, item))) {
  232. chosen = item;
  233. }
  234. }
  235. this.lastTimeChosenMs_ = Date.now();
  236. return chosen;
  237. }
  238. /**
  239. * @override
  240. * @export
  241. */
  242. enable() {
  243. this.enabled_ = true;
  244. }
  245. /**
  246. * @override
  247. * @export
  248. */
  249. disable() {
  250. this.enabled_ = false;
  251. }
  252. /**
  253. * @param {number} deltaTimeMs The duration, in milliseconds, that the request
  254. * took to complete.
  255. * @param {number} numBytes The total number of bytes transferred.
  256. * @param {boolean} allowSwitch Indicate if the segment is allowed to switch
  257. * to another stream.
  258. * @param {shaka.extern.Request=} request
  259. * A reference to the request
  260. * @override
  261. * @export
  262. */
  263. segmentDownloaded(deltaTimeMs, numBytes, allowSwitch, request) {
  264. if (deltaTimeMs < this.config_.cacheLoadThreshold) {
  265. // The time indicates that it could be a cache response, so we should
  266. // ignore this value.
  267. return;
  268. }
  269. shaka.log.v2('Segment downloaded:',
  270. 'contentType=' + (request && request.contentType),
  271. 'deltaTimeMs=' + deltaTimeMs,
  272. 'numBytes=' + numBytes,
  273. 'lastTimeChosenMs=' + this.lastTimeChosenMs_,
  274. 'enabled=' + this.enabled_);
  275. goog.asserts.assert(deltaTimeMs >= 0, 'expected a non-negative duration');
  276. this.bandwidthEstimator_.sample(deltaTimeMs, numBytes);
  277. if (allowSwitch && (this.lastTimeChosenMs_ != null) && this.enabled_) {
  278. this.suggestStreams_();
  279. }
  280. }
  281. /**
  282. * @override
  283. * @export
  284. */
  285. trySuggestStreams() {
  286. if ((this.lastTimeChosenMs_ != null) && this.enabled_) {
  287. this.suggestStreams_();
  288. }
  289. }
  290. /**
  291. * @override
  292. * @export
  293. */
  294. getBandwidthEstimate() {
  295. const defaultBandwidthEstimate = this.getDefaultBandwidth_();
  296. const bandwidthEstimate = this.bandwidthEstimator_.getBandwidthEstimate(
  297. defaultBandwidthEstimate);
  298. if (this.cmsdManager_) {
  299. return this.cmsdManager_.getBandwidthEstimate(bandwidthEstimate);
  300. }
  301. return bandwidthEstimate;
  302. }
  303. /**
  304. * @override
  305. * @export
  306. */
  307. setVariants(variants) {
  308. this.variants_ = variants;
  309. }
  310. /**
  311. * @override
  312. * @export
  313. */
  314. playbackRateChanged(rate) {
  315. this.playbackRate_ = rate;
  316. }
  317. /**
  318. * @override
  319. * @export
  320. */
  321. setMediaElement(mediaElement) {
  322. this.mediaElement_ = mediaElement;
  323. if (this.resizeObserver_) {
  324. this.resizeObserver_.disconnect();
  325. this.resizeObserver_ = null;
  326. }
  327. if (this.mediaElement_ && 'ResizeObserver' in window) {
  328. this.resizeObserver_ = new ResizeObserver(() => {
  329. const SimpleAbrManager = shaka.abr.SimpleAbrManager;
  330. // Batch up resize changes before checking them.
  331. this.resizeObserverTimer_.tickAfter(
  332. /* seconds= */ SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME);
  333. });
  334. this.resizeObserver_.observe(this.mediaElement_);
  335. }
  336. }
  337. /**
  338. * @override
  339. * @export
  340. */
  341. setCmsdManager(cmsdManager) {
  342. this.cmsdManager_ = cmsdManager;
  343. }
  344. /**
  345. * @override
  346. * @export
  347. */
  348. configure(config) {
  349. this.config_ = config;
  350. if (this.bandwidthEstimator_ && this.config_) {
  351. this.bandwidthEstimator_.configure(this.config_.advanced);
  352. }
  353. }
  354. /**
  355. * Calls switch_() with the variant chosen by chooseVariant().
  356. *
  357. * @private
  358. */
  359. suggestStreams_() {
  360. shaka.log.v2('Suggesting Streams...');
  361. goog.asserts.assert(this.lastTimeChosenMs_ != null,
  362. 'lastTimeChosenMs_ should not be null');
  363. if (!this.startupComplete_) {
  364. // Check if we've got enough data yet.
  365. if (!this.bandwidthEstimator_.hasGoodEstimate()) {
  366. shaka.log.v2('Still waiting for a good estimate...');
  367. return;
  368. }
  369. this.startupComplete_ = true;
  370. this.lastTimeChosenMs_ -=
  371. (this.config_.switchInterval - this.config_.minTimeToSwitch) * 1000;
  372. }
  373. // Check if we've left the switch interval.
  374. const now = Date.now();
  375. const delta = now - this.lastTimeChosenMs_;
  376. if (delta < this.config_.switchInterval * 1000) {
  377. shaka.log.v2('Still within switch interval...');
  378. return;
  379. }
  380. const chosenVariant = this.chooseVariant();
  381. const bandwidthEstimate = this.getBandwidthEstimate();
  382. const currentBandwidthKbps = Math.round(bandwidthEstimate / 1000.0);
  383. if (chosenVariant) {
  384. shaka.log.debug(
  385. 'Calling switch_(), bandwidth=' + currentBandwidthKbps + ' kbps');
  386. // If any of these chosen streams are already chosen, Player will filter
  387. // them out before passing the choices on to StreamingEngine.
  388. this.switch_(chosenVariant, this.config_.clearBufferSwitch,
  389. this.config_.safeMarginSwitch);
  390. }
  391. }
  392. /**
  393. * @private
  394. */
  395. getDefaultBandwidth_() {
  396. let defaultBandwidthEstimate = this.config_.defaultBandwidthEstimate;
  397. // Some browsers implement the Network Information API, which allows
  398. // retrieving information about a user's network connection. Tizen 3 has
  399. // NetworkInformation, but not the downlink attribute.
  400. if (navigator.connection && navigator.connection.downlink &&
  401. this.config_.useNetworkInformation) {
  402. // If it's available, get the bandwidth estimate from the browser (in
  403. // megabits per second) and use it as defaultBandwidthEstimate.
  404. defaultBandwidthEstimate = navigator.connection.downlink * 1e6;
  405. }
  406. return defaultBandwidthEstimate;
  407. }
  408. /**
  409. * @param {?shaka.extern.Restrictions} restrictions
  410. * @param {!Array.<shaka.extern.Variant>} variants
  411. * @param {!number} maxHeight
  412. * @param {!number} maxWidth
  413. * @return {!Array.<shaka.extern.Variant>} variants filtered according to
  414. * |restrictions| and sorted in ascending order of bandwidth.
  415. * @private
  416. */
  417. filterAndSortVariants_(restrictions, variants, maxHeight, maxWidth) {
  418. if (this.cmsdManager_) {
  419. const maxBitrate = this.cmsdManager_.getMaxBitrate();
  420. if (maxBitrate) {
  421. variants = variants.filter((variant) => {
  422. if (!variant.bandwidth || !maxBitrate) {
  423. return true;
  424. }
  425. return variant.bandwidth <= maxBitrate;
  426. });
  427. }
  428. }
  429. if (restrictions) {
  430. variants = variants.filter((variant) => {
  431. // This was already checked in another scope, but the compiler doesn't
  432. // seem to understand that.
  433. goog.asserts.assert(restrictions, 'Restrictions should exist!');
  434. return shaka.util.StreamUtils.meetsRestrictions(
  435. variant, restrictions,
  436. /* maxHwRes= */ {width: maxWidth, height: maxHeight});
  437. });
  438. }
  439. return variants.sort((v1, v2) => {
  440. return v1.bandwidth - v2.bandwidth;
  441. });
  442. }
  443. /**
  444. * @param {!Array.<shaka.extern.Variant>} variants
  445. * @return {!Array.<{height: number, width: number}>}
  446. * @private
  447. */
  448. getResolutionList_(variants) {
  449. const resolutions = [];
  450. for (const variant of variants) {
  451. const video = variant.video;
  452. if (!video || !video.height || !video.width) {
  453. continue;
  454. }
  455. resolutions.push({
  456. height: video.height,
  457. width: video.width,
  458. });
  459. }
  460. return resolutions.sort((v1, v2) => {
  461. return v1.width - v2.width;
  462. });
  463. }
  464. /**
  465. * @param {shaka.extern.Variant} chosenVariant
  466. * @param {shaka.extern.Variant} newVariant
  467. * @return {boolean}
  468. * @private
  469. */
  470. isSameBandwidthAndHigherResolution_(chosenVariant, newVariant) {
  471. if (chosenVariant.bandwidth != newVariant.bandwidth) {
  472. return false;
  473. }
  474. if (!chosenVariant.video || !newVariant.video) {
  475. return false;
  476. }
  477. return chosenVariant.video.width < newVariant.video.width ||
  478. chosenVariant.video.height < newVariant.video.height;
  479. }
  480. };
  481. /**
  482. * The amount of time, in seconds, we wait to batch up rapid resize changes.
  483. * This allows us to avoid multiple resize events in most cases.
  484. * @type {number}
  485. */
  486. shaka.abr.SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME = 1;