mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-16 16:16:40 +03:00
8d1b5e6b07
Closes #3832
1801 lines
58 KiB
JavaScript
1801 lines
58 KiB
JavaScript
/*! @license
|
|
* Shaka Player
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
|
|
goog.provide('shakaDemo.Main');
|
|
|
|
goog.require('ShakaDemoAssetInfo');
|
|
goog.require('goog.asserts');
|
|
goog.require('shakaDemo.CloseButton');
|
|
goog.require('shakaDemo.MessageIds');
|
|
goog.require('shakaDemo.Utils');
|
|
|
|
/**
|
|
* 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;
|
|
|
|
// Override the icon for the MDL library's menu button.
|
|
// eslint-disable-next-line no-restricted-syntax
|
|
MaterialLayout.prototype.Constant_.MENU_ICON = 'settings';
|
|
}
|
|
|
|
/**
|
|
* 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.Overlay.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.Overlay.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.Overlay.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.');
|
|
const openDrawer = () => {
|
|
this.dispatchEventWithName_('shaka-main-drawer-state-change');
|
|
this.showElement_(drawerCloseButton);
|
|
};
|
|
// Listen to both the "click" and "keydown" events on the drawer button,
|
|
// since the element is actually a div rather than a button, which means
|
|
// that it doesn't fire "click" events when activated by keyboard input.
|
|
drawerButton.addEventListener('click', openDrawer);
|
|
drawerButton.addEventListener('keydown', (event) => {
|
|
const key = (/** @type {!KeyboardEvent} */ (event)).key;
|
|
// Ignore "keydown" input for keys that won't trigger the button (i.e.
|
|
// anything besides spacebar or enter).
|
|
if (key == ' ' || key == 'Spacebar' || key == 'Enter') {
|
|
openDrawer();
|
|
}
|
|
});
|
|
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.Main.defaultAdvancedDrmConfig();
|
|
}
|
|
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 (this.video_.webkitDisplayingFullscreen) {
|
|
this.video_.webkitExitFullscreen();
|
|
}
|
|
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.imaAssetKey || (asset.imaContentSrcId && asset.imaVideoId)) {
|
|
manifestUri = await this.getManifestUriFromAdManager_(asset);
|
|
}
|
|
await this.player_.load(
|
|
manifestUri,
|
|
/* startTime= */ null,
|
|
asset.mimeType || undefined);
|
|
|
|
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_.getClientSideAdContainer(), 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) {
|
|
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.imaAssetKey != null) {
|
|
// LIVE stream
|
|
request = new google.ima.dai.api.LiveStreamRequest();
|
|
request.assetKey = asset.imaAssetKey;
|
|
} else {
|
|
goog.asserts.assert(asset.imaContentSrcId != null &&
|
|
asset.imaVideoId != null, 'Asset should have ima ids!');
|
|
// VOD
|
|
request = new google.ima.dai.api.VODStreamRequest();
|
|
request.contentSourceId = asset.imaContentSrcId;
|
|
request.videoId = asset.imaVideoId;
|
|
}
|
|
|
|
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_();
|
|
}
|
|
}
|
|
|
|
/** @return {!shaka.extern.AdvancedDrmConfiguration} */
|
|
static defaultAdvancedDrmConfig() {
|
|
return {
|
|
distinctiveIdentifierRequired: false,
|
|
persistentStateRequired: false,
|
|
videoRobustness: '',
|
|
audioRobustness: '',
|
|
sessionType: '',
|
|
serverCertificate: new Uint8Array(0),
|
|
serverCertificateUri: '',
|
|
individualizationServer: '',
|
|
};
|
|
}
|
|
};
|
|
|
|
|
|
/** @type {!Array.<string>} */
|
|
shakaDemo.Main.commonDrmSystems = [
|
|
'com.widevine.alpha',
|
|
'com.microsoft.playready',
|
|
'com.apple.fps',
|
|
'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.Overlay.FailReasonCode} */ (
|
|
event['detail']['reasonCode']);
|
|
shakaDemoMain.initFailed(reasonCode);
|
|
});
|
|
});
|