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