From adc3502d55f39eaca30f3c42e17961ec7d681c80 Mon Sep 17 00:00:00 2001 From: Nbcl <48532672+nbcl@users.noreply.github.com> Date: Thu, 21 Apr 2022 18:43:51 -0400 Subject: [PATCH] feat(ui): Add quality selection for audio-only content (#3649) Replaces resolution menu with audio quality menu when content is audio-only. Fixes: #2071 --- test/ui/ui_integration.js | 16 ---------- test/ui/ui_unit.js | 27 ++++++++++++++++ ui/locales/en.json | 1 + ui/locales/source.json | 5 +++ ui/resolution_selection.js | 64 ++++++++++++++++++-------------------- 5 files changed, 64 insertions(+), 49 deletions(-) diff --git a/test/ui/ui_integration.js b/test/ui/ui_integration.js index dfe9fe2095..66b8eace23 100644 --- a/test/ui/ui_integration.js +++ b/test/ui/ui_integration.js @@ -454,22 +454,6 @@ describe('UI', () => { expect(isChosen).not.toBe(null); }); - it('restores the resolutions menu after audio-only playback', async () => { - /** @type {HTMLElement} */ - const resolutionButton = shaka.util.Dom.getElementByClassName( - 'shaka-resolution-button', videoContainer); - - // Load an audio-only clip. The menu should be hidden. - await player.load('test:sintel_audio_only_compiled'); - expect(player.isAudioOnly()).toBe(true); - expect(resolutionButton.classList.contains('shaka-hidden')).toBe(true); - - // Load an audio-video clip. The menu should be visible again. - await player.load('test:sintel_multi_lingual_multi_res_compiled'); - expect(player.isAudioOnly()).toBe(false); - expect(resolutionButton.classList.contains('shaka-hidden')).toBe(false); - }); - /** * @return {Element} */ diff --git a/test/ui/ui_unit.js b/test/ui/ui_unit.js index 3417056ec0..527abee762 100644 --- a/test/ui/ui_unit.js +++ b/test/ui/ui_unit.js @@ -642,6 +642,33 @@ describe('UI', () => { expect(getResolutions()).toEqual(['540p']); }); + it('displays audio quality based on current stream', async () => { + const manifest = + shaka.test.ManifestGenerator.generate((manifest) => { + manifest.addVariant(0, (variant) => { + variant.addAudio(0); + variant.bandwidth = 100000; + }); + manifest.addVariant(1, (variant) => { + variant.addAudio(1); + variant.bandwidth = 200000; + }); + }); + + shaka.media.ManifestParser.registerParserByMime( + fakeMimeType, () => new shaka.test.FakeManifestParser(manifest)); + + await player.load( + /* uri= */ 'fake', /* startTime= */ 0, fakeMimeType); + + const qualityButtons = videoContainer.querySelectorAll( + 'button.explicit-resolution > span'); + const qualityOptions = + Array.from(qualityButtons).map((btn) => btn.innerText); + + expect(qualityOptions).toEqual(['200 kbits/s', '100 kbits/s']); + }); + /** * Use internals to update the resolution menu. Our fake manifest can * cause problems with startup where the Player will get stuck using diff --git a/ui/locales/en.json b/ui/locales/en.json index f968fafd3d..d9894411e7 100644 --- a/ui/locales/en.json +++ b/ui/locales/en.json @@ -27,6 +27,7 @@ "PICTURE_IN_PICTURE": "Picture-in-Picture", "PLAY": "Play", "PLAYBACK_RATE": "Playback speed", + "QUALITY": "Quality", "REPLAY": "Replay", "RESOLUTION": "Resolution", "REWIND": "Rewind", diff --git a/ui/locales/source.json b/ui/locales/source.json index 1070d5a4c1..66b7d2bac0 100644 --- a/ui/locales/source.json +++ b/ui/locales/source.json @@ -113,6 +113,11 @@ "message": "Playback speed", "description": "Label for a button used to navigate to a submenu to choose the playback speed in the video player." }, + "QUALITY": { + "description": "Label for a button used to open a submenu to choose audio quality in the video player.", + "meaning": "Audio quality", + "message": "Quality" + }, "REPLAY": { "description": "Label for a button used to replay the video in a video player.", "message": "Replay" diff --git a/ui/resolution_selection.js b/ui/resolution_selection.js index ba9fc688cd..b0b7a8d199 100644 --- a/ui/resolution_selection.js +++ b/ui/resolution_selection.js @@ -61,9 +61,6 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { }); this.updateResolutionSelection_(); - - // Set up all the strings in the user's preferred language. - this.updateLocalizedStrings_(); } @@ -72,27 +69,6 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { /** @type {!Array.} */ let tracks = this.player.getVariantTracks(); - // Hide resolution menu and button for audio-only content and src= content - // without resolution information. - // TODO: for audio-only content, this should be a bitrate selection menu - // instead. - if (tracks.length && !tracks[0].height) { - shaka.ui.Utils.setDisplay(this.menu, false); - shaka.ui.Utils.setDisplay(this.button, false); - return; - } - // Otherwise, restore it. - shaka.ui.Utils.setDisplay(this.button, true); - - tracks.sort((t1, t2) => { - // We have already screened for audio-only content, but the compiler - // doesn't know that. - goog.asserts.assert(t1.height != null, 'Null height'); - goog.asserts.assert(t2.height != null, 'Null height'); - - return t2.height - t1.height; - }); - // If there is a selected variant track, then we filter out any tracks in // a different language. Then we use those remaining tracks to display the // available resolutions. @@ -104,14 +80,31 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { track.channelsCount == selectedTrack.channelsCount); } - // Remove duplicate entries with the same height. This can happen if - // we have multiple resolutions of audio. Pick an arbitrary one. + // Remove duplicate entries with the same resolution or quality depending + // on content type. Pick an arbitrary one. tracks = tracks.filter((track, idx) => { - // Keep the first one with the same height. - const otherIdx = tracks.findIndex((t) => t.height == track.height); + // Keep the first one with the same height or bandwidth. + const otherIdx = this.player.isAudioOnly() ? + tracks.findIndex((t) => t.bandwidth == track.bandwidth) : + tracks.findIndex((t) => t.height == track.height); return otherIdx == idx; }); + // Sort the tracks by height or bandwith depending on content type. + if (this.player.isAudioOnly()) { + tracks.sort((t1, t2) => { + goog.asserts.assert(t1.bandwidth != null, 'Null bandwidth'); + goog.asserts.assert(t2.bandwidth != null, 'Null bandwidth'); + return t2.bandwidth - t1.bandwidth; + }); + } else { + tracks.sort((t1, t2) => { + goog.asserts.assert(t1.height != null, 'Null height'); + goog.asserts.assert(t2.height != null, 'Null height'); + return t2.height - t1.height; + }); + } + // Remove old shaka-resolutions // 1. Save the back to menu button const backButton = shaka.ui.Utils.getFirstDescendantWithClassName( @@ -133,7 +126,8 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { () => this.onTrackSelected_(track)); const span = shaka.util.Dom.createHTMLElement('span'); - span.textContent = track.height + 'p'; + span.textContent = this.player.isAudioOnly() ? + Math.round(track.bandwidth / 1000) + ' kbits/s' : track.height + 'p'; button.appendChild(span); if (!abrEnabled && track == selectedTrack) { @@ -179,6 +173,8 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { shaka.ui.Utils.focusOnTheChosenItem(this.menu); this.controls.dispatchEvent( new shaka.util.FakeEvent('resolutionselectionupdated')); + + this.updateLocalizedStrings_(); } @@ -200,13 +196,15 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.SettingsMenu { */ updateLocalizedStrings_() { const LocIds = shaka.ui.Locales.Ids; + const locId = this.player.isAudioOnly() ? + LocIds.QUALITY : LocIds.RESOLUTION; - this.button.ariaLabel = this.localization.resolve(LocIds.RESOLUTION); - this.backButton.ariaLabel = this.localization.resolve(LocIds.RESOLUTION); + this.button.ariaLabel = this.localization.resolve(locId); + this.backButton.ariaLabel = this.localization.resolve(locId); this.backSpan.textContent = - this.localization.resolve(LocIds.RESOLUTION); + this.localization.resolve(locId); this.nameSpan.textContent = - this.localization.resolve(LocIds.RESOLUTION); + this.localization.resolve(locId); this.abrOnSpan_.textContent = this.localization.resolve(LocIds.AUTO_QUALITY);