Source: ui/resolution_selection.js

  1. /*! @license
  2. * Shaka Player
  3. * Copyright 2016 Google LLC
  4. * SPDX-License-Identifier: Apache-2.0
  5. */
  6. goog.provide('shaka.ui.ResolutionSelection');
  7. goog.require('goog.asserts');
  8. goog.require('shaka.Player');
  9. goog.require('shaka.ui.Controls');
  10. goog.require('shaka.ui.Enums');
  11. goog.require('shaka.ui.Locales');
  12. goog.require('shaka.ui.Localization');
  13. goog.require('shaka.ui.OverflowMenu');
  14. goog.require('shaka.ui.SettingsMenu');
  15. goog.require('shaka.ui.Utils');
  16. goog.require('shaka.util.Dom');
  17. goog.require('shaka.util.FakeEvent');
  18. goog.requireType('shaka.ui.Controls');
  19. /**
  20. * @extends {shaka.ui.SettingsMenu}
  21. * @final
  22. * @export
  23. */
  24. shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu {
  25. /**
  26. * @param {!HTMLElement} parent
  27. * @param {!shaka.ui.Controls} controls
  28. */
  29. constructor(parent, controls) {
  30. super(parent, controls, shaka.ui.Enums.MaterialDesignIcons.RESOLUTION);
  31. this.button.classList.add('shaka-resolution-button');
  32. this.button.classList.add('shaka-tooltip-status');
  33. this.menu.classList.add('shaka-resolutions');
  34. this.eventManager.listen(
  35. this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => {
  36. this.updateLocalizedStrings_();
  37. });
  38. this.eventManager.listen(
  39. this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => {
  40. this.updateLocalizedStrings_();
  41. });
  42. this.eventManager.listen(this.player, 'variantchanged', () => {
  43. this.updateResolutionSelection_();
  44. });
  45. this.eventManager.listen(this.player, 'trackschanged', () => {
  46. this.updateResolutionSelection_();
  47. });
  48. this.eventManager.listen(this.player, 'abrstatuschanged', () => {
  49. this.updateResolutionSelection_();
  50. });
  51. this.updateResolutionSelection_();
  52. }
  53. /** @private */
  54. updateResolutionSelection_() {
  55. /** @type {!Array.<shaka.extern.Track>} */
  56. let tracks = [];
  57. // When played with src=, the variant tracks available from
  58. // player.getVariantTracks() represent languages, not resolutions.
  59. if (this.player.getLoadMode() != shaka.Player.LoadMode.SRC_EQUALS) {
  60. tracks = this.player.getVariantTracks();
  61. }
  62. // If there is a selected variant track, then we filter out any tracks in
  63. // a different language. Then we use those remaining tracks to display the
  64. // available resolutions.
  65. const selectedTrack = tracks.find((track) => track.active);
  66. if (selectedTrack) {
  67. // Filter by current audio language and channel count.
  68. tracks = tracks.filter(
  69. (track) => track.language == selectedTrack.language &&
  70. track.channelsCount == selectedTrack.channelsCount);
  71. }
  72. // Remove duplicate entries with the same resolution or quality depending
  73. // on content type. Pick an arbitrary one.
  74. tracks = tracks.filter((track, idx) => {
  75. // Keep the first one with the same height and framrate or bandwidth.
  76. let otherIdx = -1;
  77. if (this.player.isAudioOnly()) {
  78. otherIdx = tracks.findIndex((t) => t.bandwidth == track.bandwidth);
  79. } else {
  80. otherIdx = tracks.findIndex((t) => {
  81. return t.height == track.height &&
  82. t.frameRate == track.frameRate &&
  83. t.hdr == track.hdr &&
  84. t.videoLayout == track.videoLayout;
  85. });
  86. }
  87. return otherIdx == idx;
  88. });
  89. // Sort the tracks by height or bandwidth depending on content type.
  90. if (this.player.isAudioOnly()) {
  91. tracks.sort((t1, t2) => {
  92. goog.asserts.assert(t1.bandwidth != null, 'Null bandwidth');
  93. goog.asserts.assert(t2.bandwidth != null, 'Null bandwidth');
  94. return t2.bandwidth - t1.bandwidth;
  95. });
  96. } else {
  97. tracks.sort((t1, t2) => {
  98. goog.asserts.assert(t1.height != null, 'Null height');
  99. goog.asserts.assert(t2.height != null, 'Null height');
  100. return t2.height - t1.height;
  101. });
  102. }
  103. // Remove old shaka-resolutions
  104. // 1. Save the back to menu button
  105. const backButton = shaka.ui.Utils.getFirstDescendantWithClassName(
  106. this.menu, 'shaka-back-to-overflow-button');
  107. // 2. Remove everything
  108. shaka.util.Dom.removeAllChildren(this.menu);
  109. // 3. Add the backTo Menu button back
  110. this.menu.appendChild(backButton);
  111. const abrEnabled = this.player.getConfiguration().abr.enabled;
  112. // Add new ones
  113. for (const track of tracks) {
  114. const button = shaka.util.Dom.createButton();
  115. button.classList.add('explicit-resolution');
  116. this.eventManager.listen(button, 'click',
  117. () => this.onTrackSelected_(track));
  118. const span = shaka.util.Dom.createHTMLElement('span');
  119. if (this.player.isAudioOnly() && track.bandwidth) {
  120. span.textContent = Math.round(track.bandwidth / 1000) + ' kbits/s';
  121. } else if (track.height && track.width) {
  122. span.textContent = this.getResolutionLabel_(track);
  123. } else {
  124. span.textContent = 'Unknown';
  125. }
  126. button.appendChild(span);
  127. if (!abrEnabled && track == selectedTrack) {
  128. // If abr is disabled, mark the selected track's resolution.
  129. button.ariaSelected = 'true';
  130. button.appendChild(shaka.ui.Utils.checkmarkIcon());
  131. span.classList.add('shaka-chosen-item');
  132. this.currentSelection.textContent = span.textContent;
  133. }
  134. this.menu.appendChild(button);
  135. }
  136. // Add the Auto button
  137. const autoButton = shaka.util.Dom.createButton();
  138. autoButton.classList.add('shaka-enable-abr-button');
  139. this.eventManager.listen(autoButton, 'click', () => {
  140. const config = {abr: {enabled: true}};
  141. this.player.configure(config);
  142. this.updateResolutionSelection_();
  143. });
  144. /** @private {!HTMLElement}*/
  145. this.abrOnSpan_ = shaka.util.Dom.createHTMLElement('span');
  146. this.abrOnSpan_.classList.add('shaka-auto-span');
  147. this.abrOnSpan_.textContent =
  148. this.localization.resolve(shaka.ui.Locales.Ids.AUTO_QUALITY);
  149. autoButton.appendChild(this.abrOnSpan_);
  150. // If abr is enabled reflect it by marking 'Auto' as selected.
  151. if (abrEnabled) {
  152. autoButton.ariaSelected = 'true';
  153. autoButton.appendChild(shaka.ui.Utils.checkmarkIcon());
  154. this.abrOnSpan_.classList.add('shaka-chosen-item');
  155. this.currentSelection.textContent =
  156. this.localization.resolve(shaka.ui.Locales.Ids.AUTO_QUALITY);
  157. }
  158. this.button.setAttribute('shaka-status', this.currentSelection.textContent);
  159. this.menu.appendChild(autoButton);
  160. shaka.ui.Utils.focusOnTheChosenItem(this.menu);
  161. this.controls.dispatchEvent(
  162. new shaka.util.FakeEvent('resolutionselectionupdated'));
  163. this.updateLocalizedStrings_();
  164. }
  165. /**
  166. * @param {!shaka.extern.Track} track
  167. * @return {string}
  168. * @private
  169. */
  170. getResolutionLabel_(track) {
  171. const trackHeight = track.height || 0;
  172. const trackWidth = track.width || 0;
  173. let height = trackHeight;
  174. const aspectRatio = trackWidth / trackHeight;
  175. if (aspectRatio > (16 / 9)) {
  176. height = Math.round(trackWidth * 9 / 16);
  177. }
  178. let text = height + 'p';
  179. if (height == 2160) {
  180. text = '4K';
  181. }
  182. const frameRate = track.frameRate;
  183. if (frameRate && (frameRate >= 50 || frameRate <= 20)) {
  184. text += Math.round(track.frameRate);
  185. }
  186. if (track.hdr == 'PQ' || track.hdr == 'HLG') {
  187. text += ' (HDR)';
  188. }
  189. if (track.videoLayout == 'CH-STEREO') {
  190. text += ' (3D)';
  191. }
  192. return text;
  193. }
  194. /**
  195. * @param {!shaka.extern.Track} track
  196. * @private
  197. */
  198. onTrackSelected_(track) {
  199. // Disable abr manager before changing tracks.
  200. const config = {abr: {enabled: false}};
  201. this.player.configure(config);
  202. const clearBuffer = this.controls.getConfig().clearBufferOnQualityChange;
  203. this.player.selectVariantTrack(track, clearBuffer);
  204. }
  205. /**
  206. * @private
  207. */
  208. updateLocalizedStrings_() {
  209. const LocIds = shaka.ui.Locales.Ids;
  210. const locId = this.player.isAudioOnly() ?
  211. LocIds.QUALITY : LocIds.RESOLUTION;
  212. this.button.ariaLabel = this.localization.resolve(locId);
  213. this.backButton.ariaLabel = this.localization.resolve(locId);
  214. this.backSpan.textContent =
  215. this.localization.resolve(locId);
  216. this.nameSpan.textContent =
  217. this.localization.resolve(locId);
  218. this.abrOnSpan_.textContent =
  219. this.localization.resolve(LocIds.AUTO_QUALITY);
  220. if (this.player.getConfiguration().abr.enabled) {
  221. this.currentSelection.textContent =
  222. this.localization.resolve(shaka.ui.Locales.Ids.AUTO_QUALITY);
  223. }
  224. }
  225. };
  226. /**
  227. * @implements {shaka.extern.IUIElement.Factory}
  228. * @final
  229. */
  230. shaka.ui.ResolutionSelection.Factory = class {
  231. /** @override */
  232. create(rootElement, controls) {
  233. return new shaka.ui.ResolutionSelection(rootElement, controls);
  234. }
  235. };
  236. shaka.ui.OverflowMenu.registerElement(
  237. 'quality', new shaka.ui.ResolutionSelection.Factory());
  238. shaka.ui.Controls.registerElement(
  239. 'quality', new shaka.ui.ResolutionSelection.Factory());