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. if (this.player.isAudioOnly()) {
  98. tracks = tracks.filter((track, idx) => {
  99. return tracks.findIndex((t) => t.bandwidth == track.bandwidth) == idx;
  100. });
  101. } else {
  102. const audiosIds = [...new Set(tracks.map((t) => t.audioId))];
  103. if (audiosIds.length > 1) {
  104. tracks = tracks.filter((track, idx) => {
  105. // Keep the first one with the same height and framerate or bandwidth.
  106. const otherIdx = tracks.findIndex((t) => {
  107. return t.height == track.height &&
  108. t.videoBandwidth == track.videoBandwidth &&
  109. t.frameRate == track.frameRate &&
  110. t.hdr == track.hdr &&
  111. t.videoLayout == track.videoLayout;
  112. });
  113. return otherIdx == idx;
  114. });
  115. } else {
  116. tracks = tracks.filter((track, idx) => {
  117. // Keep the first one with the same height and framerate or bandwidth.
  118. const otherIdx = tracks.findIndex((t) => {
  119. return t.height == track.height &&
  120. t.bandwidth == track.bandwidth &&
  121. t.frameRate == track.frameRate &&
  122. t.hdr == track.hdr &&
  123. t.videoLayout == track.videoLayout;
  124. });
  125. return otherIdx == idx;
  126. });
  127. }
  128. }
  129. // Sort the tracks by height or bandwidth depending on content type.
  130. if (this.player.isAudioOnly()) {
  131. tracks.sort((t1, t2) => {
  132. goog.asserts.assert(t1.bandwidth != null, 'Null bandwidth');
  133. goog.asserts.assert(t2.bandwidth != null, 'Null bandwidth');
  134. return t2.bandwidth - t1.bandwidth;
  135. });
  136. } else {
  137. tracks.sort((t1, t2) => {
  138. if (t2.height == t1.height || t1.height == null || t2.height == null) {
  139. return t2.bandwidth - t1.bandwidth;
  140. }
  141. return t2.height - t1.height;
  142. });
  143. }
  144. // Remove old shaka-resolutions
  145. // 1. Save the back to menu button
  146. const backButton = shaka.ui.Utils.getFirstDescendantWithClassName(
  147. this.menu, 'shaka-back-to-overflow-button');
  148. // 2. Remove everything
  149. shaka.util.Dom.removeAllChildren(this.menu);
  150. // 3. Add the backTo Menu button back
  151. this.menu.appendChild(backButton);
  152. const abrEnabled = this.player.getConfiguration().abr.enabled;
  153. // Add new ones
  154. for (const track of tracks) {
  155. const button = shaka.util.Dom.createButton();
  156. button.classList.add('explicit-resolution');
  157. this.eventManager.listen(button, 'click',
  158. () => this.onTrackSelected_(track));
  159. const span = shaka.util.Dom.createHTMLElement('span');
  160. if (!this.player.isAudioOnly() && track.height && track.width) {
  161. span.textContent = this.getResolutionLabel_(track, tracks);
  162. } else if (track.bandwidth) {
  163. span.textContent = Math.round(track.bandwidth / 1000) + ' kbits/s';
  164. } else {
  165. span.textContent = 'Unknown';
  166. }
  167. button.appendChild(span);
  168. if (!abrEnabled && track == selectedTrack) {
  169. // If abr is disabled, mark the selected track's resolution.
  170. button.ariaSelected = 'true';
  171. button.appendChild(shaka.ui.Utils.checkmarkIcon());
  172. span.classList.add('shaka-chosen-item');
  173. this.currentSelection.textContent = span.textContent;
  174. }
  175. this.menu.appendChild(button);
  176. }
  177. // Add the Auto button
  178. const autoButton = shaka.util.Dom.createButton();
  179. autoButton.classList.add('shaka-enable-abr-button');
  180. this.eventManager.listen(autoButton, 'click', () => {
  181. const config = {abr: {enabled: true}};
  182. this.player.configure(config);
  183. this.updateResolutionSelection_();
  184. });
  185. /** @private {!HTMLElement}*/
  186. this.abrOnSpan_ = shaka.util.Dom.createHTMLElement('span');
  187. this.abrOnSpan_.classList.add('shaka-auto-span');
  188. this.abrOnSpan_.textContent =
  189. this.localization.resolve(shaka.ui.Locales.Ids.AUTO_QUALITY);
  190. autoButton.appendChild(this.abrOnSpan_);
  191. // If abr is enabled reflect it by marking 'Auto' as selected.
  192. if (abrEnabled) {
  193. autoButton.ariaSelected = 'true';
  194. autoButton.appendChild(shaka.ui.Utils.checkmarkIcon());
  195. this.abrOnSpan_.classList.add('shaka-chosen-item');
  196. this.currentSelection.textContent =
  197. this.localization.resolve(shaka.ui.Locales.Ids.AUTO_QUALITY);
  198. }
  199. this.button.setAttribute('shaka-status', this.currentSelection.textContent);
  200. this.menu.appendChild(autoButton);
  201. shaka.ui.Utils.focusOnTheChosenItem(this.menu);
  202. this.controls.dispatchEvent(
  203. new shaka.util.FakeEvent('resolutionselectionupdated'));
  204. this.updateLocalizedStrings_();
  205. shaka.ui.Utils.setDisplay(this.button, tracks.length > 1);
  206. }
  207. /**
  208. * @param {!shaka.extern.Track} track
  209. * @param {!Array.<!shaka.extern.Track>} tracks
  210. * @return {string}
  211. * @private
  212. */
  213. getResolutionLabel_(track, tracks) {
  214. const trackHeight = track.height || 0;
  215. const trackWidth = track.width || 0;
  216. let height = trackHeight;
  217. const aspectRatio = trackWidth / trackHeight;
  218. if (aspectRatio > (16 / 9)) {
  219. height = Math.round(trackWidth * 9 / 16);
  220. }
  221. let text = height + 'p';
  222. if (height == 2160) {
  223. text = '4K';
  224. }
  225. const frameRates = new Set();
  226. for (const item of tracks) {
  227. if (item.frameRate) {
  228. frameRates.add(Math.round(item.frameRate));
  229. }
  230. }
  231. if (frameRates.size > 1) {
  232. const frameRate = track.frameRate;
  233. if (frameRate && (frameRate >= 50 || frameRate <= 20)) {
  234. text += Math.round(track.frameRate);
  235. }
  236. }
  237. if (track.hdr == 'PQ' || track.hdr == 'HLG') {
  238. text += ' (HDR)';
  239. }
  240. if (track.videoLayout == 'CH-STEREO') {
  241. text += ' (3D)';
  242. }
  243. const hasDuplicateResolution = tracks.some((otherTrack) => {
  244. return otherTrack != track && otherTrack.height == track.height;
  245. });
  246. if (hasDuplicateResolution) {
  247. const bandwidth = track.videoBandwidth || track.bandwidth;
  248. text += ' (' + Math.round(bandwidth / 1000) + ' kbits/s)';
  249. }
  250. return text;
  251. }
  252. /**
  253. * @param {!shaka.extern.Track} track
  254. * @private
  255. */
  256. onTrackSelected_(track) {
  257. // Disable abr manager before changing tracks.
  258. const config = {abr: {enabled: false}};
  259. this.player.configure(config);
  260. const clearBuffer = this.controls.getConfig().clearBufferOnQualityChange;
  261. this.player.selectVariantTrack(track, clearBuffer);
  262. }
  263. /**
  264. * @private
  265. */
  266. updateLocalizedStrings_() {
  267. const LocIds = shaka.ui.Locales.Ids;
  268. const locId = this.player.isAudioOnly() ?
  269. LocIds.QUALITY : LocIds.RESOLUTION;
  270. this.button.ariaLabel = this.localization.resolve(locId);
  271. this.backButton.ariaLabel = this.localization.resolve(locId);
  272. this.backSpan.textContent =
  273. this.localization.resolve(locId);
  274. this.nameSpan.textContent =
  275. this.localization.resolve(locId);
  276. this.abrOnSpan_.textContent =
  277. this.localization.resolve(LocIds.AUTO_QUALITY);
  278. if (this.player.getConfiguration().abr.enabled) {
  279. this.currentSelection.textContent =
  280. this.localization.resolve(shaka.ui.Locales.Ids.AUTO_QUALITY);
  281. }
  282. }
  283. };
  284. /**
  285. * @implements {shaka.extern.IUIElement.Factory}
  286. * @final
  287. */
  288. shaka.ui.ResolutionSelection.Factory = class {
  289. /** @override */
  290. create(rootElement, controls) {
  291. return new shaka.ui.ResolutionSelection(rootElement, controls);
  292. }
  293. };
  294. shaka.ui.OverflowMenu.registerElement(
  295. 'quality', new shaka.ui.ResolutionSelection.Factory());
  296. shaka.ui.Controls.registerElement(
  297. 'quality', new shaka.ui.ResolutionSelection.Factory());