mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-25 17:45:03 +03:00
aa9fe1d049
This modifies the demo to load localizations even when the UI fails to load, so that the footer links will still be visible. This also makes the footer links work in that situation, and modifies the shaka-ui-load-failed event to return a failure reason code, so that we can display a contextual error message in event of a failure. Closes #2669 Change-Id: I0cf38f7e39558f1977eee490131378c32105437f
1759 lines
56 KiB
JavaScript
1759 lines
56 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
|
|
goog.provide('shakaDemo.Main');
|
|
|
|
|
|
/**
|
|
* Shaka Player demo, main section.
|
|
* This controls the header and the footer, and contains all methods that should
|
|
* be shared by multiple page layouts (loading assets, setting/checking
|
|
* configuration, etc).
|
|
*/
|
|
shakaDemo.Main = class {
|
|
constructor() {
|
|
/** @private {HTMLVideoElement} */
|
|
this.video_ = null;
|
|
|
|
/** @private {HTMLElement} */
|
|
this.container_ = null;
|
|
|
|
/** @private {shaka.Player} */
|
|
this.player_ = null;
|
|
|
|
/** @type {?ShakaDemoAssetInfo} */
|
|
this.selectedAsset = null;
|
|
|
|
/** @type {shaka.ui.Localization} */
|
|
this.localization_ = null;
|
|
|
|
/**
|
|
* The configuration asked for by the user. I.e., not from the asset.
|
|
* @private {shaka.extern.PlayerConfiguration}
|
|
*/
|
|
this.desiredConfig_;
|
|
|
|
/** @private {shaka.extern.PlayerConfiguration} */
|
|
this.defaultConfig_;
|
|
|
|
/** @private {boolean} */
|
|
this.fullyLoaded_ = false;
|
|
|
|
/** @private {?shaka.ui.Controls} */
|
|
this.controls_ = null;
|
|
|
|
/** @private {?Array.<shaka.extern.StoredContent>} */
|
|
this.initialStoredList_;
|
|
|
|
/** @private {boolean} */
|
|
this.trickPlayControlsEnabled_ = false;
|
|
|
|
/** @private {boolean} */
|
|
this.nativeControlsEnabled_ = false;
|
|
|
|
/** @private {shaka.extern.SupportType} */
|
|
this.support_;
|
|
|
|
/** @private {string} */
|
|
this.uiLocale_ = '';
|
|
|
|
/** @private {boolean} */
|
|
this.noInput_ = false;
|
|
|
|
/** @private {!HTMLAnchorElement} */
|
|
this.errorDisplayLink_ = /** @type {!HTMLAnchorElement} */(
|
|
document.getElementById('error-display-link'));
|
|
|
|
/** @private {?number} */
|
|
this.currentErrorSeverity_ = null;
|
|
}
|
|
|
|
/**
|
|
* This function contains the steps of initialization that should be followed
|
|
* whether or not the demo successfully set up.
|
|
* @private
|
|
*/
|
|
initCommon_() {
|
|
// Display uncaught exceptions. Note that this doesn't seem to work in IE.
|
|
// See shakaDemo.Main.initWrapper for a failsafe that works for init-time
|
|
// errors on IE.
|
|
window.addEventListener('error', (event) => {
|
|
const errorEvent = /** @type {!ErrorEvent} */(event);
|
|
|
|
// Exception to the exceptions we catch: ChromeVox (screenreader) always
|
|
// throws an error as of Chrome 73. Screen these out since they are
|
|
// unrelated to our application and we can't control them.
|
|
if (errorEvent.message.includes('cvox.Api')) {
|
|
return;
|
|
}
|
|
|
|
this.onError_(/** @type {!shaka.util.Error} */ (errorEvent.error));
|
|
});
|
|
|
|
// Set up event listeners.
|
|
document.getElementById('error-display-close-button').addEventListener(
|
|
'click', (event) => this.closeError_());
|
|
|
|
// Set up version strings in the appropriate divs.
|
|
this.setUpVersionStrings_();
|
|
}
|
|
|
|
/**
|
|
* Set up the application with errors to show that load failed.
|
|
* This does not dispatch the shaka-main-loaded event, so it will not cause
|
|
* the nav bar buttons to be set up.
|
|
* @param {!shaka.ui.FailReasonCode} reasonCode
|
|
* @return {!Promise}
|
|
*/
|
|
async initFailed(reasonCode) {
|
|
this.initCommon_();
|
|
|
|
// Set up version links, so the user can switch to compiled mode if
|
|
// necessary.
|
|
this.makeVersionLinks_();
|
|
|
|
const errorCloseButton =
|
|
document.getElementById('error-display-close-button');
|
|
errorCloseButton.style.display = 'none';
|
|
|
|
// Update the componentHandler, to account for any new MDL elements added.
|
|
componentHandler.upgradeDom();
|
|
|
|
// Disable elements that should not be used.
|
|
const elementsToDisable = [];
|
|
const disableClass = 'should-disable-on-fail';
|
|
for (const element of document.getElementsByClassName(disableClass)) {
|
|
elementsToDisable.push(element);
|
|
}
|
|
// The hamburger menu close button is added programmatically by MDL, and
|
|
// thus isn't given our 'disableonfail' class.
|
|
for (const element of document.getElementsByClassName(
|
|
'mdl-layout__drawer-button')) {
|
|
elementsToDisable.push(element);
|
|
}
|
|
for (const element of elementsToDisable) {
|
|
element.tabIndex = -1;
|
|
element.classList.add('disabled-by-fail');
|
|
}
|
|
|
|
// Because the UI did not load, this will need to set up a localization
|
|
// object manually.
|
|
this.localization_ = new shaka.ui.Localization(/* fallbackLocale= */ 'en');
|
|
this.localization_.changeLocale(navigator.languages || []);
|
|
await this.setupLocalization_();
|
|
|
|
// Process a synthetic error about lack of browser support.
|
|
const severity = shaka.util.Error.Severity.CRITICAL;
|
|
let href = '';
|
|
let message = '';
|
|
switch (reasonCode) {
|
|
case shaka.ui.FailReasonCode.NO_BROWSER_SUPPORT:
|
|
message = this.getLocalizedString(
|
|
shakaDemo.MessageIds.FAILURE_NO_BROWSER_SUPPORT);
|
|
href = 'https://github.com/google/shaka-player#' +
|
|
'platform-and-browser-support-matrix';
|
|
break;
|
|
case shaka.ui.FailReasonCode.PLAYER_FAILED_TO_LOAD:
|
|
message = this.getLocalizedString(shakaDemo.MessageIds.FAILURE_MISC);
|
|
break;
|
|
}
|
|
this.handleError_(severity, message, href);
|
|
}
|
|
|
|
/**
|
|
* Initialize the application.
|
|
*/
|
|
async init() {
|
|
this.initCommon_();
|
|
|
|
this.support_ = await shaka.Player.probeSupport();
|
|
|
|
this.video_ =
|
|
/** @type {!HTMLVideoElement} */(document.getElementById('video'));
|
|
this.video_.poster = shakaDemo.Main.mainPoster_;
|
|
|
|
this.container_ = /** @type {!HTMLElement} */(
|
|
document.getElementsByClassName('video-container')[0]);
|
|
|
|
if (navigator.serviceWorker) {
|
|
console.debug('Registering service worker.');
|
|
// NOTE: This can sometimes hang on iOS 12, so let's not wait for it to
|
|
// complete before setting up the app. We don't even use the Promise
|
|
// result or react to the registration failure except to log it.
|
|
navigator.serviceWorker.register('service_worker.js');
|
|
}
|
|
|
|
// Optionally enter noinput mode. This has to happen before setting up the
|
|
// player.
|
|
this.noInput_ = 'noinput' in this.getParams_();
|
|
await this.setupPlayer_();
|
|
this.readHash_();
|
|
window.addEventListener('hashchange', () => this.hashChanged_());
|
|
|
|
await this.setupStorage_();
|
|
|
|
this.setupBugButton_();
|
|
|
|
if (this.noInput_) {
|
|
// Set the page to noInput mode, disabling the header and footer.
|
|
const hideClass = 'should-hide-in-no-input-mode';
|
|
for (const element of document.getElementsByClassName(hideClass)) {
|
|
this.hideElement_(element);
|
|
}
|
|
const showClass = 'should-show-in-no-input-mode';
|
|
for (const element of document.getElementsByClassName(showClass)) {
|
|
this.showElement_(element);
|
|
}
|
|
// Also fullscreen the container.
|
|
this.container_.classList.add('no-input-sized');
|
|
document.getElementById('video-bar').classList.add('no-input-sized');
|
|
}
|
|
|
|
// The main page is loaded. Dispatch an event, so the various
|
|
// configurations will load themselves.
|
|
this.dispatchEventWithName_('shaka-main-loaded');
|
|
|
|
// Wait for one interruptor cycle, so that the tabs have time to load.
|
|
// This ensures that, for example, if there is an asset playing at page
|
|
// load time, the video will scroll into view second, and the page won't
|
|
// scroll away from the video.
|
|
await Promise.resolve();
|
|
|
|
// Update the componentHandler, to account for any new MDL elements added.
|
|
componentHandler.upgradeDom();
|
|
|
|
const asset = this.getLastAssetFromHash_();
|
|
|
|
this.fullyLoaded_ = true;
|
|
this.remakeHash();
|
|
|
|
if (asset && !this.selectedAsset) {
|
|
// If an asset has begun loading in the meantime (for example, due to
|
|
// re-joining an existing cast session), don't play this.
|
|
this.loadAsset(asset);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string} url
|
|
* @return {!Promise.<string>}
|
|
* @private
|
|
*/
|
|
async loadText_(url) {
|
|
const netEngine = new shaka.net.NetworkingEngine();
|
|
const retryParams = shaka.net.NetworkingEngine.defaultRetryParameters();
|
|
const request = shaka.net.NetworkingEngine.makeRequest([url], retryParams);
|
|
const requestType = shaka.net.NetworkingEngine.RequestType.APP;
|
|
const operation = netEngine.request(requestType, request);
|
|
const response = await operation.promise;
|
|
const text = shaka.util.StringUtils.fromUTF8(response.data);
|
|
await netEngine.destroy();
|
|
return text;
|
|
}
|
|
|
|
/** @private */
|
|
async reportBug_() {
|
|
// Fetch the special bug template.
|
|
let text = await this.loadText_('autoTemplate.txt');
|
|
|
|
// Fill in what parts of the template we can.
|
|
const fillInTemplate = (replaceString, value) => {
|
|
text = text.replace(replaceString, value);
|
|
};
|
|
fillInTemplate('RE:player', shaka.Player.version);
|
|
fillInTemplate('RE:link', window.location.href);
|
|
fillInTemplate('RE:browser', navigator.userAgent);
|
|
if (this.selectedAsset &&
|
|
this.selectedAsset.source == shakaAssets.Source.CUSTOM) {
|
|
// This is a custom asset, so add a comment warning about custom assets.
|
|
const warning = await this.loadText_('customWarning.txt');
|
|
fillInTemplate('RE:customwarning', warning);
|
|
} else {
|
|
// No need for any warnings. So remove it (and the newline after it).
|
|
fillInTemplate('RE:customwarning\n', '');
|
|
}
|
|
|
|
// Navigate to the github issue opening interface, with the
|
|
// partially-filled template as a preset body.
|
|
let url = 'https://github.com/google/shaka-player/issues/new?';
|
|
url += 'body=' + encodeURIComponent(text);
|
|
// Open in another tab.
|
|
window.open(url, '_blank');
|
|
}
|
|
|
|
/** @private */
|
|
setupBugButton_() {
|
|
const bugButton = document.getElementById('bug-button');
|
|
bugButton.addEventListener('click', () => this.reportBug_());
|
|
|
|
// The button should be disabled when offline, as we can't report bugs in
|
|
// that state.
|
|
if (!navigator.onLine) {
|
|
bugButton.setAttribute('disabled', '');
|
|
}
|
|
window.addEventListener('online', () => {
|
|
bugButton.removeAttribute('disabled');
|
|
});
|
|
window.addEventListener('offline', () => {
|
|
bugButton.setAttribute('disabled', '');
|
|
});
|
|
}
|
|
|
|
/** @private */
|
|
configureUI_() {
|
|
const video = /** @type {!HTMLVideoElement} */ (this.video_);
|
|
const ui = video['ui'];
|
|
|
|
const uiConfig = ui.getConfiguration();
|
|
// Remove any trick play configurations from a previous config.
|
|
uiConfig.addSeekBar = true;
|
|
uiConfig.controlPanelElements =
|
|
uiConfig.controlPanelElements.filter((element) => {
|
|
return element != 'rewind' && element != 'fast_forward';
|
|
});
|
|
if (this.trickPlayControlsEnabled_) {
|
|
// Trick mode controls don't have a seek bar.
|
|
uiConfig.addSeekBar = false;
|
|
// Replace the position the play_pause button was at with a full suite of
|
|
// trick play controls, including rewind and fast-forward.
|
|
const index = uiConfig.controlPanelElements.indexOf('play_pause');
|
|
uiConfig.controlPanelElements.splice(
|
|
index, 1, 'rewind', 'play_pause', 'fast_forward');
|
|
}
|
|
if (!uiConfig.controlPanelElements.includes('close')) {
|
|
uiConfig.controlPanelElements.push('close');
|
|
}
|
|
ui.configure(uiConfig);
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async setupPlayer_() {
|
|
const video = /** @type {!HTMLVideoElement} */ (this.video_);
|
|
const ui = video['ui'];
|
|
this.player_ = ui.getControls().getPlayer();
|
|
|
|
if (!this.noInput_) {
|
|
// Don't add the close button if in noInput mode; it doesn't make much
|
|
// sense to stop playing a video if you can't start playing other videos.
|
|
|
|
// Register custom controls to the UI.
|
|
const closeFactory = new shakaDemo.CloseButton.Factory();
|
|
shaka.ui.Controls.registerElement('close', closeFactory);
|
|
|
|
// Configure UI.
|
|
this.configureUI_();
|
|
}
|
|
|
|
// Add application-level default configs here. These are not the library
|
|
// defaults, but they are the application defaults. This will affect the
|
|
// default values assigned to UI config elements as well as the decision
|
|
// about what values to place in the URL hash.
|
|
this.player_.configure(
|
|
'manifest.dash.clockSyncUri',
|
|
'https://shaka-player-demo.appspot.com/time.txt');
|
|
|
|
// Get default config.
|
|
this.defaultConfig_ = this.player_.getConfiguration();
|
|
this.desiredConfig_ = this.player_.getConfiguration();
|
|
const languages = navigator.languages || ['en-us'];
|
|
this.configure('preferredAudioLanguage', languages[0]);
|
|
this.configure('preferredTextLanguage', languages[0]);
|
|
this.uiLocale_ = languages[0];
|
|
// TODO(#1591): Support multiple language preferences
|
|
|
|
const onErrorEvent = (event) => this.onErrorEvent_(event);
|
|
this.player_.addEventListener('error', onErrorEvent);
|
|
|
|
// Listen to events on controls.
|
|
this.controls_ = ui.getControls();
|
|
this.controls_.addEventListener('error', onErrorEvent);
|
|
this.controls_.addEventListener('caststatuschanged', (event) => {
|
|
this.onCastStatusChange_(event['newStatus']);
|
|
});
|
|
|
|
this.localization_ = this.controls_.getLocalization();
|
|
await this.setupLocalization_();
|
|
|
|
const drawerCloseButton = document.getElementById('drawer-close-button');
|
|
drawerCloseButton.addEventListener('click', () => {
|
|
const layout = document.getElementById('main-layout');
|
|
layout.MaterialLayout.toggleDrawer();
|
|
this.dispatchEventWithName_('shaka-main-drawer-state-change');
|
|
this.hideElement_(drawerCloseButton);
|
|
});
|
|
// Dispatch drawer state change events when the drawer button or obfuscator
|
|
// are pressed also.
|
|
const drawerButton = document.querySelector('.mdl-layout__drawer-button');
|
|
goog.asserts.assert(drawerButton, 'There should be a drawer button.');
|
|
drawerButton.addEventListener('click', () => {
|
|
this.dispatchEventWithName_('shaka-main-drawer-state-change');
|
|
this.showElement_(drawerCloseButton);
|
|
});
|
|
const obfuscator = document.querySelector('.mdl-layout__obfuscator');
|
|
goog.asserts.assert(obfuscator, 'There should be an obfuscator.');
|
|
obfuscator.addEventListener('click', () => {
|
|
this.dispatchEventWithName_('shaka-main-drawer-state-change');
|
|
this.hideElement_(drawerCloseButton);
|
|
});
|
|
this.hideElement_(drawerCloseButton);
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async setupLocalization_() {
|
|
// Set up localization lazy-loading.
|
|
const applyNewLocaleIfPossible = () => {
|
|
this.localizeHTMLElements_();
|
|
this.dispatchEventWithName_('shaka-main-locale-changed');
|
|
};
|
|
const UNKNOWN_LOCALES = shaka.ui.Localization.UNKNOWN_LOCALES;
|
|
this.localization_.addEventListener(UNKNOWN_LOCALES, (event) => {
|
|
for (const locale of event['locales']) {
|
|
// This will leave promise rejections uncaught; this is acceptable, as
|
|
// this function is actually expected to fail fairly often, and has
|
|
// built-in fallback behavior (from localization events) without needing
|
|
// to catch the promise rejection.
|
|
this.loadUILocale_(locale).then(() => {
|
|
if (locale == this.uiLocale_) {
|
|
applyNewLocaleIfPossible();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
const LOCALE_CHANGED = shaka.ui.Localization.LOCALE_CHANGED;
|
|
this.localization_.addEventListener(LOCALE_CHANGED, (event) => {
|
|
applyNewLocaleIfPossible();
|
|
});
|
|
const initialLocaleLoads = [];
|
|
initialLocaleLoads.push(this.loadUILocale_(this.uiLocale_));
|
|
if (this.uiLocale_.includes('-')) {
|
|
// Also try to load the 'base' localization.
|
|
// This is so that, for example, the uiLocale_ is set to 'en-US', it will
|
|
// try to load 'en'.
|
|
initialLocaleLoads.push(this.loadUILocale_(this.uiLocale_.split('-')[0]));
|
|
}
|
|
if (!this.uiLocale_.startsWith('en')) {
|
|
// Load 'en' as a fallback option, if not already loaded.
|
|
initialLocaleLoads.push(this.loadUILocale_('en'));
|
|
}
|
|
await Promise.all(initialLocaleLoads);
|
|
this.localizeHTMLElements_();
|
|
}
|
|
|
|
/** @return {boolean} */
|
|
getIsDrawerOpen() {
|
|
const drawer = document.querySelector('.mdl-layout__drawer');
|
|
goog.asserts.assert(drawer, 'There should be a drawer.');
|
|
return drawer.classList.contains('is-visible');
|
|
}
|
|
|
|
/**
|
|
* Gets a unique storage identifier for an asset.
|
|
* @param {!ShakaDemoAssetInfo} asset
|
|
* @return {string}
|
|
* @private
|
|
*/
|
|
getIdentifierFromAsset_(asset) {
|
|
// Custom assets can't have special characters like [ or ] in their name,
|
|
// and none of the default assets will have that in their name, so we can
|
|
// be sure that no asset will have [CUSTOM] in its name.
|
|
return asset.name +
|
|
(asset.source == shakaAssets.Source.CUSTOM ? ' [CUSTOM]' : '');
|
|
}
|
|
|
|
/**
|
|
* Creates a storage instance.
|
|
* If and only if storage is not available, this will return null.
|
|
* These storage instances are meant to be used once and then destroyed, using
|
|
* the |Storage.destroy| method.
|
|
* @return {?shaka.offline.Storage}
|
|
* @private
|
|
*/
|
|
makeStorageInstance_() {
|
|
if (!shaka.offline.Storage.support()) {
|
|
return null;
|
|
}
|
|
|
|
const storage = new shaka.offline.Storage();
|
|
|
|
// Configure the storage instance.
|
|
/**
|
|
* @param {string} identifier
|
|
* @return {?ShakaDemoAssetInfo}
|
|
*/
|
|
const getAssetWithIdentifier = (identifier) => {
|
|
for (const asset of shakaAssets.testAssets) {
|
|
if (this.getIdentifierFromAsset_(asset) == identifier) {
|
|
return asset;
|
|
}
|
|
}
|
|
if (shakaDemoCustom) {
|
|
for (const asset of shakaDemoCustom.assets()) {
|
|
if (this.getIdentifierFromAsset_(asset) == identifier) {
|
|
return asset;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
/**
|
|
* @param {shaka.extern.StoredContent} content
|
|
* @param {number} progress
|
|
*/
|
|
const progressCallback = (content, progress) => {
|
|
const identifier = content.appMetadata['identifier'];
|
|
const asset = getAssetWithIdentifier(identifier);
|
|
if (asset) {
|
|
asset.storedProgress = progress;
|
|
this.dispatchEventWithName_('shaka-main-offline-progress');
|
|
}
|
|
};
|
|
storage.configure(this.desiredConfig_);
|
|
storage.configure('offline.progressCallback', progressCallback);
|
|
|
|
return storage;
|
|
}
|
|
|
|
/**
|
|
* Attaches callbacks to an asset so that it can be downloaded online.
|
|
* This method does not verify whether storage is or is not possible.
|
|
* Also, if an asset has an associated offline version, load it with that
|
|
* info.
|
|
* @param {!ShakaDemoAssetInfo} asset
|
|
*/
|
|
setupOfflineSupport(asset) {
|
|
if (!this.initialStoredList_) {
|
|
// Storage failed to set up, so nothing happened.
|
|
return;
|
|
}
|
|
|
|
// If the list of stored content does not contain this asset, then make sure
|
|
// that the asset's |storedContent| value is null. Custom assets that were
|
|
// once stored might have that object serialized with their other data.
|
|
asset.storedContent = null;
|
|
for (const storedContent of this.initialStoredList_) {
|
|
const identifier = storedContent.appMetadata['identifier'];
|
|
if (this.getIdentifierFromAsset_(asset) == identifier) {
|
|
asset.storedContent = storedContent;
|
|
}
|
|
}
|
|
|
|
asset.storeCallback = async () => {
|
|
const storage = this.makeStorageInstance_();
|
|
if (!storage) {
|
|
return;
|
|
}
|
|
try {
|
|
await this.drmConfiguration_(asset, storage);
|
|
const metadata = {
|
|
'identifier': this.getIdentifierFromAsset_(asset),
|
|
'downloaded': new Date(),
|
|
};
|
|
asset.storedProgress = 0;
|
|
this.dispatchEventWithName_('shaka-main-offline-progress');
|
|
const stored = await storage.store(asset.manifestUri, metadata).promise;
|
|
asset.storedContent = stored;
|
|
} catch (error) {
|
|
this.onError_(/** @type {!shaka.util.Error} */ (error));
|
|
asset.storedContent = null;
|
|
}
|
|
storage.destroy();
|
|
asset.storedProgress = 1;
|
|
this.dispatchEventWithName_('shaka-main-offline-progress');
|
|
};
|
|
|
|
asset.unstoreCallback = async () => {
|
|
if (asset == this.selectedAsset) {
|
|
this.unload();
|
|
}
|
|
if (asset.storedContent && asset.storedContent.offlineUri) {
|
|
const storage = this.makeStorageInstance_();
|
|
if (!storage) {
|
|
return;
|
|
}
|
|
try {
|
|
asset.storedProgress = 0;
|
|
this.dispatchEventWithName_('shaka-main-offline-progress');
|
|
await storage.remove(asset.storedContent.offlineUri);
|
|
asset.storedContent = null;
|
|
} catch (error) {
|
|
this.onError_(/** @type {!shaka.util.Error} */ (error));
|
|
// Presumably, if deleting the asset fails, it still exists?
|
|
}
|
|
storage.destroy();
|
|
asset.storedProgress = 1;
|
|
this.dispatchEventWithName_('shaka-main-offline-progress');
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async setupStorage_() {
|
|
// Load stored asset infos.
|
|
const storage = this.makeStorageInstance_();
|
|
if (!storage) {
|
|
return;
|
|
}
|
|
try {
|
|
this.initialStoredList_ = await storage.list();
|
|
} catch (error) {
|
|
// If this operation errors, it means that storage (while supported) is
|
|
// being held up by some kind of error.
|
|
// Log that error, and then pretend that storage is unsupported.
|
|
console.error(error);
|
|
this.initialStoredList_ = null;
|
|
} finally {
|
|
storage.destroy();
|
|
}
|
|
|
|
// Setup asset callbacks for storage, for the test assets.
|
|
for (const asset of shakaAssets.testAssets) {
|
|
if (this.getAssetUnsupportedReason(asset, /* needOffline= */ true)) {
|
|
// Don't bother setting up the callbacks.
|
|
continue;
|
|
}
|
|
|
|
this.setupOfflineSupport(asset);
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
hashChanged_() {
|
|
this.readHash_();
|
|
this.dispatchEventWithName_('shaka-main-config-change');
|
|
}
|
|
|
|
/**
|
|
* Get why the asset is unplayable, if it is unplayable.
|
|
*
|
|
* @param {!ShakaDemoAssetInfo} asset
|
|
* @param {boolean} needOffline True if offline support is required.
|
|
* @return {?shakaDemo.MessageIds} unsupportedReason
|
|
* Null if asset is supported.
|
|
*/
|
|
getAssetUnsupportedReason(asset, needOffline) {
|
|
if (needOffline &&
|
|
(!shaka.offline.Storage.support() || !this.initialStoredList_)) {
|
|
return shakaDemo.MessageIds.UNSUPPORTED_NO_OFFLINE;
|
|
}
|
|
|
|
if (asset.source == shakaAssets.Source.CUSTOM) {
|
|
// We can't be sure if custom assets are supported or not. Just assume
|
|
// they are.
|
|
return null;
|
|
}
|
|
|
|
// Is the asset disabled?
|
|
if (asset.disabled) {
|
|
return shakaDemo.MessageIds.UNSUPPORTED_DISABLED;
|
|
}
|
|
|
|
if (needOffline && !asset.features.includes(shakaAssets.Feature.OFFLINE)) {
|
|
return shakaDemo.MessageIds.UNSUPPORTED_NO_DOWNLOAD;
|
|
}
|
|
|
|
if (!asset.isClear()) {
|
|
const hasSupportedDRM = asset.drm.some((drm) => {
|
|
return this.support_.drm[shakaAssets.identifierForKeySystem(drm)];
|
|
});
|
|
if (!hasSupportedDRM) {
|
|
return shakaDemo.MessageIds.UNSUPPORTED_NO_KEY_SUPPORT;
|
|
}
|
|
if (needOffline) {
|
|
const hasSupportedOfflineDRM = asset.drm.some((drm) => {
|
|
const identifier = shakaAssets.identifierForKeySystem(drm);
|
|
return this.support_.drm[identifier] &&
|
|
this.support_.drm[identifier].persistentState;
|
|
});
|
|
if (!hasSupportedOfflineDRM) {
|
|
return shakaDemo.MessageIds.UNSUPPORTED_NO_LICENSE_SUPPORT;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Does the browser support the asset's manifest type?
|
|
if (asset.features.includes(shakaAssets.Feature.DASH) &&
|
|
!this.support_.manifest['mpd']) {
|
|
return shakaDemo.MessageIds.UNSUPPORTED_NO_DASH_SUPPORT;
|
|
}
|
|
if (asset.features.includes(shakaAssets.Feature.HLS) &&
|
|
!this.support_.manifest['m3u8']) {
|
|
return shakaDemo.MessageIds.UNSUPPORTED_NO_HLS_SUPPORT;
|
|
}
|
|
|
|
// Does the asset contain a playable mime type?
|
|
const mimeTypes = [];
|
|
if (asset.features.includes(shakaAssets.Feature.WEBM)) {
|
|
mimeTypes.push('video/webm');
|
|
}
|
|
if (asset.features.includes(shakaAssets.Feature.MP4)) {
|
|
mimeTypes.push('video/mp4');
|
|
}
|
|
if (asset.features.includes(shakaAssets.Feature.MP2TS)) {
|
|
mimeTypes.push('video/mp2t');
|
|
}
|
|
const hasSupportedMimeType = mimeTypes.some((type) => {
|
|
return this.support_.media[type];
|
|
});
|
|
if (!hasSupportedMimeType) {
|
|
return shakaDemo.MessageIds.UNSUPPORTED_NO_FORMAT_SUPPORT;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Enable or disable the UI's trick play controls.
|
|
*
|
|
* @param {boolean} enabled
|
|
*/
|
|
setTrickPlayControlsEnabled(enabled) {
|
|
this.trickPlayControlsEnabled_ = enabled;
|
|
// Configure the UI, to add or remove the controls.
|
|
this.configureUI_();
|
|
this.remakeHash();
|
|
}
|
|
|
|
/**
|
|
* Get if the trick play controls are enabled.
|
|
*
|
|
* @return {boolean} enabled
|
|
*/
|
|
getTrickPlayControlsEnabled() {
|
|
return this.trickPlayControlsEnabled_;
|
|
}
|
|
|
|
/**
|
|
* Enable or disable the native controls.
|
|
* Goes into effect during the next load.
|
|
*
|
|
* @param {boolean} enabled
|
|
*/
|
|
setNativeControlsEnabled(enabled) {
|
|
this.nativeControlsEnabled_ = enabled;
|
|
this.remakeHash();
|
|
}
|
|
|
|
/**
|
|
* Get if the native controls are enabled.
|
|
*
|
|
* @return {boolean} enabled
|
|
*/
|
|
getNativeControlsEnabled() {
|
|
return this.nativeControlsEnabled_;
|
|
}
|
|
|
|
/**
|
|
* Look through all elements in the DOM, and look for things tagged as having
|
|
* a localized string. Then, localize them.
|
|
*
|
|
* @private
|
|
*/
|
|
localizeHTMLElements_() {
|
|
for (const element of document.querySelectorAll('[localized-string]')) {
|
|
const key = element.getAttribute('localized-string');
|
|
const value = shakaDemo.MessageIds[key];
|
|
if (value) {
|
|
element.textContent = this.getLocalizedString(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!shakaDemo.MessageIds} string
|
|
* @return {string}
|
|
*/
|
|
getLocalizedString(string) {
|
|
return this.localization_.resolve(string);
|
|
}
|
|
|
|
/**
|
|
* @param {string} locale
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async loadUILocale_(locale) {
|
|
if (!locale) {
|
|
return;
|
|
}
|
|
|
|
const load = async (urlBase) => {
|
|
const url = urlBase + '/locales/' + locale + '.json';
|
|
|
|
try {
|
|
const text = await this.loadText_(url);
|
|
const obj = /** @type {!Object.<string, string>} */(JSON.parse(text));
|
|
const map = new Map(Object.entries(obj));
|
|
this.localization_.insert(locale, map);
|
|
} catch (error) {
|
|
console.warn('Unable to load locale', locale, 'for url', url);
|
|
}
|
|
};
|
|
await Promise.all([load('../ui'), load('../demo')]);
|
|
}
|
|
|
|
/** @param {string} locale */
|
|
setUILocale(locale) {
|
|
this.uiLocale_ = locale;
|
|
|
|
// Fall back to browser languages after the demo page setting.
|
|
const preferredLocales = [locale].concat(navigator.languages);
|
|
|
|
this.localization_.changeLocale(preferredLocales);
|
|
}
|
|
|
|
/** @return {string} */
|
|
getUILocale() {
|
|
return this.uiLocale_;
|
|
}
|
|
|
|
/**
|
|
* @return {?ShakaDemoAssetInfo}
|
|
* @private
|
|
*/
|
|
getLastAssetFromHash_() {
|
|
const params = this.getParams_();
|
|
|
|
const manifest = params['asset'];
|
|
const adTagUri = params['adTagUri'];
|
|
if (manifest) {
|
|
// See if it's a default asset.
|
|
for (const asset of shakaAssets.testAssets) {
|
|
if (asset.manifestUri == manifest && asset.adTagUri == adTagUri) {
|
|
return asset;
|
|
}
|
|
}
|
|
|
|
// See if it's a custom asset saved here.
|
|
for (const asset of shakaDemoCustom.assets()) {
|
|
if (asset.manifestUri == manifest) {
|
|
return asset;
|
|
}
|
|
}
|
|
|
|
// Construct a new asset.
|
|
const asset = new ShakaDemoAssetInfo(
|
|
/* name= */ 'loaded asset',
|
|
/* iconUri= */ '',
|
|
/* manifestUri= */ manifest,
|
|
/* source= */ shakaAssets.Source.CUSTOM);
|
|
if ('license' in params) {
|
|
let drmSystems = shakaDemo.Main.commonDrmSystems;
|
|
if ('drmSystem' in params) {
|
|
drmSystems = [params['drmSystem']];
|
|
}
|
|
for (const drmSystem of drmSystems) {
|
|
asset.addLicenseServer(drmSystem, params['license']);
|
|
}
|
|
}
|
|
if ('certificate' in params) {
|
|
asset.addCertificateUri(params['certificate']);
|
|
}
|
|
return asset;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** @private */
|
|
readHash_() {
|
|
const params = this.getParams_();
|
|
|
|
if (this.player_) {
|
|
const readParam = (hashName, configName) => {
|
|
if (hashName in params) {
|
|
const existing = this.getCurrentConfigValue(configName);
|
|
|
|
// Translate the param string into a non-string value if appropriate.
|
|
// Determine what type the parsed value should be based on the current
|
|
// value.
|
|
let value = params[hashName];
|
|
if (typeof existing == 'boolean') {
|
|
value = value == 'true';
|
|
} else if (typeof existing == 'number') {
|
|
value = parseFloat(value);
|
|
}
|
|
|
|
this.configure(configName, value);
|
|
}
|
|
};
|
|
const config = this.player_.getConfiguration();
|
|
shakaDemo.Utils.runThroughHashParams(readParam, config);
|
|
const advanced = this.getCurrentConfigValue('drm.advanced');
|
|
if (advanced) {
|
|
for (const drmSystem of shakaDemo.Main.commonDrmSystems) {
|
|
if (!advanced[drmSystem]) {
|
|
advanced[drmSystem] = shakaDemo.Config.emptyAdvancedConfiguration();
|
|
}
|
|
if ('videoRobustness' in params) {
|
|
advanced[drmSystem].videoRobustness = params['videoRobustness'];
|
|
}
|
|
if ('audioRobustness' in params) {
|
|
advanced[drmSystem].audioRobustness = params['audioRobustness'];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ('lang' in params) {
|
|
// Load the legacy 'lang' hash value.
|
|
const lang = params['lang'];
|
|
this.configure('preferredAudioLanguage', lang);
|
|
this.configure('preferredTextLanguage', lang);
|
|
this.setUILocale(lang);
|
|
}
|
|
if ('uilang' in params) {
|
|
this.setUILocale(params['uilang']);
|
|
// TODO(#1591): Support multiple language preferences
|
|
}
|
|
if ('noadaptation' in params) {
|
|
this.configure('abr.enabled', false);
|
|
}
|
|
if ('jumpLargeGaps' in params) {
|
|
this.configure('streaming.jumpLargeGaps', true);
|
|
}
|
|
|
|
// Add compiled/uncompiled links.
|
|
this.makeVersionLinks_();
|
|
|
|
// Disable custom controls.
|
|
this.nativeControlsEnabled_ = 'nativecontrols' in params;
|
|
|
|
// Enable trick play.
|
|
if ('trickplay' in params) {
|
|
this.trickPlayControlsEnabled_ = true;
|
|
this.configureUI_();
|
|
}
|
|
|
|
// Check if uncompiled mode is supported.
|
|
if (!shakaDemo.Utils.browserSupportsUncompiledMode()) {
|
|
const uncompiledLink = document.getElementById('uncompiled-link');
|
|
goog.asserts.assert(
|
|
uncompiledLink instanceof HTMLAnchorElement, 'Wrong element type!');
|
|
uncompiledLink.setAttribute('disabled', '');
|
|
uncompiledLink.removeAttribute('href');
|
|
uncompiledLink.title = 'requires a newer browser';
|
|
}
|
|
|
|
if (shaka.log) {
|
|
if ('vv' in params) {
|
|
shaka.log.setLevel(shaka.log.Level.V2);
|
|
} else if ('v' in params) {
|
|
shaka.log.setLevel(shaka.log.Level.V1);
|
|
} else if ('debug' in params) {
|
|
shaka.log.setLevel(shaka.log.Level.DEBUG);
|
|
} else if ('info' in params) {
|
|
shaka.log.setLevel(shaka.log.Level.INFO);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @private */
|
|
makeVersionLinks_() {
|
|
const params = this.getParams_();
|
|
let buildType = 'uncompiled';
|
|
if ('build' in params) {
|
|
buildType = params['build'];
|
|
} else if ('compiled' in params) {
|
|
buildType = 'compiled';
|
|
}
|
|
for (const type of ['compiled', 'debug_compiled', 'uncompiled']) {
|
|
const elem = document.getElementById(type.split('_').join('-') + '-link');
|
|
goog.asserts.assert(
|
|
elem instanceof HTMLAnchorElement, 'Wrong element type!');
|
|
if (buildType == type) {
|
|
elem.setAttribute('disabled', '');
|
|
elem.removeAttribute('href');
|
|
elem.title = 'currently selected';
|
|
} else {
|
|
elem.removeAttribute('disabled');
|
|
elem.addEventListener('click', () => {
|
|
const rawParams = location.hash.substr(1).split(';');
|
|
const newParams = rawParams.filter((param) => {
|
|
// Remove current build type param(s).
|
|
return param != 'compiled' && param.split('=')[0] != 'build';
|
|
});
|
|
newParams.push('build=' + type);
|
|
this.setNewHashSilent_(newParams.join(';'));
|
|
location.reload();
|
|
return false;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {!Object.<string, string>} params
|
|
* @private
|
|
*/
|
|
getParams_() {
|
|
// Read URL parameters.
|
|
let fields = location.search.substr(1);
|
|
fields = fields ? fields.split(';') : [];
|
|
let fragments = location.hash.substr(1);
|
|
fragments = fragments ? fragments.split(';') : [];
|
|
|
|
// Because they are being concatenated in this order, if both an
|
|
// URL fragment and an URL parameter of the same type are present
|
|
// the URL fragment takes precendence.
|
|
/** @type {!Array.<string>} */
|
|
const combined = fields.concat(fragments);
|
|
const params = {};
|
|
for (const line of combined) {
|
|
const kv = line.split('=');
|
|
params[kv[0]] = kv.slice(1).join('=');
|
|
}
|
|
return params;
|
|
}
|
|
|
|
/**
|
|
* Recovers the value from the given config field, from an arbitrary config
|
|
* object.
|
|
* This uses the same syntax as setting a single configuration field.
|
|
* @param {string} valueName
|
|
* @param {?shaka.extern.PlayerConfiguration} configObject
|
|
* @return {*}
|
|
* @private
|
|
*/
|
|
getValueFromGivenConfig_(valueName, configObject) {
|
|
let objOn = configObject;
|
|
let valueNameOn = valueName;
|
|
while (valueNameOn) {
|
|
// Split using a regex that only matches the first period.
|
|
const split = valueNameOn.split(/\.(.+)/);
|
|
if (split.length == 3) {
|
|
valueNameOn = split[1];
|
|
objOn = objOn[split[0]];
|
|
} else {
|
|
return objOn[split[0]];
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Recovers the value from the given config field.
|
|
* This uses the same syntax as setting a single configuration field.
|
|
* @example getCurrentConfigValue('abr.bandwidthDowngradeTarget')
|
|
* @param {string} valueName
|
|
* @return {*}
|
|
*/
|
|
getCurrentConfigValue(valueName) {
|
|
const config = this.desiredConfig_;
|
|
return this.getValueFromGivenConfig_(valueName, config);
|
|
}
|
|
|
|
/**
|
|
* @param {string} valueName
|
|
*/
|
|
resetConfiguration(valueName) {
|
|
this.configure(valueName, undefined);
|
|
}
|
|
|
|
/**
|
|
* @param {string|!Object} config
|
|
* @param {*=} value
|
|
*/
|
|
configure(config, value) {
|
|
if (arguments.length == 2 && typeof(config) == 'string') {
|
|
config = shaka.util.ConfigUtils.convertToConfigObject(config, value);
|
|
}
|
|
const asObj = /** @type {!Object} */ (config);
|
|
shaka.util.PlayerConfiguration.mergeConfigObjects(
|
|
this.desiredConfig_, asObj, this.defaultConfig_);
|
|
this.player_.configure(config, value);
|
|
}
|
|
|
|
/** @return {!shaka.extern.PlayerConfiguration} */
|
|
getConfiguration() {
|
|
return this.desiredConfig_;
|
|
}
|
|
|
|
/**
|
|
* @param {string} uri
|
|
* @param {!shaka.net.NetworkingEngine} netEngine
|
|
* @return {!Promise.<!ArrayBuffer>}
|
|
* @private
|
|
*/
|
|
async requestCertificate_(uri, netEngine) {
|
|
const requestType = shaka.net.NetworkingEngine.RequestType.APP;
|
|
const request = /** @type {shaka.extern.Request} */ ({uris: [uri]});
|
|
const response = await netEngine.request(requestType, request).promise;
|
|
return response.data;
|
|
}
|
|
|
|
/** Unload the currently-playing asset. */
|
|
unload() {
|
|
this.selectedAsset = null;
|
|
const videoBar = document.getElementById('video-bar');
|
|
this.hideElement_(videoBar);
|
|
this.video_.poster = shakaDemo.Main.mainPoster_;
|
|
|
|
if (document.fullscreenElement) {
|
|
document.exitFullscreen();
|
|
}
|
|
if (document.pictureInPictureElement) {
|
|
document.exitPictureInPicture();
|
|
}
|
|
this.player_.unload();
|
|
|
|
// The currently-selected asset changed, so update asset cards.
|
|
this.dispatchEventWithName_('shaka-main-selected-asset-changed');
|
|
|
|
// Unset media session title, but only if the browser supports that API.
|
|
if (navigator.mediaSession) {
|
|
navigator.mediaSession.metadata = null;
|
|
}
|
|
|
|
// Remake hash, to change the current asset.
|
|
this.remakeHash();
|
|
}
|
|
|
|
/**
|
|
* @param {ShakaDemoAssetInfo} asset
|
|
* @param {shaka.offline.Storage=} storage
|
|
* @return {!Promise}
|
|
* @private
|
|
*/
|
|
async drmConfiguration_(asset, storage) {
|
|
const netEngine = storage ?
|
|
storage.getNetworkingEngine() :
|
|
this.player_.getNetworkingEngine();
|
|
goog.asserts.assert(netEngine, 'There should be a net engine.');
|
|
asset.applyFilters(netEngine);
|
|
|
|
const assetConfig = asset.getConfiguration();
|
|
if (storage) {
|
|
storage.configure(assetConfig);
|
|
} else {
|
|
// Remove all not-player-applied configurations, by resetting the
|
|
// configuration then re-applying the desired configuration.
|
|
this.player_.resetConfiguration();
|
|
this.player_.configure(this.desiredConfig_);
|
|
this.player_.configure(assetConfig);
|
|
// This uses Player.configure so as to not change |this.desiredConfig_|.
|
|
}
|
|
|
|
const config = storage ?
|
|
storage.getConfiguration() :
|
|
this.player_.getConfiguration();
|
|
|
|
// Change the config's serverCertificate fields based on
|
|
// asset.certificateUri.
|
|
if (asset.certificateUri) {
|
|
// Fetch the certificate, and apply it to the configuration.
|
|
const certificate = await this.requestCertificate_(
|
|
asset.certificateUri, netEngine);
|
|
const certArray = shaka.util.BufferUtils.toUint8(certificate);
|
|
for (const drmSystem of asset.licenseServers.keys()) {
|
|
config.drm.advanced[drmSystem] = config.drm.advanced[drmSystem] || {};
|
|
config.drm.advanced[drmSystem].serverCertificate = certArray;
|
|
}
|
|
} else {
|
|
// Remove any server certificates.
|
|
for (const drmSystem of asset.licenseServers.keys()) {
|
|
if (config.drm.advanced[drmSystem]) {
|
|
delete config.drm.advanced[drmSystem].serverCertificate;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (storage) {
|
|
storage.configure(config);
|
|
} else {
|
|
this.player_.configure('drm.advanced', config.drm.advanced);
|
|
}
|
|
this.remakeHash();
|
|
}
|
|
|
|
/**
|
|
* Performs all visual operations that should be performed when a new asset
|
|
* begins playing. The video bar is un-hidden, the screen is scrolled, and so
|
|
* on.
|
|
*
|
|
* @private
|
|
*/
|
|
showPlayer_() {
|
|
const videoBar = document.getElementById('video-bar');
|
|
this.showElement_(videoBar);
|
|
this.closeError_();
|
|
this.video_.poster = shakaDemo.Main.mainPoster_;
|
|
|
|
// Scroll to the top of the page, so that if the page is scrolled down,
|
|
// the user won't need to manually scroll up to see the video.
|
|
videoBar.scrollIntoView({behavior: 'smooth', block: 'start'});
|
|
}
|
|
|
|
/**
|
|
* @param {ShakaDemoAssetInfo} asset
|
|
*/
|
|
async loadAsset(asset) {
|
|
try {
|
|
this.selectedAsset = asset;
|
|
this.showPlayer_();
|
|
|
|
// The currently-selected asset changed, so update asset cards.
|
|
this.dispatchEventWithName_('shaka-main-selected-asset-changed');
|
|
|
|
await this.drmConfiguration_(asset);
|
|
this.controls_.getCastProxy().setAppData({'asset': asset});
|
|
|
|
// Enable the correct set of controls before loading.
|
|
// The video container influences the TextDisplayer used.
|
|
if (this.nativeControlsEnabled_) {
|
|
this.controls_.setEnabledShakaControls(false);
|
|
this.controls_.setEnabledNativeControls(true);
|
|
// This will force the player to use SimpleTextDisplayer.
|
|
this.player_.setVideoContainer(null);
|
|
} else {
|
|
this.controls_.setEnabledShakaControls(true);
|
|
this.controls_.setEnabledNativeControls(false);
|
|
// This will force the player to use UITextDisplayer.
|
|
this.player_.setVideoContainer(this.container_);
|
|
}
|
|
|
|
// Finally, the asset can be loaded.
|
|
let manifestUri = asset.manifestUri;
|
|
// If we have an offline copy, use that. If the offlineUri field is null,
|
|
// we are still downloading it.
|
|
if (asset.storedContent && asset.storedContent.offlineUri) {
|
|
manifestUri = asset.storedContent.offlineUri;
|
|
}
|
|
// If it's a server side dai asset, request ad-containing manifest
|
|
// from the ad manager.
|
|
if (asset.imaIds) {
|
|
manifestUri = await this.getManifestUriFromAdManager_(asset);
|
|
}
|
|
await this.player_.load(manifestUri);
|
|
if (this.player_.isAudioOnly()) {
|
|
this.video_.poster = shakaDemo.Main.audioOnlyPoster_;
|
|
}
|
|
|
|
// If the asset has an ad tag attached to it, load the ads
|
|
const adManager = this.player_.getAdManager();
|
|
if (adManager && asset.adTagUri) {
|
|
try {
|
|
// If IMA is blocked by an AdBlocker, init() will throw.
|
|
// If that happens, just proceed to load.
|
|
goog.asserts.assert(this.video_ != null, 'this.video should exist!');
|
|
adManager.initClientSide(
|
|
this.controls_.getControlsContainer(), this.video_);
|
|
const adRequest = new google.ima.AdsRequest();
|
|
adRequest.adTagUrl = asset.adTagUri;
|
|
adManager.requestClientSideAds(adRequest);
|
|
} catch (error) {
|
|
console.log(error);
|
|
console.warn('Ads code has been prevented from running. ' +
|
|
'Proceeding without ads.');
|
|
}
|
|
}
|
|
|
|
// Set media session title, but only if the browser supports that API.
|
|
if (navigator.mediaSession) {
|
|
const metadata = {
|
|
title: asset.name,
|
|
artwork: [{src: asset.iconUri}],
|
|
};
|
|
metadata.artist = asset.source;
|
|
navigator.mediaSession.metadata = new MediaMetadata(metadata);
|
|
}
|
|
} catch (reason) {
|
|
const error = /** @type {!shaka.util.Error} */ (reason);
|
|
if (error.code == shaka.util.Error.Code.LOAD_INTERRUPTED) {
|
|
// Don't use shaka.log, which is not present in compiled builds.
|
|
console.debug('load() interrupted');
|
|
} else {
|
|
this.onError_(error);
|
|
}
|
|
}
|
|
|
|
// Remake hash, to change the current asset.
|
|
this.remakeHash();
|
|
}
|
|
|
|
/** Remakes the location's hash. */
|
|
remakeHash() {
|
|
if (!this.fullyLoaded_) {
|
|
// Don't remake the hash until the demo page is fully loaded.
|
|
return;
|
|
}
|
|
|
|
const params = [];
|
|
|
|
if (this.player_) {
|
|
const setParam = (hashName, configName) => {
|
|
const currentValue = this.getCurrentConfigValue(configName);
|
|
const defaultConfig = this.defaultConfig_;
|
|
const defaultValue =
|
|
this.getValueFromGivenConfig_(configName, defaultConfig);
|
|
// NaN != NaN, so there has to be a special check for it to prevent
|
|
// false positives.
|
|
const bothAreNaN = isNaN(currentValue) && isNaN(defaultValue);
|
|
if (currentValue != defaultValue && !bothAreNaN) {
|
|
// Don't bother saving in the hash unless it's a non-default value.
|
|
params.push(hashName + '=' + currentValue);
|
|
}
|
|
};
|
|
const config = this.player_.getConfiguration();
|
|
shakaDemo.Utils.runThroughHashParams(setParam, config);
|
|
const advanced = this.getCurrentConfigValue('drm.advanced');
|
|
if (advanced) {
|
|
for (const drmSystem of shakaDemo.Main.commonDrmSystems) {
|
|
const advancedFor = advanced[drmSystem];
|
|
if (advancedFor) {
|
|
if (advancedFor.videoRobustness) {
|
|
params.push('videoRobustness=' + advancedFor.videoRobustness);
|
|
}
|
|
if (advancedFor.audioRobustness) {
|
|
params.push('audioRobustness=' + advancedFor.audioRobustness);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!this.getCurrentConfigValue('abr.enabled')) {
|
|
params.push('noadaptation');
|
|
}
|
|
if (this.getCurrentConfigValue('streaming.jumpLargeGaps')) {
|
|
params.push('jumpLargeGaps');
|
|
}
|
|
params.push('uilang=' + this.getUILocale());
|
|
|
|
if (this.selectedAsset) {
|
|
const isDefault = shakaAssets.testAssets.includes(this.selectedAsset);
|
|
params.push('asset=' + this.selectedAsset.manifestUri);
|
|
if (this.selectedAsset.adTagUri) {
|
|
params.push('adTagUri=' + this.selectedAsset.adTagUri);
|
|
}
|
|
if (!isDefault && this.selectedAsset.licenseServers.size) {
|
|
const uri = this.selectedAsset.licenseServers.values().next().value;
|
|
params.push('license=' + uri);
|
|
for (const drmSystem of this.selectedAsset.licenseServers.keys()) {
|
|
if (!shakaDemo.Main.commonDrmSystems.includes(drmSystem)) {
|
|
params.push('drmSystem=' + drmSystem);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (!isDefault && this.selectedAsset.certificateUri) {
|
|
params.push('certificate=' + this.selectedAsset.certificateUri);
|
|
}
|
|
}
|
|
|
|
const navButtons = document.getElementById('nav-button-container');
|
|
for (const button of navButtons.childNodes) {
|
|
if (button.nodeType == Node.ELEMENT_NODE) {
|
|
goog.asserts.assert( button instanceof HTMLElement, 'Wrong node type!');
|
|
if (button.classList.contains('mdl-button--accent')) {
|
|
params.push('panel=' + button.getAttribute('tab-identifier'));
|
|
const hashValues = button.getAttribute('tab-hash');
|
|
if (hashValues) {
|
|
params.push('panelData=' + hashValues);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const type of ['compiled', 'debug_compiled', 'uncompiled']) {
|
|
const elem = document.getElementById(type.split('_').join('-') + '-link');
|
|
if (elem.hasAttribute('disabled')) {
|
|
params.push('build=' + type);
|
|
}
|
|
}
|
|
|
|
if (this.noInput_) {
|
|
params.push('noinput');
|
|
}
|
|
|
|
if (this.nativeControlsEnabled_) {
|
|
params.push('nativecontrols');
|
|
}
|
|
|
|
if (this.trickPlayControlsEnabled_) {
|
|
params.push('trickplay');
|
|
}
|
|
|
|
// MAX_LOG_LEVEL is the default starting log level. Only save the log level
|
|
// if it's different from this default.
|
|
if (shaka.log && shaka.log.currentLevel != shaka.log.MAX_LOG_LEVEL) {
|
|
switch (shaka.log.currentLevel) {
|
|
case shaka.log.Level.INFO:
|
|
params.push('info');
|
|
break;
|
|
case shaka.log.Level.DEBUG:
|
|
params.push('debug');
|
|
break;
|
|
case shaka.log.Level.V2:
|
|
params.push('vv');
|
|
break;
|
|
case shaka.log.Level.V1:
|
|
params.push('v');
|
|
break;
|
|
}
|
|
}
|
|
|
|
this.setNewHashSilent_(params.join(';'));
|
|
}
|
|
|
|
/**
|
|
* Sets the hash to a given value WITHOUT triggering a |hashchange| event.
|
|
* @param {string} hash
|
|
* @private
|
|
*/
|
|
setNewHashSilent_(hash) {
|
|
const state = null;
|
|
const title = ''; // Unused; just needed to make Closure happy.
|
|
const newURL = document.location.pathname + '#' + hash;
|
|
// Calling history.replaceState can change the URL or hash of the page
|
|
// without actually triggering any changes; it won't make the page navigate,
|
|
// or trigger a |hashchange| event.
|
|
history.replaceState(state, title, newURL);
|
|
}
|
|
|
|
/**
|
|
* Gets the hamburger menu's content div, so that the caller to add elements
|
|
* to it.
|
|
* There is no guarantee that the caller is the only entity that has added
|
|
* contents to the hamburger menu.
|
|
* @return {!HTMLDivElement} The container for the hamburger menu.
|
|
*/
|
|
getHamburgerMenu() {
|
|
const menu = document.getElementById('hamburger-menu-contents');
|
|
return /** @type {!HTMLDivElement} */ (menu);
|
|
}
|
|
|
|
/**
|
|
* @param {Element} element
|
|
* @private
|
|
*/
|
|
hideElement_(element) {
|
|
element.classList.add('hidden');
|
|
}
|
|
|
|
/**
|
|
* @param {Element} element
|
|
* @private
|
|
*/
|
|
showElement_(element) {
|
|
element.classList.remove('hidden');
|
|
}
|
|
|
|
/**
|
|
* @param {ShakaDemoAssetInfo} asset
|
|
* @return {!Promise.<string>}
|
|
* @private
|
|
*/
|
|
async getManifestUriFromAdManager_(asset) {
|
|
goog.asserts.assert(asset.imaIds != null,
|
|
'Asset should have imaIds!');
|
|
|
|
const adManager = this.player_.getAdManager();
|
|
const container = this.controls_.getServerSideAdContainer();
|
|
try {
|
|
// If IMA is blocked by an AdBlocker, init() will throw.
|
|
// If that happens, return our backup uri.
|
|
goog.asserts.assert(this.video_ != null, 'Video should not be null!');
|
|
adManager.initServerSide(container, this.video_);
|
|
let request;
|
|
if (asset.imaIds.assetKey.length) {
|
|
// LIVE stream
|
|
request = new google.ima.dai.api.LiveStreamRequest();
|
|
request.assetKey = asset.imaIds.assetKey;
|
|
} else {
|
|
// VOD
|
|
request = new google.ima.dai.api.VODStreamRequest();
|
|
request.contentSourceId = asset.imaIds.contentSourceId;
|
|
request.videoId = asset.imaIds.videoId;
|
|
}
|
|
|
|
const uri = await adManager.requestServerSideStream(
|
|
request, /* backupUri= */ asset.manifestUri);
|
|
return uri;
|
|
} catch (error) {
|
|
console.log(error);
|
|
console.warn('Ads code has been prevented from running ' +
|
|
'or returned an error. Proceeding without ads.');
|
|
|
|
return asset.manifestUri;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up a nav button, and an associated tab.
|
|
* This method is meant to be called by the various tabs, as part of their
|
|
* setup process.
|
|
* @param {string} containerName Used to determine the id of the button this
|
|
* is looking for. Also used as the className of the container, for CSS.
|
|
* @return {{
|
|
* container: !HTMLDivElement,
|
|
* button: !HTMLButtonElement,
|
|
* }} The container for the tab, and the button element that activates it.
|
|
*/
|
|
addNavButton(containerName) {
|
|
const navButtons = document.getElementById('nav-button-container');
|
|
const contents = document.getElementById('contents');
|
|
const button = document.getElementById('nav-button-' + containerName);
|
|
|
|
// TODO: Switch to using MDL tabs.
|
|
|
|
// Determine if the element is selected.
|
|
const params = this.getParams_();
|
|
let selected =
|
|
params['panel'] == encodeURI(button.getAttribute('tab-identifier'));
|
|
if (selected) {
|
|
// Re-apply any saved data from hash.
|
|
const hashValues = params['panelData'];
|
|
if (hashValues) {
|
|
button.setAttribute('tab-hash', hashValues);
|
|
}
|
|
} else if (!params['panel']) {
|
|
// Check if it's selected by default.
|
|
selected = button.getAttribute('defaultselected') != null;
|
|
}
|
|
|
|
// Create the div for this nav button's container within the contents.
|
|
const container = document.createElement('div');
|
|
this.hideElement_(container);
|
|
contents.appendChild(container);
|
|
|
|
// Add a click listener to display this container, and hide the others.
|
|
const switchPage = () => {
|
|
// This element should be the selected one.
|
|
for (const child of navButtons.childNodes) {
|
|
if (child.nodeType == Node.ELEMENT_NODE) {
|
|
goog.asserts.assert(child instanceof Element, 'Wrong node type!');
|
|
child.classList.remove('mdl-button--accent');
|
|
}
|
|
}
|
|
for (const child of contents.childNodes) {
|
|
if (child.nodeType == Node.ELEMENT_NODE) {
|
|
goog.asserts.assert(child instanceof Element, 'Wrong node type!');
|
|
this.hideElement_(child);
|
|
}
|
|
}
|
|
button.classList.add('mdl-button--accent');
|
|
this.showElement_(container);
|
|
this.remakeHash();
|
|
|
|
// Dispatch an event so that a page can load any deferred content.
|
|
this.dispatchEventWithName_('shaka-main-page-changed');
|
|
|
|
// Scroll so that the top of the tab is in view.
|
|
container.scrollIntoView({behavior: 'smooth', block: 'start'});
|
|
};
|
|
|
|
button.addEventListener('click', switchPage);
|
|
if (selected) {
|
|
// Defer this call to switchPage until the container is fully set up.
|
|
Promise.resolve().then(switchPage);
|
|
}
|
|
|
|
return {
|
|
container: /** @type {!HTMLDivElement} */ (container),
|
|
button: /** @type {!HTMLButtonElement} */ (button),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Dispatches a custom event to document.
|
|
* @param {string} name
|
|
* @private
|
|
*/
|
|
dispatchEventWithName_(name) {
|
|
const event =
|
|
/** @type {!CustomEvent} */(document.createEvent('CustomEvent'));
|
|
event.initCustomEvent(name,
|
|
/* canBubble= */ false,
|
|
/* cancelable= */ false,
|
|
/* detail= */ null);
|
|
document.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Sets the "version-string" divs to a version string.
|
|
* For example, "v2.5.4-master (uncompiled)".
|
|
* @private
|
|
*/
|
|
setUpVersionStrings_() {
|
|
const version = shaka.Player.version;
|
|
let split = version.split('-');
|
|
const inParen = [];
|
|
|
|
// Separate out some special terms into parentheses after the rest of the
|
|
// version, to make them stand out visually.
|
|
for (const whitelisted of ['debug', 'uncompiled']) {
|
|
if (split.includes(whitelisted)) {
|
|
inParen.push(whitelisted);
|
|
split = split.filter((term) => term != whitelisted);
|
|
}
|
|
}
|
|
|
|
// Put the version into the version string div.
|
|
const versionStringDivs = document.getElementsByClassName('version-string');
|
|
for (const div of versionStringDivs) {
|
|
div.textContent = split.join('-');
|
|
if (inParen.length > 0) {
|
|
div.textContent += ' (' + inParen.join(', ') + ')';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Closes the error bar.
|
|
* @private
|
|
*/
|
|
closeError_() {
|
|
document.getElementById('error-display').classList.add('hidden');
|
|
this.errorDisplayLink_.href = '';
|
|
this.errorDisplayLink_.textContent = '';
|
|
this.currentErrorSeverity_ = null;
|
|
}
|
|
|
|
/**
|
|
* @param {!Event} event
|
|
* @private
|
|
*/
|
|
onErrorEvent_(event) {
|
|
// TODO: generate externs automatically from @event types
|
|
// This event should be shaka.Player.ErrorEvent
|
|
this.onError_(event['detail']);
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.util.Error} error
|
|
* @private
|
|
*/
|
|
onError_(error) {
|
|
let severity = error.severity;
|
|
if (severity == null || error.severity == undefined) {
|
|
// It's not a shaka.util.Error. Treat it as very severe, since those
|
|
// should not be happening.
|
|
severity = shaka.util.Error.Severity.CRITICAL;
|
|
}
|
|
|
|
const message = error.message || ('Error code ' + error.code);
|
|
|
|
let href = '';
|
|
if (error.code) {
|
|
href = '../docs/api/shaka.util.Error.html#value:' + error.code;
|
|
}
|
|
|
|
console.error(error);
|
|
this.handleError_(severity, message, href);
|
|
}
|
|
|
|
/**
|
|
* @param {!shaka.util.Error.Severity} severity
|
|
* @param {string} message
|
|
* @param {string} href
|
|
* @private
|
|
*/
|
|
handleError_(severity, message, href) {
|
|
// Always show the new error if:
|
|
// 1. there is no error showing currently
|
|
// 2. the new error is more severe than the old one
|
|
// Sadly, we do not (yet?) have localizations for error messages.
|
|
if (this.currentErrorSeverity_ == null ||
|
|
severity > this.currentErrorSeverity_) {
|
|
this.errorDisplayLink_.href = href;
|
|
// IE8 and other very old browsers don't have textContent.
|
|
if (this.errorDisplayLink_.textContent === undefined) {
|
|
this.errorDisplayLink_.innerText = message;
|
|
} else {
|
|
this.errorDisplayLink_.textContent = message;
|
|
}
|
|
this.currentErrorSeverity_ = severity;
|
|
if (this.errorDisplayLink_.href) {
|
|
this.errorDisplayLink_.classList.remove('input-disabled');
|
|
} else {
|
|
this.errorDisplayLink_.classList.add('input-disabled');
|
|
}
|
|
document.getElementById('error-display').classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {boolean} connected
|
|
* @private
|
|
*/
|
|
onCastStatusChange_(connected) {
|
|
if (connected && !this.selectedAsset) {
|
|
// You joined an existing session.
|
|
// The exact asset playing is unknown. Just have a selectedAsset, to show
|
|
// that this is playing something.
|
|
this.selectedAsset = ShakaDemoAssetInfo.makeBlankAsset();
|
|
this.showPlayer_();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/** @type {!Array.<string>} */
|
|
shakaDemo.Main.commonDrmSystems = [
|
|
'com.widevine.alpha',
|
|
'com.microsoft.playready',
|
|
'com.apple.fps.1_0',
|
|
'com.adobe.primetime',
|
|
'org.w3.clearkey',
|
|
];
|
|
|
|
|
|
const shakaDemoMain = new shakaDemo.Main();
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @const {string}
|
|
*/
|
|
shakaDemo.Main.mainPoster_ =
|
|
'https://shaka-player-demo.appspot.com/assets/poster.jpg';
|
|
|
|
|
|
/**
|
|
* @private
|
|
* @const {string}
|
|
*/
|
|
shakaDemo.Main.audioOnlyPoster_ =
|
|
'https://shaka-player-demo.appspot.com/assets/audioOnly.gif';
|
|
|
|
|
|
// If setup fails and the global error handler does, too, (as happened on IE
|
|
// right before the launch of this demo), at least log that error to the console
|
|
// for debugging. Wrap init functions in an async function with a try/catch to
|
|
// make sure no error goes unseen when debugging.
|
|
/**
|
|
* @param {function()} initFn
|
|
* @return {!Promise}
|
|
* @suppress {accessControls}
|
|
*/
|
|
shakaDemo.Main.initWrapper = async (initFn) => {
|
|
try {
|
|
await initFn();
|
|
} catch (error) {
|
|
shakaDemoMain.onError_(error);
|
|
console.error(error);
|
|
}
|
|
};
|
|
document.addEventListener('shaka-ui-loaded', () => {
|
|
shakaDemo.Main.initWrapper(() => shakaDemoMain.init());
|
|
});
|
|
document.addEventListener('shaka-ui-load-failed', (event) => {
|
|
shakaDemo.Main.initWrapper(() => {
|
|
const reasonCode = /** @type {!shaka.ui.FailReasonCode} */ (
|
|
event['detail']['reasonCode']);
|
|
shakaDemoMain.initFailed(reasonCode);
|
|
});
|
|
});
|