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