UNPKG

shaka-player

Version:
634 lines (503 loc) 19.2 kB
/** * @license * Copyright 2016 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ describe('UI', () => { const Util = shaka.test.Util; /** @type {!jasmine.Spy} */ let onErrorSpy; /** @type {!HTMLVideoElement} */ let video; /** @type {!HTMLElement} */ let videoContainer; /** @type {!shaka.Player} */ let player; /** @type {shaka.util.EventManager} */ let eventManager; /** @type {shaka.test.Waiter} */ let waiter; /** @type {!Element} */ let cssLink; /** @type {!shaka.ui.Controls} */ let controls; let compiledShaka; beforeAll(async () => { cssLink = document.createElement('link'); await Util.setupCSS(cssLink); compiledShaka = await Util.loadShaka(getClientArg('uncompiled')); await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled'); }); beforeEach(async () => { video = shaka.util.Dom.createVideoElement(); videoContainer = shaka.util.Dom.createHTMLElement('div'); videoContainer.appendChild(video); document.body.appendChild(videoContainer); player = new compiledShaka.Player(video); // Create UI // Add all of the buttons we have const config = { controlPanelElements: [ 'time_and_duration', 'mute', 'volume', 'fullscreen', 'overflow_menu', 'fast_forward', 'rewind', ], overflowMenuButtons: [ 'captions', 'quality', 'language', 'picture_in_picture', 'cast', ], // TODO: Cast receiver id to test chromecast integration }; const ui = new compiledShaka.ui.Overlay(player, videoContainer, video); ui.configure(config); // Grab event manager from the uncompiled library: eventManager = new shaka.util.EventManager(); waiter = new shaka.test.Waiter(eventManager); controls = ui.getControls(); onErrorSpy = jasmine.createSpy('onError'); onErrorSpy.and.callFake(function(event) { fail(event.detail); }); eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy)); eventManager.listen(controls, 'error', Util.spyFunc(onErrorSpy)); await player.load('test:sintel_multi_lingual_multi_res_compiled'); // For this event, we ignore a timeout, since we sometimes miss this event // on Tizen. But expect that the video is ready anyway. await waiter.failOnTimeout(false).waitForEvent(video, 'canplay'); expect(video.readyState).not.toEqual(0); // All other events after this should fail on timeout (the default). await waiter.failOnTimeout(true); }); afterEach(async () => { eventManager.release(); waiter = null; await shaka.test.Util.cleanupUI(); }); afterAll(() => { document.head.removeChild(cssLink); }); describe('language selections', () => { /** @type {!Map.<string, !HTMLElement>} */ let languagesToButtons; /** @type {!Array.<string>} */ let langsFromContent; /** @type {!Array.<!HTMLElement>} */ let languageButtons; /** @type {!Element} */ let languageMenu; /** @type {string} */ let oldLanguage; /** @type {string} */ let newLanguage; describe('audio', () => { beforeEach(() => { oldLanguage = 'en'; newLanguage = 'es'; languageMenu = shaka.util.Dom.getElementByClassName( 'shaka-audio-languages', videoContainer); setupLanguageTests(player.getAudioLanguagesAndRoles()); }); it('contains all the languages', () => { verifyLanguages(); }); it('choosing language through UI has effect on player', async () => { await verifyLanguageChangeViaUI( 'variantchanged', () => player.getVariantTracks()); }); it('choosing language through API has effect on UI', async () => { await verifyLanguageChangeViaAPI( 'languageselectionupdated', () => player.getVariantTracks(), (language) => player.selectAudioLanguage(language)); }); }); describe('caption selection', () => { beforeEach(() => { oldLanguage = 'zh'; newLanguage = 'fr'; languageMenu = shaka.util.Dom.getElementByClassName( 'shaka-text-languages', videoContainer); setupLanguageTests(player.getTextLanguagesAndRoles()); }); it('contains all the languages', () => { verifyLanguages(); }); it('choosing caption language through UI has effect on player', async () => { await verifyLanguageChangeViaUI( 'textchanged', () => player.getTextTracks()); }); it('choosing language through API has effect on UI', async () => { // Enable & verify the text, or else the text won't be streamed and the // language selection won't do anything. await player.setTextTrackVisibility(true); expect(player.isTextTrackVisible()).toBe(true); await verifyLanguageChangeViaAPI( 'captionselectionupdated', () => player.getTextTracks(), (language) => player.selectTextLanguage(language)); }); it('turning captions off through UI has effect on player', async () => { // Enable & verify the text. await player.setTextTrackVisibility(true); expect(player.isTextTrackVisible()).toBe(true); // Find and click the 'Off' button getOffButton().click(); // Wait for the change to take effect await waiter.waitForEvent(player, 'texttrackvisibility'); expect(player.isTextTrackVisible()).toBe(false); }); it('turning captions off through API has effect on UI', async () => { // This test is invalid if the text is not initially visible, because // setTextTrackVisibility() does nothing if there are no changes. await player.setTextTrackVisibility(true); expect(player.isTextTrackVisible()).toBe(true); const p = waiter.waitForEvent(controls, 'captionselectionupdated'); // Disable & verify the text. await player.setTextTrackVisibility(false); expect(player.isTextTrackVisible()).toBe(false); // Wait for the change to take effect await p; const offButtonChosen = getOffButton().querySelector('.shaka-chosen-item'); expect(offButtonChosen).not.toBe(null); }); /** * @return {Element} */ function getOffButton() { const offButton = languageMenu.querySelector('.shaka-turn-captions-off-button'); expect(offButton).not.toBe(null); return offButton; } }); /** * @param {!Array.<shaka.extern.LanguageRole>} languagesAndRoles */ function setupLanguageTests(languagesAndRoles) { langsFromContent = languagesAndRoles.map((langAndRole) => { return langAndRole.language; }); languageButtons = filterButtons(languageMenu.childNodes, ['shaka-back-to-overflow-button', 'shaka-turn-captions-off-button']); languagesToButtons = mapChoicesToButtons( /* allButtons= */ languageButtons, /* choices= */ langsFromContent, /* modifier= */ getNativeName, ); } /** * @param {string} language * @return {string} */ function getNativeName(language) { return mozilla.LanguageMapping[language].nativeName; } /** * Make sure languages specified by the manifest match what we show on UI. */ function verifyLanguages() { const langsFromContentNative = langsFromContent.map((lang) => { return getNativeName(lang); }); verifyItems(langsFromContentNative, languageButtons); } /** * @param {string} playerEventName * @param {function():!Array.<!shaka.extern.Track>} getTracks */ async function verifyLanguageChangeViaUI(playerEventName, getTracks) { expect(getSelectedTrack(getTracks()).language).toEqual(oldLanguage); const button = languagesToButtons.get(newLanguage); button.click(); // Wait for the change to take effect await waiter.waitForEvent(player, playerEventName); expect(getSelectedTrack(getTracks()).language).toEqual(newLanguage); } /** * @param {string} controlsEventName * @param {function():!Array.<!shaka.extern.Track>} getTracks * @param {function(string)} selectLanguage */ async function verifyLanguageChangeViaAPI( controlsEventName, getTracks, selectLanguage) { expect(getSelectedTrack(getTracks()).language).toEqual(oldLanguage); const p = waiter.waitForEvent(controls, controlsEventName); selectLanguage(newLanguage); // Wait for the UI to get updated await p; // Buttons were re-created on variant change languageButtons = filterButtons(languageMenu.childNodes, ['shaka-back-to-overflow-button', 'shaka-turn-captions-off-button']); languagesToButtons = mapChoicesToButtons( /* allButtons= */ languageButtons, /* choices */ langsFromContent, /* modifier */ getNativeName ); const button = languagesToButtons.get(newLanguage); const isChosen = button.querySelector('.shaka-chosen-item'); expect(isChosen).not.toBe(null); } }); describe('resolution selection', () => { /** @type {!Map.<number, !HTMLElement>} */ let resolutionsToButtons; /** @type {!Array.<number>} */ let resolutionsFromContent; /** @type {!Array.<!HTMLElement>} */ let resolutionButtons; /** @type {!Element} */ let resolutionsMenu; /** @type {number} */ let oldResolution; /** @type {number} */ let newResolution; /** @type {!Array.<shaka.extern.Track>} */ let tracks; /** @type {string} */ let preferredLanguage; /** @type {!shaka.extern.Track} */ let oldResolutionTrack; beforeEach(async () => { oldResolution = 182; newResolution = 272; // Chosen language affects which resolutions get // displayed in the UI. preferredLanguage = 'en'; // Disable abr for the resolution tests player.configure('abr.enabled', false); const selectedLanguage = getSelectedTrack(player.getVariantTracks()).language; if (selectedLanguage != preferredLanguage) { player.selectAudioLanguage(preferredLanguage); await waiter.waitForEvent(player, 'variantchanged'); } resolutionsMenu = shaka.util.Dom.getElementByClassName( 'shaka-resolutions', videoContainer); updateResolutionButtonsAndMap(); oldResolutionTrack = findTrackWithHeight(tracks, oldResolution); }); it('contains all the relevant resolutions', () => { const formattedResolutions = resolutionsFromContent.map((res) => { return formatResolution(res); }); verifyItems(formattedResolutions, resolutionButtons); }); it('changing resolution via UI has effect on the player', async () => { player.selectVariantTrack(oldResolutionTrack); // Wait for the change to take effect await waiter.waitForEvent(player, 'variantchanged'); // Update the tracks tracks = player.getVariantTracks(); expect(getSelectedTrack(tracks).height).toEqual(oldResolution); const button = resolutionsToButtons.get(newResolution); button.click(); // Wait for the change to take effect await waiter.waitForEvent(player, 'variantchanged'); // Update the tracks tracks = player.getVariantTracks(); expect(getSelectedTrack(tracks).height).toEqual(newResolution); }); it('changing resolution via API has effect on the UI', async () => { // Start with the old resolution player.selectVariantTrack(oldResolutionTrack); // Wait for the change to take effect await waiter.waitForEvent(player, 'variantchanged'); updateResolutionButtonsAndMap(); expect(getSelectedTrack(tracks).height).toEqual(oldResolution); const p = waiter.waitForEvent(controls, 'resolutionselectionupdated'); const newResolutionTrack = findTrackWithHeight(tracks, newResolution); player.selectVariantTrack(newResolutionTrack); // Wait for the change to take effect await p; updateResolutionButtonsAndMap(); expect(getSelectedTrack(tracks).height).toEqual(newResolution); const button = resolutionsToButtons.get(newResolution); const isChosen = button.querySelector('.shaka-chosen-item'); expect(isChosen).not.toBe(null); }); it('selecting Auto via UI enables ABR', async () => { // We disabled abr in beforeEach() expect(player.getConfiguration().abr.enabled).toBe(false); const p = waiter.waitForEvent(controls, 'resolutionselectionupdated'); // Find the 'Auto' button const auto = getAutoButton(); auto.click(); await p; expect(player.getConfiguration().abr.enabled).toBe(true); }); it('selecting specific resolution disables ABR', async () => { const config = {abr: {enabled: true}}; player.configure(config); const p = waiter.waitForEvent(controls, 'resolutionselectionupdated'); // Any resolution would works const button = resolutionsToButtons.get(newResolution); button.click(); await p; expect(player.getConfiguration().abr.enabled).toBe(false); }); it('enabling ABR via API gets the Auto button selected', async () => { expect(player.getConfiguration().abr.enabled).toBe(false); // Setup listener to the ui event. The event, trigerring the update // is dispatched inside player.configure(), so we need to start // listening before calling it. const uiReady = waiter.waitForEvent( controls, 'resolutionselectionupdated'); const config = {abr: {enabled: true}}; player.configure(config); await uiReady; const auto = getAutoButton(); const isChosen = auto.querySelector('.shaka-chosen-item'); 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} */ function getAutoButton() { const auto = resolutionsMenu.querySelector('.shaka-enable-abr-button'); expect(auto).not.toBe(null); return auto; } /** * Gets the resolution to the same format it * appears in the UI: height + 'p'. * * @param {number} height * @return {string} */ function formatResolution(height) { return height.toString() + 'p'; } /** * @param {!Array.<!shaka.extern.Track>} tracks * @param {number} height * @return {shaka.extern.Track} */ function findTrackWithHeight(tracks, height) { let trackWithRes = null; for (const track of tracks) { if (track.height == height) { trackWithRes = track; } } goog.asserts.assert(trackWithRes != null, 'Should have found track!'); return trackWithRes; } function updateResolutionButtonsAndMap() { tracks = player.getVariantTracks(); tracks = tracks.filter((track) => { return track.language == preferredLanguage; }); resolutionsFromContent = tracks.map((track) => { return track.height; }); resolutionButtons = filterButtons( /* buttons= */ resolutionsMenu.childNodes, /* excludeClasses= */ [ 'shaka-back-to-overflow-button', 'shaka-enable-abr-button', ]); resolutionsToButtons = mapChoicesToButtons( /* buttons= */ resolutionButtons, /* choices= */ resolutionsFromContent, /* modifier=*/ formatResolution); } }); /** * @param {!Array.<!shaka.extern.Track>} tracks * @return {!shaka.extern.Track} */ function getSelectedTrack(tracks) { const activeTracks = tracks.filter((track) => { return track.active == true; }); return activeTracks[0]; } /** * @param {!Array.<!HTMLElement>} buttons * @param {!Array.<string>} choices * @param {function(string):string|function(number):string} modifier * @return {!Map.<string, !HTMLElement>|!Map.<number, !HTMLElement>} */ function mapChoicesToButtons(buttons, choices, modifier) { expect(buttons.length).toEqual(choices.length); const map = new Map(); // Find which choice corresponds to which button for (const choice of choices) { for (const button of buttons) { expect(button.childNodes.length).toBeGreaterThan(0); const uiOption = button.childNodes[0].textContent; const contentOption = modifier(choice); if (contentOption == uiOption) { map.set(choice, button); } } } return map; } /** * Filter out buttons with given classes. * * @param {!NodeList} buttons * @param {!Array.<string>} excludeClasses * @return {!Array.<!HTMLElement>} */ function filterButtons(buttons, excludeClasses) { return shaka.util.Iterables.filter(buttons, (node) => { const button = shaka.util.Dom.asHTMLElement(node); for (const excludeClass of excludeClasses) { if (button.classList.contains(excludeClass)) { return false; } } return true; }); } /** * Make sure elements from content match their UI representation. * (The order doesn't matter). * * @param {!Array.<string>} elementsFromContent * @param {!Array.<!HTMLElement>} elementsFromUI */ function verifyItems(elementsFromContent, elementsFromUI) { for (const element of elementsFromUI) { expect(element.childNodes.length).toBeGreaterThan(0); const elementName = element.childNodes[0].textContent; expect(elementsFromContent.indexOf(elementName)).not.toBe(-1); } } });