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. * @param {shaka.extern.RequestContext=} context
  286. * A reference to the request context
  287. * @override
  288. * @export
  289. */
  290. segmentDownloaded(deltaTimeMs, numBytes, allowSwitch, request, context) {
  291. let realTimeMs = deltaTimeMs;
  292. if (this.config_.removeLatencyFromFirstPacketTime &&
  293. request && request.packetNumber === 1 && request.timeToFirstByte) {
  294. realTimeMs = deltaTimeMs - request.timeToFirstByte;
  295. }
  296. if (realTimeMs < this.config_.cacheLoadThreshold) {
  297. // The time indicates that it could be a cache response, so we should
  298. // ignore this value.
  299. return;
  300. }
  301. shaka.log.v2('Segment downloaded:',
  302. 'contentType=' + (request && request.contentType),
  303. 'deltaTimeMs=' + realTimeMs,
  304. 'numBytes=' + numBytes,
  305. 'lastTimeChosenMs=' + this.lastTimeChosenMs_,
  306. 'enabled=' + this.enabled_);
  307. goog.asserts.assert(realTimeMs >= 0, 'expected a non-negative duration');
  308. this.bandwidthEstimator_.sample(realTimeMs, numBytes);
  309. if (allowSwitch && (this.lastTimeChosenMs_ != null) && this.enabled_) {
  310. this.suggestStreams_();
  311. }
  312. }
  313. /**
  314. * @override
  315. * @export
  316. */
  317. trySuggestStreams() {
  318. if ((this.lastTimeChosenMs_ != null) && this.enabled_) {
  319. this.suggestStreams_();
  320. }
  321. }
  322. /**
  323. * @override
  324. * @export
  325. */
  326. getBandwidthEstimate() {
  327. const defaultBandwidthEstimate = this.getDefaultBandwidth_();
  328. if (navigator.connection && navigator.connection.downlink &&
  329. this.config_.useNetworkInformation &&
  330. this.config_.preferNetworkInformationBandwidth) {
  331. return defaultBandwidthEstimate;
  332. }
  333. const bandwidthEstimate = this.bandwidthEstimator_.getBandwidthEstimate(
  334. defaultBandwidthEstimate);
  335. if (this.cmsdManager_) {
  336. return this.cmsdManager_.getBandwidthEstimate(bandwidthEstimate);
  337. }
  338. return bandwidthEstimate;
  339. }
  340. /**
  341. * @override
  342. * @export
  343. */
  344. setVariants(variants) {
  345. this.variants_ = variants;
  346. }
  347. /**
  348. * @override
  349. * @export
  350. */
  351. playbackRateChanged(rate) {
  352. this.playbackRate_ = rate;
  353. }
  354. /**
  355. * @override
  356. * @export
  357. */
  358. setMediaElement(mediaElement) {
  359. this.mediaElement_ = mediaElement;
  360. if (this.resizeObserver_) {
  361. this.resizeObserver_.disconnect();
  362. this.resizeObserver_ = null;
  363. }
  364. const onResize = () => {
  365. const SimpleAbrManager = shaka.abr.SimpleAbrManager;
  366. // Batch up resize changes before checking them.
  367. this.resizeObserverTimer_.tickAfter(
  368. /* seconds= */ SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME);
  369. };
  370. if (this.mediaElement_ && 'ResizeObserver' in window) {
  371. this.resizeObserver_ = new ResizeObserver(onResize);
  372. this.resizeObserver_.observe(this.mediaElement_);
  373. }
  374. this.eventManager_.listen(mediaElement, 'enterpictureinpicture', (e) => {
  375. const event = /** @type {PictureInPictureEvent} */(e);
  376. if (event.pictureInPictureWindow) {
  377. this.pictureInPictureWindow_ = event.pictureInPictureWindow;
  378. this.eventManager_.listen(
  379. this.pictureInPictureWindow_, 'resize', onResize);
  380. }
  381. });
  382. this.eventManager_.listen(mediaElement, 'leavepictureinpicture', () => {
  383. if (this.pictureInPictureWindow_) {
  384. this.eventManager_.unlisten(
  385. this.pictureInPictureWindow_, 'resize', onResize);
  386. }
  387. this.pictureInPictureWindow_ = null;
  388. });
  389. }
  390. /**
  391. * @override
  392. * @export
  393. */
  394. setCmsdManager(cmsdManager) {
  395. this.cmsdManager_ = cmsdManager;
  396. }
  397. /**
  398. * @override
  399. * @export
  400. */
  401. configure(config) {
  402. this.config_ = config;
  403. if (this.bandwidthEstimator_ && this.config_) {
  404. this.bandwidthEstimator_.configure(this.config_.advanced);
  405. }
  406. }
  407. /**
  408. * Calls switch_() with the variant chosen by chooseVariant().
  409. *
  410. * @private
  411. */
  412. suggestStreams_() {
  413. shaka.log.v2('Suggesting Streams...');
  414. goog.asserts.assert(this.lastTimeChosenMs_ != null,
  415. 'lastTimeChosenMs_ should not be null');
  416. if (!this.startupComplete_) {
  417. // Check if we've got enough data yet.
  418. if (!this.bandwidthEstimator_.hasGoodEstimate()) {
  419. shaka.log.v2('Still waiting for a good estimate...');
  420. return;
  421. }
  422. this.startupComplete_ = true;
  423. this.lastTimeChosenMs_ -=
  424. (this.config_.switchInterval - this.config_.minTimeToSwitch) * 1000;
  425. }
  426. // Check if we've left the switch interval.
  427. const now = Date.now();
  428. const delta = now - this.lastTimeChosenMs_;
  429. if (delta < this.config_.switchInterval * 1000) {
  430. shaka.log.v2('Still within switch interval...');
  431. return;
  432. }
  433. const chosenVariant = this.chooseVariant();
  434. const bandwidthEstimate = this.getBandwidthEstimate();
  435. const currentBandwidthKbps = Math.round(bandwidthEstimate / 1000.0);
  436. if (chosenVariant) {
  437. shaka.log.debug(
  438. 'Calling switch_(), bandwidth=' + currentBandwidthKbps + ' kbps');
  439. // If any of these chosen streams are already chosen, Player will filter
  440. // them out before passing the choices on to StreamingEngine.
  441. this.switch_(chosenVariant, this.config_.clearBufferSwitch,
  442. this.config_.safeMarginSwitch);
  443. }
  444. }
  445. /**
  446. * @private
  447. */
  448. getDefaultBandwidth_() {
  449. let defaultBandwidthEstimate = this.config_.defaultBandwidthEstimate;
  450. // Some browsers implement the Network Information API, which allows
  451. // retrieving information about a user's network connection. Tizen 3 has
  452. // NetworkInformation, but not the downlink attribute.
  453. if (navigator.connection && navigator.connection.downlink &&
  454. this.config_.useNetworkInformation) {
  455. // If it's available, get the bandwidth estimate from the browser (in
  456. // megabits per second) and use it as defaultBandwidthEstimate.
  457. defaultBandwidthEstimate = navigator.connection.downlink * 1e6;
  458. }
  459. return defaultBandwidthEstimate;
  460. }
  461. /**
  462. * @param {?shaka.extern.Restrictions} restrictions
  463. * @param {!Array<shaka.extern.Variant>} variants
  464. * @param {!number} maxHeight
  465. * @param {!number} maxWidth
  466. * @return {!Array<shaka.extern.Variant>} variants filtered according to
  467. * |restrictions| and sorted in ascending order of bandwidth.
  468. * @private
  469. */
  470. filterAndSortVariants_(restrictions, variants, maxHeight, maxWidth) {
  471. if (this.cmsdManager_) {
  472. const maxBitrate = this.cmsdManager_.getMaxBitrate();
  473. if (maxBitrate) {
  474. variants = variants.filter((variant) => {
  475. if (!variant.bandwidth || !maxBitrate) {
  476. return true;
  477. }
  478. return variant.bandwidth <= maxBitrate;
  479. });
  480. }
  481. }
  482. if (restrictions) {
  483. variants = variants.filter((variant) => {
  484. // This was already checked in another scope, but the compiler doesn't
  485. // seem to understand that.
  486. goog.asserts.assert(restrictions, 'Restrictions should exist!');
  487. return shaka.util.StreamUtils.meetsRestrictions(
  488. variant, restrictions,
  489. /* maxHwRes= */ {width: maxWidth, height: maxHeight});
  490. });
  491. }
  492. return variants.sort((v1, v2) => {
  493. return v1.bandwidth - v2.bandwidth;
  494. });
  495. }
  496. /**
  497. * @param {!Array<shaka.extern.Variant>} variants
  498. * @return {!Array<{height: number, width: number}>}
  499. * @private
  500. */
  501. getResolutionList_(variants) {
  502. const resolutions = [];
  503. for (const variant of variants) {
  504. const video = variant.video;
  505. if (!video || !video.height || !video.width) {
  506. continue;
  507. }
  508. resolutions.push({
  509. height: video.height,
  510. width: video.width,
  511. });
  512. }
  513. return resolutions.sort((v1, v2) => {
  514. return v1.width - v2.width;
  515. });
  516. }
  517. /**
  518. * @param {shaka.extern.Variant} chosenVariant
  519. * @param {shaka.extern.Variant} newVariant
  520. * @return {boolean}
  521. * @private
  522. */
  523. isSameBandwidthAndHigherResolution_(chosenVariant, newVariant) {
  524. if (chosenVariant.bandwidth != newVariant.bandwidth) {
  525. return false;
  526. }
  527. if (!chosenVariant.video || !newVariant.video) {
  528. return false;
  529. }
  530. return chosenVariant.video.width < newVariant.video.width ||
  531. chosenVariant.video.height < newVariant.video.height;
  532. }
  533. };
  534. /**
  535. * The amount of time, in seconds, we wait to batch up rapid resize changes.
  536. * This allows us to avoid multiple resize events in most cases.
  537. * @type {number}
  538. */
  539. shaka.abr.SimpleAbrManager.RESIZE_OBSERVER_BATCH_TIME = 1;