Source: ui/statistics_button.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.StatisticsButton');
  7. goog.require('shaka.log');
  8. goog.require('shaka.ui.ContextMenu');
  9. goog.require('shaka.ui.Controls');
  10. goog.require('shaka.ui.Element');
  11. goog.require('shaka.ui.Enums');
  12. goog.require('shaka.ui.Locales');
  13. goog.require('shaka.ui.Localization');
  14. goog.require('shaka.ui.OverflowMenu');
  15. goog.require('shaka.ui.Utils');
  16. goog.require('shaka.util.Dom');
  17. goog.require('shaka.util.Timer');
  18. goog.requireType('shaka.ui.Controls');
  19. /**
  20. * @extends {shaka.ui.Element}
  21. * @final
  22. * @export
  23. */
  24. shaka.ui.StatisticsButton = class extends shaka.ui.Element {
  25. /**
  26. * @param {!HTMLElement} parent
  27. * @param {!shaka.ui.Controls} controls
  28. */
  29. constructor(parent, controls) {
  30. super(parent, controls);
  31. /** @private {!HTMLButtonElement} */
  32. this.button_ = shaka.util.Dom.createButton();
  33. this.button_.classList.add('shaka-statistics-button');
  34. /** @private {!HTMLElement} */
  35. this.icon_ = shaka.util.Dom.createHTMLElement('i');
  36. this.icon_.classList.add('material-icons-round');
  37. this.icon_.textContent =
  38. shaka.ui.Enums.MaterialDesignIcons.STATISTICS_ON;
  39. this.button_.appendChild(this.icon_);
  40. const label = shaka.util.Dom.createHTMLElement('label');
  41. label.classList.add('shaka-overflow-button-label');
  42. /** @private {!HTMLElement} */
  43. this.nameSpan_ = shaka.util.Dom.createHTMLElement('span');
  44. label.appendChild(this.nameSpan_);
  45. /** @private {!HTMLElement} */
  46. this.stateSpan_ = shaka.util.Dom.createHTMLElement('span');
  47. this.stateSpan_.classList.add('shaka-current-selection-span');
  48. label.appendChild(this.stateSpan_);
  49. this.button_.appendChild(label);
  50. this.parent.appendChild(this.button_);
  51. /** @private {!HTMLElement} */
  52. this.container_ = shaka.util.Dom.createHTMLElement('div');
  53. this.container_.classList.add('shaka-no-propagation');
  54. this.container_.classList.add('shaka-show-controls-on-mouse-over');
  55. this.container_.classList.add('shaka-statistics-container');
  56. this.container_.classList.add('shaka-hidden');
  57. const controlsContainer = this.controls.getControlsContainer();
  58. controlsContainer.appendChild(this.container_);
  59. /** @private {!Array} */
  60. this.statisticsList_ = [];
  61. /** @private {!Array} */
  62. this.skippedStats_ = ['stateHistory', 'switchHistory'];
  63. /** @private {!shaka.extern.Stats} */
  64. this.currentStats_ = this.player.getStats();
  65. /** @private {!Map<string, HTMLElement>} */
  66. this.displayedElements_ = new Map();
  67. const parsePx = (name) => {
  68. return this.currentStats_[name] + ' (px)';
  69. };
  70. const parsePercent = (name) => {
  71. return this.currentStats_[name] + ' (%)';
  72. };
  73. const parseFrames = (name) => {
  74. return this.currentStats_[name] + ' (frames)';
  75. };
  76. const parseSeconds = (name) => {
  77. return this.currentStats_[name].toFixed(2) + ' (s)';
  78. };
  79. const parseBits = (name) => {
  80. return Math.round(this.currentStats_[name] / 1000) + ' (kbits/s)';
  81. };
  82. const parseTime = (name) => {
  83. return shaka.ui.Utils.buildTimeString(
  84. this.currentStats_[name], false) + ' (m)';
  85. };
  86. const parseGaps = (name) => {
  87. return this.currentStats_[name] + ' (gaps)';
  88. };
  89. const parseStalls = (name) => {
  90. return this.currentStats_[name] + ' (stalls)';
  91. };
  92. const parseErrors = (name) => {
  93. return this.currentStats_[name] + ' (errors)';
  94. };
  95. const parsePeriods = (name) => {
  96. return this.currentStats_[name] + ' (periods)';
  97. };
  98. const parseBytes = (name) => {
  99. const bytes = parseInt(this.currentStats_[name], 10);
  100. if (bytes > 2 * 1e9) {
  101. return (bytes / 1e9).toFixed(2) + 'GB';
  102. } else if (bytes > 1e6) {
  103. return (bytes / 1e6).toFixed(2) + 'MB';
  104. } else if (bytes > 1e3) {
  105. return (bytes / 1e3).toFixed(2) + 'KB';
  106. } else {
  107. return bytes + 'B';
  108. }
  109. };
  110. /** @private {!Map<string, function(string): string>} */
  111. this.parseFrom_ = new Map()
  112. .set('width', parsePx)
  113. .set('height', parsePx)
  114. .set('completionPercent', parsePercent)
  115. .set('bufferingTime', parseSeconds)
  116. .set('drmTimeSeconds', parseSeconds)
  117. .set('licenseTime', parseSeconds)
  118. .set('liveLatency', parseSeconds)
  119. .set('loadLatency', parseSeconds)
  120. .set('manifestTimeSeconds', parseSeconds)
  121. .set('estimatedBandwidth', parseBits)
  122. .set('streamBandwidth', parseBits)
  123. .set('maxSegmentDuration', parseSeconds)
  124. .set('pauseTime', parseTime)
  125. .set('playTime', parseTime)
  126. .set('corruptedFrames', parseFrames)
  127. .set('decodedFrames', parseFrames)
  128. .set('droppedFrames', parseFrames)
  129. .set('stallsDetected', parseStalls)
  130. .set('gapsJumped', parseGaps)
  131. .set('manifestSizeBytes', parseBytes)
  132. .set('bytesDownloaded', parseBytes)
  133. .set('nonFatalErrorCount', parseErrors)
  134. .set('manifestPeriodCount', parsePeriods)
  135. .set('manifestGapCount', parseGaps);
  136. /** @private {shaka.util.Timer} */
  137. this.timer_ = new shaka.util.Timer(() => {
  138. this.onTimerTick_();
  139. });
  140. this.updateLocalizedStrings_();
  141. this.loadContainer_();
  142. this.eventManager.listen(
  143. this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
  144. this.updateLocalizedStrings_();
  145. });
  146. this.eventManager.listen(
  147. this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => {
  148. this.updateLocalizedStrings_();
  149. });
  150. this.eventManager.listen(this.button_, 'click', () => {
  151. this.onClick_();
  152. this.updateLocalizedStrings_();
  153. });
  154. }
  155. /** @private */
  156. onClick_() {
  157. if (this.container_.classList.contains('shaka-hidden')) {
  158. this.icon_.textContent =
  159. shaka.ui.Enums.MaterialDesignIcons.STATISTICS_OFF;
  160. this.timer_.tickEvery(0.1);
  161. shaka.ui.Utils.setDisplay(this.container_, true);
  162. } else {
  163. this.icon_.textContent =
  164. shaka.ui.Enums.MaterialDesignIcons.STATISTICS_ON;
  165. this.timer_.stop();
  166. shaka.ui.Utils.setDisplay(this.container_, false);
  167. }
  168. }
  169. /** @private */
  170. updateLocalizedStrings_() {
  171. const LocIds = shaka.ui.Locales.Ids;
  172. this.nameSpan_.textContent =
  173. this.localization.resolve(LocIds.STATISTICS);
  174. this.button_.ariaLabel = this.localization.resolve(LocIds.STATISTICS);
  175. const labelText = this.container_.classList.contains('shaka-hidden') ?
  176. LocIds.OFF : LocIds.ON;
  177. this.stateSpan_.textContent = this.localization.resolve(labelText);
  178. }
  179. /**
  180. * @param {string} name
  181. * @return {!HTMLElement}
  182. * @private
  183. */
  184. generateComponent_(name) {
  185. const section = shaka.util.Dom.createHTMLElement('div');
  186. const label = shaka.util.Dom.createHTMLElement('label');
  187. label.textContent = name + ':';
  188. section.appendChild(label);
  189. const value = shaka.util.Dom.createHTMLElement('span');
  190. value.textContent = this.parseFrom_.get(name)(name);
  191. section.appendChild(value);
  192. this.displayedElements_.set(name, value);
  193. return section;
  194. }
  195. /** @private */
  196. loadContainer_() {
  197. const closeElement = shaka.util.Dom.createHTMLElement('div');
  198. closeElement.classList.add('shaka-no-propagation');
  199. closeElement.classList.add('shaka-statistics-close');
  200. const icon = shaka.util.Dom.createHTMLElement('i');
  201. icon.classList.add('material-icons-round');
  202. icon.textContent =
  203. shaka.ui.Enums.MaterialDesignIcons.CLOSE;
  204. closeElement.appendChild(icon);
  205. this.container_.appendChild(closeElement);
  206. this.eventManager.listen(icon, 'click', () => {
  207. this.onClick_();
  208. });
  209. for (const name of this.controls.getConfig().statisticsList) {
  210. if (name in this.currentStats_ && !this.skippedStats_.includes(name)) {
  211. const element = this.generateComponent_(name);
  212. this.container_.appendChild(element);
  213. this.statisticsList_.push(name);
  214. } else {
  215. shaka.log.alwaysWarn('Unrecognized statistic element:', name);
  216. }
  217. }
  218. }
  219. /** @private */
  220. onTimerTick_() {
  221. this.currentStats_ = this.player.getStats();
  222. for (const name of this.statisticsList_) {
  223. const element = this.displayedElements_.get(name);
  224. element.textContent = this.parseFrom_.get(name)(name);
  225. if (element && element.parentElement) {
  226. shaka.ui.Utils.setDisplay(element.parentElement,
  227. !isNaN(this.currentStats_[name]));
  228. }
  229. }
  230. }
  231. /** @override */
  232. release() {
  233. this.timer_.stop();
  234. this.timer_ = null;
  235. super.release();
  236. }
  237. };
  238. /**
  239. * @implements {shaka.extern.IUIElement.Factory}
  240. * @final
  241. */
  242. shaka.ui.StatisticsButton.Factory = class {
  243. /** @override */
  244. create(rootElement, controls) {
  245. return new shaka.ui.StatisticsButton(rootElement, controls);
  246. }
  247. };
  248. shaka.ui.OverflowMenu.registerElement(
  249. 'statistics', new shaka.ui.StatisticsButton.Factory());
  250. shaka.ui.ContextMenu.registerElement(
  251. 'statistics', new shaka.ui.StatisticsButton.Factory());