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. const bandwidthEstimate = this.bandwidthEstimator_.getBandwidthEstimate(
  295. defaultBandwidthEstimate);
  296. if (this.cmsdManager_) {
  297. return this.cmsdManager_.getBandwidthEstimate(bandwidthEstimate);
  298. }
  299. return bandwidthEstimate;
  300. }
  301. /**
  302. * @override
  303. * @export
  304. */
  305. setVariants(variants) {
  306. this.variants_ = variants;
  307. }
  308. /**
  309. * @override
  310. * @export
  311. */
  312. playbackRateChanged(rate) {
  313. this.playbackRate_ = rate;
  314. }
  315. /**
  316. * @override
  317. * @export
  318. */
  319. setMediaElement(mediaElement) {
  320. this.mediaElement_ = mediaElement;
  321. if (this.resizeObserver_) {
  322. this.resizeObserver_.disconnect();
  323. this.resizeObserver_ = null;
  324. }
  325. if (this.mediaElement_ && 'ResizeObserver' in window) {
  326. this.resizeObserver_ = new ResizeObserver(() => {
  327. const SimpleAbrManager = shaka.abr.SimpleAbrManager;
  328. // Batch up resize changes before checking them.
  329. this.resizeObserverTimer_.tickAfter(
  330. /* seconds= */ SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME);
  331. });
  332. this.resizeObserver_.observe(this.mediaElement_);
  333. }
  334. }
  335. /**
  336. * @override
  337. * @export
  338. */
  339. setCmsdManager(cmsdManager) {
  340. this.cmsdManager_ = cmsdManager;
  341. }
  342. /**
  343. * @override
  344. * @export
  345. */
  346. configure(config) {
  347. this.config_ = config;
  348. if (this.bandwidthEstimator_ && this.config_) {
  349. this.bandwidthEstimator_.configure(this.config_.advanced);
  350. }
  351. }
  352. /**
  353. * Calls switch_() with the variant chosen by chooseVariant().
  354. *
  355. * @private
  356. */
  357. suggestStreams_() {
  358. shaka.log.v2('Suggesting Streams...');
  359. goog.asserts.assert(this.lastTimeChosenMs_ != null,
  360. 'lastTimeChosenMs_ should not be null');
  361. if (!this.startupComplete_) {
  362. // Check if we've got enough data yet.
  363. if (!this.bandwidthEstimator_.hasGoodEstimate()) {
  364. shaka.log.v2('Still waiting for a good estimate...');
  365. return;
  366. }
  367. this.startupComplete_ = true;
  368. this.lastTimeChosenMs_ -=
  369. (this.config_.switchInterval - this.config_.minTimeToSwitch) * 1000;
  370. }
  371. // Check if we've left the switch interval.
  372. const now = Date.now();
  373. const delta = now - this.lastTimeChosenMs_;
  374. if (delta < this.config_.switchInterval * 1000) {
  375. shaka.log.v2('Still within switch interval...');
  376. return;
  377. }
  378. const chosenVariant = this.chooseVariant();
  379. const bandwidthEstimate = this.getBandwidthEstimate();
  380. const currentBandwidthKbps = Math.round(bandwidthEstimate / 1000.0);
  381. if (chosenVariant) {
  382. shaka.log.debug(
  383. 'Calling switch_(), bandwidth=' + currentBandwidthKbps + ' kbps');
  384. // If any of these chosen streams are already chosen, Player will filter
  385. // them out before passing the choices on to StreamingEngine.
  386. this.switch_(chosenVariant, this.config_.clearBufferSwitch,
  387. this.config_.safeMarginSwitch);
  388. }
  389. }
  390. /**
  391. * @private
  392. */
  393. getDefaultBandwidth_() {
  394. let defaultBandwidthEstimate = this.config_.defaultBandwidthEstimate;
  395. // Some browsers implement the Network Information API, which allows
  396. // retrieving information about a user's network connection. Tizen 3 has
  397. // NetworkInformation, but not the downlink attribute.
  398. if (navigator.connection && navigator.connection.downlink &&
  399. this.config_.useNetworkInformation) {
  400. // If it's available, get the bandwidth estimate from the browser (in
  401. // megabits per second) and use it as defaultBandwidthEstimate.
  402. defaultBandwidthEstimate = navigator.connection.downlink * 1e6;
  403. }
  404. return defaultBandwidthEstimate;
  405. }
  406. /**
  407. * @param {?shaka.extern.Restrictions} restrictions
  408. * @param {!Array.<shaka.extern.Variant>} variants
  409. * @param {!number} maxHeight
  410. * @param {!number} maxWidth
  411. * @return {!Array.<shaka.extern.Variant>} variants filtered according to
  412. * |restrictions| and sorted in ascending order of bandwidth.
  413. * @private
  414. */
  415. filterAndSortVariants_(restrictions, variants, maxHeight, maxWidth) {
  416. if (this.cmsdManager_) {
  417. const maxBitrate = this.cmsdManager_.getMaxBitrate();
  418. if (maxBitrate) {
  419. variants = variants.filter((variant) => {
  420. if (!variant.bandwidth || !maxBitrate) {
  421. return true;
  422. }
  423. return variant.bandwidth <= maxBitrate;
  424. });
  425. }
  426. }
  427. if (restrictions) {
  428. variants = variants.filter((variant) => {
  429. // This was already checked in another scope, but the compiler doesn't
  430. // seem to understand that.
  431. goog.asserts.assert(restrictions, 'Restrictions should exist!');
  432. return shaka.util.StreamUtils.meetsRestrictions(
  433. variant, restrictions,
  434. /* maxHwRes= */ {width: maxWidth, height: maxHeight});
  435. });
  436. }
  437. return variants.sort((v1, v2) => {
  438. return v1.bandwidth - v2.bandwidth;
  439. });
  440. }
  441. /**
  442. * @param {!Array.<shaka.extern.Variant>} variants
  443. * @return {!Array.<{height: number, width: number}>}
  444. * @private
  445. */
  446. getResolutionList_(variants) {
  447. const resolutions = [];
  448. for (const variant of variants) {
  449. const video = variant.video;
  450. if (!video || !video.height || !video.width) {
  451. continue;
  452. }
  453. resolutions.push({
  454. height: video.height,
  455. width: video.width,
  456. });
  457. }
  458. return resolutions.sort((v1, v2) => {
  459. return v1.width - v2.width;
  460. });
  461. }
  462. /**
  463. * @param {shaka.extern.Variant} chosenVariant
  464. * @param {shaka.extern.Variant} newVariant
  465. * @return {boolean}
  466. * @private
  467. */
  468. isSameBandwidthAndHigherResolution_(chosenVariant, newVariant) {
  469. if (chosenVariant.bandwidth != newVariant.bandwidth) {
  470. return false;
  471. }
  472. if (!chosenVariant.video || !newVariant.video) {
  473. return false;
  474. }
  475. return chosenVariant.video.width < newVariant.video.width ||
  476. chosenVariant.video.height < newVariant.video.height;
  477. }
  478. };
  479. /**
  480. * The amount of time, in seconds, we wait to batch up rapid resize changes.
  481. * This allows us to avoid multiple resize events in most cases.
  482. * @type {number}
  483. */
  484. shaka.abr.SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME = 1;