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