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