UNPKG

shaka-player

Version:
431 lines (366 loc) 12.6 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. */ goog.provide('shaka.ui.Overlay'); goog.provide('shaka.ui.Overlay.TrackLabelFormat'); goog.require('goog.asserts'); goog.require('shaka.Deprecate'); goog.require('shaka.polyfill.installAll'); goog.require('shaka.ui.Controls'); goog.require('shaka.ui.TextDisplayer'); goog.require('shaka.util.Platform'); /** * @param {!shaka.Player} player * @param {!HTMLElement} videoContainer * @param {!HTMLMediaElement} video * @implements {shaka.util.IDestroyable} * @constructor * @export */ shaka.ui.Overlay = function(player, videoContainer, video) { /** @private {shaka.Player} */ this.player_ = player; /** @private {!shaka.extern.UIConfiguration} */ this.config_ = this.defaultConfig_(); // Make sure this container is discoverable and that the UI can be reached // through it. videoContainer['dataset']['shakaPlayerContainer'] = ''; videoContainer['ui'] = this; // Tag the container for mobile platforms, to allow different styles. if (this.isMobile()) { videoContainer.classList.add('shaka-mobile'); } /** @private {shaka.ui.Controls} */ this.controls_ = new shaka.ui.Controls( player, videoContainer, video, this.config_); // Run the initial setup so that no configure() call is required for default // settings. this.configure({}); // If the browser's native controls are disabled, use UI TextDisplayer. // Arrow functions cannot be used with "new", so the factory must use // a "function" function. if (!video.controls) { // eslint-disable-next-line no-restricted-syntax const textDisplayer = function() { return new shaka.ui.TextDisplayer(video, videoContainer); }; player.configure('textDisplayFactory', textDisplayer); } videoContainer['ui'] = this; video['ui'] = this; }; /** * @override * @export */ shaka.ui.Overlay.prototype.destroy = async function() { if (this.controls_) { await this.controls_.destroy(); this.controls_ = null; } if (this.player_) { await this.player_.destroy(); this.player_ = null; } }; /** * Detects if this is a mobile platform, in case you want to choose a different * UI configuration on mobile devices. * * @return {boolean} * @export */ shaka.ui.Overlay.prototype.isMobile = function() { return shaka.util.Platform.isMobile(); }; /** * @return {!shaka.extern.UIConfiguration} * @export */ shaka.ui.Overlay.prototype.getConfiguration = function() { const ret = this.defaultConfig_(); shaka.util.ConfigUtils.mergeConfigObjects( ret, this.config_, this.defaultConfig_(), /* overrides (only used for player config)*/ {}, /* path */ ''); return ret; }; /** * @param {string|!Object} config This should either be a field name or an * object following the form of {@link shaka.extern.UIConfiguration}, where * you may omit any field you do not wish to change. * @param {*=} value This should be provided if the previous parameter * was a string field name. * @export */ shaka.ui.Overlay.prototype.configure = function(config, value) { goog.asserts.assert(typeof(config) == 'object' || arguments.length == 2, 'String configs should have values!'); // ('fieldName', value) format if (arguments.length == 2 && typeof(config) == 'string') { config = shaka.util.ConfigUtils.convertToConfigObject(config, value); } goog.asserts.assert(typeof(config) == 'object', 'Should be an object!'); shaka.util.ConfigUtils.mergeConfigObjects( this.config_, config, this.defaultConfig_(), /* overrides (only used for player config)*/ {}, /* path */ ''); // If a cast receiver app id has been given, add a cast button to the UI if (this.config_.castReceiverAppId && !this.config_.overflowMenuButtons.includes('cast')) { this.config_.overflowMenuButtons.push('cast'); } goog.asserts.assert(this.player_ != null, 'Should have a player!'); this.controls_.configure(this.config_); this.controls_.dispatchEvent(new shaka.util.FakeEvent('uiupdated')); }; /** * @return {shaka.Player} * @export * @deprecated Use getControls().getPlayer() instead. */ shaka.ui.Overlay.prototype.getPlayer = function() { shaka.Deprecate.deprecateFeature( 2, 6, 'ui.Overlay.getPlayer()', 'Please use getControls().getPlayer() instead.'); return this.controls_.getPlayer(); }; /** * @return {shaka.ui.Controls} * @export */ shaka.ui.Overlay.prototype.getControls = function() { return this.controls_; }; /** * Enable or disable the custom controls. * * @param {boolean} enabled * @export */ shaka.ui.Overlay.prototype.setEnabled = function(enabled) { this.controls_.setEnabledShakaControls(enabled); }; /** * @return {!shaka.extern.UIConfiguration} * @private */ shaka.ui.Overlay.prototype.defaultConfig_ = function() { return { controlPanelElements: [ 'time_and_duration', 'spacer', 'mute', 'volume', 'fullscreen', 'overflow_menu', ], overflowMenuButtons: [ 'captions', 'quality', 'language', 'picture_in_picture', 'cast', ], addSeekBar: true, addBigPlayButton: true, castReceiverAppId: '', clearBufferOnQualityChange: true, showUnbufferedStart: true, seekBarColors: { base: 'rgba(255, 255, 255, 0.3)', buffered: 'rgba(255, 255, 255, 0.54)', played: 'rgb(255, 255, 255)', }, volumeBarColors: { base: 'rgba(255, 255, 255, 0.54)', level: 'rgb(255, 255, 255)', }, trackLabelFormat: shaka.ui.Overlay.TrackLabelFormat.LANGUAGE, fadeDelay: 0, doubleClickForFullscreen: true, enableKeyboardPlaybackControls: true, enableFullscreenOnRotation: true, }; }; /** * @private */ shaka.ui.Overlay.scanPageForShakaElements_ = async function() { // Install built-in polyfills to patch browser incompatibilities. shaka.polyfill.installAll(); // Check to see if the browser supports the basic APIs Shaka needs. if (!shaka.Player.isBrowserSupported()) { shaka.log.error('Shaka Player does not support this browser. ' + 'Please see https://tinyurl.com/y7s4j9tr for the list of ' + 'supported browsers.'); // After scanning the page for elements, fire a special "loaded" event for // when the load fails. This will allow the page to react to the failure. shaka.ui.Overlay.dispatchLoadedEvent_('shaka-ui-load-failed'); return; } // Look for elements marked 'data-shaka-player-container' // on the page. These will be used to create our default // UI. const containers = document.querySelectorAll( '[data-shaka-player-container]'); // Look for elements marked 'data-shaka-player'. They will // either be used in our default UI or with native browser // controls. const videos = document.querySelectorAll( '[data-shaka-player]'); if (!videos.length && !containers.length) { // No elements have been tagged with shaka attributes. } else if (videos.length && !containers.length) { // Just the video elements were provided. for (let i = 0; i < videos.length; i++) { const video = videos[i]; // If the app has already manually created a UI for this element, // don't create another one. if (video['ui']) { continue; } goog.asserts.assert(video.tagName.toLowerCase() == 'video', 'Should be a video element!'); const container = document.createElement('div'); const videoParent = video.parentElement; videoParent.replaceChild(container, video); container.appendChild(video); shaka.ui.Overlay.setupUIandAutoLoad_(container, video); } } else { for (let i = 0; i < containers.length; i++) { const container = containers[i]; // If the app has already manually created a UI for this element, // don't create another one. if (container['ui']) { continue; } goog.asserts.assert(container.tagName.toLowerCase() == 'div', 'Container should be a div!'); let video = null; for (let j = 0; j < videos.length; j++) { goog.asserts.assert(videos[j].tagName.toLowerCase() == 'video', 'Should be a video element!'); if (videos[j].parentElement == container) { video = videos[j]; break; } } if (!video) { video = document.createElement('video'); video.setAttribute('playsinline', ''); container.appendChild(video); } // eslint-disable-next-line no-await-in-loop await shaka.ui.Overlay.setupUIandAutoLoad_(container, video); } } // After scanning the page for elements, fire the "loaded" event. This will // let apps know they can use the UI library programmatically now, even if // they didn't have any Shaka-related elements declared in their HTML. shaka.ui.Overlay.dispatchLoadedEvent_('shaka-ui-loaded'); }; /** * @param {string} eventName * @private */ shaka.ui.Overlay.dispatchLoadedEvent_ = function(eventName) { // "Event" is not constructable on IE, so we use this CustomEvent pattern. const uiLoadedEvent = /** @type {!CustomEvent} */( document.createEvent('CustomEvent')); uiLoadedEvent.initCustomEvent(eventName, false, false, null); document.dispatchEvent(uiLoadedEvent); }; /** * @param {!Element} container * @param {!Element} video * @private */ shaka.ui.Overlay.setupUIandAutoLoad_ = async function(container, video) { // Create the UI const player = new shaka.Player( shaka.util.Dom.asHTMLMediaElement(video)); const ui = new shaka.ui.Overlay(player, shaka.util.Dom.asHTMLElement(container), shaka.util.Dom.asHTMLMediaElement(video)); // Get and configure cast app id. let castAppId = ''; // Cast receiver id can be specified on either container or video. // It should not be provided on both. If it was, we will use the last // one we saw. if (container['dataset'] && container['dataset']['shakaPlayerCastReceiverId']) { castAppId = container['dataset']['shakaPlayerCastReceiverId']; } else if (video['dataset'] && video['dataset']['shakaPlayerCastReceiverId']) { castAppId = video['dataset']['shakaPlayerCastReceiverId']; } if (castAppId.length) { ui.configure({castReceiverAppId: castAppId}); } if (shaka.util.Dom.asHTMLMediaElement(video).controls) { ui.getControls().setEnabledNativeControls(true); } // Get the source and load it // Source can be specified either on the video element: // <video src='foo.m2u8'></video> // or as a separate element inside the video element: // <video> // <source src='foo.m2u8'/> // </video> // It should not be specified on both. const src = video.getAttribute('src'); if (src) { const sourceElem = document.createElement('source'); sourceElem.setAttribute('src', src); video.appendChild(sourceElem); video.removeAttribute('src'); } for (const elem of video.querySelectorAll('source')) { try { // eslint-disable-next-line no-await-in-loop await ui.getControls().getPlayer().load(elem.getAttribute('src')); break; } catch (e) { shaka.log.error('Error auto-loading asset', e); } } }; /** * Describes what information should show up in labels for selecting audio * variants and text tracks. * * @enum {number} * @export */ shaka.ui.Overlay.TrackLabelFormat = { 'LANGUAGE': 0, 'ROLE': 1, 'LANGUAGE_ROLE': 2, }; /** * "shaka.ui.TrackLabelFormat" is deprecated and will be removed in v4. * * @deprecated * @enum {number} */ shaka.ui.TrackLabelFormat = shaka.ui.Overlay.TrackLabelFormat; if (document.readyState == 'complete') { // Don't fire this event synchronously. In a compiled bundle, the "shaka" // namespace might not be exported to the window until after this point. Promise.resolve().then(shaka.ui.Overlay.scanPageForShakaElements_); } else { window.addEventListener('load', shaka.ui.Overlay.scanPageForShakaElements_); }