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('shaka.util.IReleasable');
  8. goog.require('goog.asserts');
  9. goog.require('shaka.abr.EwmaBandwidthEstimator');
  10. goog.require('shaka.log');
  11. goog.require('shaka.util.StreamUtils');
  12. goog.require('shaka.util.Timer');
  13. /**
  14. * @summary
  15. * <p>
  16. * This defines the default ABR manager for the Player. An instance of this
  17. * class is used when no ABR manager is given.
  18. * </p>
  19. * <p>
  20. * The behavior of this class is to take throughput samples using
  21. * segmentDownloaded to estimate the current network bandwidth. Then it will
  22. * use that to choose the streams that best fit the current bandwidth. It will
  23. * always pick the highest bandwidth variant it thinks can be played.
  24. * </p>
  25. * <p>
  26. * After initial choices are made, this class will call switchCallback() when
  27. * there is a better choice. switchCallback() will not be called more than once
  28. * per ({@link shaka.abr.SimpleAbrManager.SWITCH_INTERVAL_MS}).
  29. * </p>
  30. *
  31. * @implements {shaka.extern.AbrManager}
  32. * @implements {shaka.util.IReleasable}
  33. * @export
  34. */
  35. shaka.abr.SimpleAbrManager = class {
  36. /** */
  37. constructor() {
  38. /** @private {?shaka.extern.AbrManager.SwitchCallback} */
  39. this.switch_ = null;
  40. /** @private {boolean} */
  41. this.enabled_ = false;
  42. /** @private {shaka.abr.EwmaBandwidthEstimator} */
  43. this.bandwidthEstimator_ = new shaka.abr.EwmaBandwidthEstimator();
  44. /** @private {?function():void} */
  45. this.onNetworkInformationChange_ = null;
  46. // Some browsers implement the Network Information API, which allows
  47. // retrieving information about a user's network connection. We listen
  48. // to the change event to be able to make quick changes in case the type
  49. // of connectivity changes.
  50. if (navigator.connection && navigator.connection.addEventListener) {
  51. this.onNetworkInformationChange_ = () => {
  52. if (this.enabled_ && this.config_.useNetworkInformation) {
  53. this.bandwidthEstimator_ = new shaka.abr.EwmaBandwidthEstimator();
  54. if (this.config_) {
  55. this.bandwidthEstimator_.configure(this.config_.advanced);
  56. }
  57. const chosenVariant = this.chooseVariant();
  58. if (chosenVariant && navigator.onLine) {
  59. this.switch_(chosenVariant);
  60. }
  61. }
  62. };
  63. navigator.connection.addEventListener(
  64. 'change', this.onNetworkInformationChange_);
  65. }
  66. /**
  67. * A filtered list of Variants to choose from.
  68. * @private {!Array.<!shaka.extern.Variant>}
  69. */
  70. this.variants_ = [];
  71. /** @private {number} */
  72. this.playbackRate_ = 1;
  73. /** @private {boolean} */
  74. this.startupComplete_ = false;
  75. /**
  76. * The last wall-clock time, in milliseconds, when streams were chosen.
  77. *
  78. * @private {?number}
  79. */
  80. this.lastTimeChosenMs_ = null;
  81. /** @private {?shaka.extern.AbrConfiguration} */
  82. this.config_ = null;
  83. /** @private {HTMLMediaElement} */
  84. this.mediaElement_ = null;
  85. /** @private {ResizeObserver} */
  86. this.resizeObserver_ = null;
  87. /** @private {shaka.util.Timer} */
  88. this.resizeObserverTimer_ = new shaka.util.Timer(() => {
  89. if (this.config_.restrictToElementSize) {
  90. const chosenVariant = this.chooseVariant();
  91. if (chosenVariant) {
  92. this.switch_(chosenVariant);
  93. }
  94. }
  95. });
  96. }
  97. /**
  98. * @override
  99. * @export
  100. */
  101. stop() {
  102. this.switch_ = null;
  103. this.enabled_ = false;
  104. this.variants_ = [];
  105. this.playbackRate_ = 1;
  106. this.lastTimeChosenMs_ = null;
  107. this.mediaElement_ = null;
  108. if (this.resizeObserver_) {
  109. this.resizeObserver_.disconnect();
  110. this.resizeObserver_ = null;
  111. }
  112. this.resizeObserverTimer_.stop();
  113. // Don't reset |startupComplete_|: if we've left the startup interval, we
  114. // can start using bandwidth estimates right away after init() is called.
  115. }
  116. /**
  117. * @override
  118. * @export
  119. */
  120. release() {
  121. // stop() should already have been called for unload
  122. if (navigator.connection && navigator.connection.removeEventListener) {
  123. navigator.connection.removeEventListener(
  124. 'change', this.onNetworkInformationChange_);
  125. this.onNetworkInformationChange_ = null;
  126. }
  127. this.resizeObserverTimer_ = null;
  128. }
  129. /**
  130. * @override
  131. * @export
  132. */
  133. init(switchCallback) {
  134. this.switch_ = switchCallback;
  135. }
  136. /**
  137. * @override
  138. * @export
  139. */
  140. chooseVariant() {
  141. const SimpleAbrManager = shaka.abr.SimpleAbrManager;
  142. let maxHeight = Infinity;
  143. let maxWidth = Infinity;
  144. if (this.config_.restrictToScreenSize) {
  145. const devicePixelRatio =
  146. this.config_.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio;
  147. maxHeight = window.screen.height * devicePixelRatio;
  148. maxWidth = window.screen.width * devicePixelRatio;
  149. }
  150. if (this.resizeObserver_ && this.config_.restrictToElementSize) {
  151. const devicePixelRatio =
  152. this.config_.ignoreDevicePixelRatio ? 1 : window.devicePixelRatio;
  153. maxHeight = this.mediaElement_.clientHeight * devicePixelRatio;
  154. maxWidth = this.mediaElement_.clientWidth * devicePixelRatio;
  155. }
  156. // Get sorted Variants.
  157. let sortedVariants = SimpleAbrManager.filterAndSortVariants_(
  158. this.config_.restrictions, this.variants_, maxHeight, maxWidth);
  159. const defaultBandwidthEstimate = this.getDefaultBandwidth_();
  160. const currentBandwidth = this.bandwidthEstimator_.getBandwidthEstimate(
  161. defaultBandwidthEstimate);
  162. if (this.variants_.length && !sortedVariants.length) {
  163. // If we couldn't meet the ABR restrictions, we should still play
  164. // something.
  165. // These restrictions are not "hard" restrictions in the way that
  166. // top-level or DRM-based restrictions are. Sort the variants without
  167. // restrictions and keep just the first (lowest-bandwidth) one.
  168. shaka.log.warning('No variants met the ABR restrictions. ' +
  169. 'Choosing a variant by lowest bandwidth.');
  170. sortedVariants = SimpleAbrManager.filterAndSortVariants_(
  171. /* restrictions= */ null, this.variants_,
  172. /* maxHeight= */ Infinity, /* maxWidth= */ Infinity);
  173. sortedVariants = [sortedVariants[0]];
  174. }
  175. // Start by assuming that we will use the first Stream.
  176. let chosen = sortedVariants[0] || null;
  177. for (let i = 0; i < sortedVariants.length; i++) {
  178. const item = sortedVariants[i];
  179. const playbackRate =
  180. !isNaN(this.playbackRate_) ? Math.abs(this.playbackRate_) : 1;
  181. const itemBandwidth = playbackRate * item.bandwidth;
  182. const minBandwidth =
  183. itemBandwidth / this.config_.bandwidthDowngradeTarget;
  184. let next = {bandwidth: Infinity};
  185. for (let j = i + 1; j < sortedVariants.length; j++) {
  186. if (item.bandwidth != sortedVariants[j].bandwidth) {
  187. next = sortedVariants[j];
  188. break;
  189. }
  190. }
  191. const nextBandwidth = playbackRate * next.bandwidth;
  192. const maxBandwidth = nextBandwidth / this.config_.bandwidthUpgradeTarget;
  193. shaka.log.v2('Bandwidth ranges:',
  194. (itemBandwidth / 1e6).toFixed(3),
  195. (minBandwidth / 1e6).toFixed(3),
  196. (maxBandwidth / 1e6).toFixed(3));
  197. if (currentBandwidth >= minBandwidth &&
  198. currentBandwidth <= maxBandwidth &&
  199. chosen.bandwidth != item.bandwidth) {
  200. chosen = item;
  201. }
  202. }
  203. this.lastTimeChosenMs_ = Date.now();
  204. return chosen;
  205. }
  206. /**
  207. * @override
  208. * @export
  209. */
  210. enable() {
  211. this.enabled_ = true;
  212. }
  213. /**
  214. * @override
  215. * @export
  216. */
  217. disable() {
  218. this.enabled_ = false;
  219. }
  220. /**
  221. * @override
  222. * @export
  223. */
  224. segmentDownloaded(deltaTimeMs, numBytes) {
  225. shaka.log.v2('Segment downloaded:',
  226. 'deltaTimeMs=' + deltaTimeMs,
  227. 'numBytes=' + numBytes,
  228. 'lastTimeChosenMs=' + this.lastTimeChosenMs_,
  229. 'enabled=' + this.enabled_);
  230. goog.asserts.assert(deltaTimeMs >= 0, 'expected a non-negative duration');
  231. this.bandwidthEstimator_.sample(deltaTimeMs, numBytes);
  232. if ((this.lastTimeChosenMs_ != null) && this.enabled_) {
  233. this.suggestStreams_();
  234. }
  235. }
  236. /**
  237. * @override
  238. * @export
  239. */
  240. getBandwidthEstimate() {
  241. const defaultBandwidthEstimate = this.getDefaultBandwidth_();
  242. return this.bandwidthEstimator_.getBandwidthEstimate(
  243. defaultBandwidthEstimate);
  244. }
  245. /**
  246. * @override
  247. * @export
  248. */
  249. setVariants(variants) {
  250. this.variants_ = variants;
  251. }
  252. /**
  253. * @override
  254. * @export
  255. */
  256. playbackRateChanged(rate) {
  257. this.playbackRate_ = rate;
  258. }
  259. /**
  260. * @override
  261. * @export
  262. */
  263. setMediaElement(mediaElement) {
  264. this.mediaElement_ = mediaElement;
  265. if (this.resizeObserver_) {
  266. this.resizeObserver_.disconnect();
  267. this.resizeObserver_ = null;
  268. }
  269. if (this.mediaElement_ && 'ResizeObserver' in window) {
  270. this.resizeObserver_ = new ResizeObserver(() => {
  271. const SimpleAbrManager = shaka.abr.SimpleAbrManager;
  272. // Batch up resize changes before checking them.
  273. this.resizeObserverTimer_.tickAfter(
  274. /* seconds= */ SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME);
  275. });
  276. this.resizeObserver_.observe(this.mediaElement_);
  277. }
  278. }
  279. /**
  280. * @override
  281. * @export
  282. */
  283. configure(config) {
  284. this.config_ = config;
  285. if (this.bandwidthEstimator_ && this.config_) {
  286. this.bandwidthEstimator_.configure(this.config_.advanced);
  287. }
  288. }
  289. /**
  290. * Calls switch_() with the variant chosen by chooseVariant().
  291. *
  292. * @private
  293. */
  294. suggestStreams_() {
  295. shaka.log.v2('Suggesting Streams...');
  296. goog.asserts.assert(this.lastTimeChosenMs_ != null,
  297. 'lastTimeChosenMs_ should not be null');
  298. if (!this.startupComplete_) {
  299. // Check if we've got enough data yet.
  300. if (!this.bandwidthEstimator_.hasGoodEstimate()) {
  301. shaka.log.v2('Still waiting for a good estimate...');
  302. return;
  303. }
  304. this.startupComplete_ = true;
  305. } else {
  306. // Check if we've left the switch interval.
  307. const now = Date.now();
  308. const delta = now - this.lastTimeChosenMs_;
  309. if (delta < this.config_.switchInterval * 1000) {
  310. shaka.log.v2('Still within switch interval...');
  311. return;
  312. }
  313. }
  314. const chosenVariant = this.chooseVariant();
  315. const defaultBandwidthEstimate = this.getDefaultBandwidth_();
  316. const bandwidthEstimate = this.bandwidthEstimator_.getBandwidthEstimate(
  317. defaultBandwidthEstimate);
  318. const currentBandwidthKbps = Math.round(bandwidthEstimate / 1000.0);
  319. if (chosenVariant) {
  320. shaka.log.debug(
  321. 'Calling switch_(), bandwidth=' + currentBandwidthKbps + ' kbps');
  322. // If any of these chosen streams are already chosen, Player will filter
  323. // them out before passing the choices on to StreamingEngine.
  324. this.switch_(chosenVariant);
  325. }
  326. }
  327. /**
  328. * @private
  329. */
  330. getDefaultBandwidth_() {
  331. let defaultBandwidthEstimate = this.config_.defaultBandwidthEstimate;
  332. // Some browsers implement the Network Information API, which allows
  333. // retrieving information about a user's network connection. Tizen 3 has
  334. // NetworkInformation, but not the downlink attribute.
  335. if (navigator.connection && navigator.connection.downlink &&
  336. this.config_.useNetworkInformation) {
  337. // If it's available, get the bandwidth estimate from the browser (in
  338. // megabits per second) and use it as defaultBandwidthEstimate.
  339. defaultBandwidthEstimate = navigator.connection.downlink * 1e6;
  340. }
  341. return defaultBandwidthEstimate;
  342. }
  343. /**
  344. * @param {?shaka.extern.Restrictions} restrictions
  345. * @param {!Array.<shaka.extern.Variant>} variants
  346. * @param {!number} maxHeight
  347. * @param {!number} maxWidth
  348. * @return {!Array.<shaka.extern.Variant>} variants filtered according to
  349. * |restrictions| and sorted in ascending order of bandwidth.
  350. * @private
  351. */
  352. static filterAndSortVariants_(restrictions, variants, maxHeight, maxWidth) {
  353. if (restrictions) {
  354. variants = variants.filter((variant) => {
  355. // This was already checked in another scope, but the compiler doesn't
  356. // seem to understand that.
  357. goog.asserts.assert(restrictions, 'Restrictions should exist!');
  358. return shaka.util.StreamUtils.meetsRestrictions(
  359. variant, restrictions,
  360. /* maxHwRes= */ {width: maxWidth, height: maxHeight});
  361. });
  362. }
  363. return variants.sort((v1, v2) => {
  364. return v1.bandwidth - v2.bandwidth;
  365. });
  366. }
  367. };
  368. /**
  369. * The amount of time, in seconds, we wait to batch up rapid resize changes.
  370. * This allows us to avoid multiple resize events in most cases.
  371. * @type {number}
  372. */
  373. shaka.abr.SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME = 1;