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