diff --git a/.gitignore b/.gitignore index 4e91cce7b..06fa1c1ea 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ package-lock.json dist/ docs/api/ coverage/ +.DS_Store diff --git a/build/all.py b/build/all.py index 2fca8eee4..857adb217 100755 --- a/build/all.py +++ b/build/all.py @@ -30,6 +30,17 @@ import shakaBuildHelpers import os import re +def compile_less(path_name, main_file_name, parsed_args): + match = re.compile(r'.*\.less$') + base = shakaBuildHelpers.get_source_base() + main_less_src = os.path.join(base, path_name, main_file_name + '.less') + all_less_srcs = shakaBuildHelpers.get_all_files( + os.path.join(base, path_name), match) + output = os.path.join(base, 'dist', main_file_name + '.css') + + less = compiler.Less(main_less_src, all_less_srcs, output) + return less.compile(parsed_args.force) + def main(args): parser = argparse.ArgumentParser( description='User facing build script for building the Shaka' @@ -98,14 +109,9 @@ def main(args): if docs.main(docs_args) != 0: return 1 - match = re.compile(r'.*\.less$') - main_less_src = os.path.join(base, 'ui', 'controls.less') - all_less_srcs = shakaBuildHelpers.get_all_files( - os.path.join(base, 'ui'), match) - output = os.path.join(base, 'dist', 'controls.css') - - less = compiler.Less(main_less_src, all_less_srcs, output) - if not less.compile(parsed_args.force): + if not compile_less('ui', 'controls', parsed_args): + return 1; + if not compile_less('demo', 'demo', parsed_args): return 1 build_args_with_ui = ['--name', 'ui', '+@complete'] diff --git a/build/check.py b/build/check.py index 8af3c1f31..15189c1c7 100755 --- a/build/check.py +++ b/build/check.py @@ -118,6 +118,7 @@ def check_tests(args): files = set(get('lib') + get('externs') + get('test') + get('ui') + get('third_party', 'closure') + get('third_party', 'language-mapping-list')) + files.add(os.path.join(base, 'demo', 'common', 'asset.js')) files.add(os.path.join(base, 'demo', 'common', 'assets.js')) localizations = compiler.GenerateLocalizations(None) diff --git a/build/conformance.textproto b/build/conformance.textproto index 4d2f0a43a..8937c1d8a 100644 --- a/build/conformance.textproto +++ b/build/conformance.textproto @@ -52,6 +52,7 @@ requirement { # Until we can get this rule updated for ES6 static methods # (https://github.com/google/closure-compiler/issues/2880): whitelist_regexp: 'test/test/util/canned_idb.js' + whitelist_regexp: 'test/assets/assets_integration.js' } @@ -214,13 +215,13 @@ requirement: { type: BANNED_NAME value: 'window.eval' error_message: 'Using "eval" is not allowed' - whitelist_regexp: 'demo/common/demo_utils.js' + whitelist_regexp: 'demo/demo_utils.js' } requirement: { type: BANNED_NAME value: 'eval' error_message: 'Using "eval" is not allowed' - whitelist_regexp: 'demo/common/demo_utils.js' + whitelist_regexp: 'demo/demo_utils.js' } diff --git a/build/gendeps.py b/build/gendeps.py index 07cb08ebc..f1b9697ae 100755 --- a/build/gendeps.py +++ b/build/gendeps.py @@ -31,6 +31,7 @@ deps_args = [ '--root_with_prefix=third_party/language-mapping-list ' + '../../../third_party/language-mapping-list', '--root_with_prefix=dist ../../../dist', + '--root_with_prefix=demo ../../../demo', ] diff --git a/demo/app_manifest.json b/demo/app_manifest.json index 320085d21..f2d1e9c7a 100644 --- a/demo/app_manifest.json +++ b/demo/app_manifest.json @@ -22,7 +22,7 @@ "sizes": "512x512", "type": "image/png" }], - "start_url": "./#compiled", + "start_url": "./#build=compiled", "display": "standalone", "background_color": "#FFFFFF", "theme_color": "#2F3BA2" diff --git a/demo/asset_card.js b/demo/asset_card.js new file mode 100644 index 000000000..415833901 --- /dev/null +++ b/demo/asset_card.js @@ -0,0 +1,253 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * Creates and contains an MDL card that presents info about the given asset. + * @final + */ +class AssetCard { + /** + * @param {!Element} parentDiv + * @param {!ShakaDemoAssetInfo} asset + * @param {boolean} isFeatured True if this card should use the "featured" + * style, which use the asset's short name and have descriptions. + */ + constructor(parentDiv, asset, isFeatured) { + /** @private {!Element} */ + this.card_ = document.createElement('div'); + /** @private {!ShakaDemoAssetInfo} */ + this.asset_ = asset; + /** @private {!Element} */ + this.actions_ = document.createElement('div'); + /** @private {!Element} */ + this.featureIconsContainer_ = document.createElement('div'); + /** @private {!Element} */ + this.progressBar_ = document.createElement('progress'); + + // Lay out the card. + this.card_.classList.add('mdl-card-wide'); + this.card_.classList.add('mdl-card'); + this.card_.classList.add('mdl-shadow--2dp'); + this.card_.classList.add('asset-card'); + + const titleDiv = document.createElement('div'); + titleDiv.classList.add('mdl-card__title'); + this.card_.appendChild(titleDiv); + const titleText = document.createElement('h2'); + titleText.classList.add('mdl-card__title-text'); + titleText.textContent = asset.shortName || asset.name; + titleDiv.appendChild(titleText); + + if (asset.iconUri) { + const img = document.createElement('IMG'); + img.src = asset.iconUri; + this.card_.appendChild(img); + } + + if (asset.description && isFeatured) { + const supportingText = document.createElement('div'); + supportingText.classList.add('mdl-card__supporting-text'); + supportingText.textContent = asset.description; + this.card_.appendChild(supportingText); + } + + this.card_.appendChild(this.featureIconsContainer_); + this.addFeatureIcons_(asset); + + this.actions_.classList.add('mdl-card__actions'); + this.actions_.classList.add('mdl-card--border'); + this.card_.appendChild(this.actions_); + + const progressContainer = document.createElement('div'); + this.progressBar_.classList.add('hidden'); + this.progressBar_.setAttribute('max', 1); + this.progressBar_.setAttribute('value', asset.storedProgress); + progressContainer.appendChild(this.progressBar_); + this.card_.appendChild(progressContainer); + + parentDiv.appendChild(this.card_); + } + + /** + * @param {string} icon + * @param {string} title + * @private + */ + addFeatureIcon_(icon, title) { + const iconDiv = document.createElement('div'); + iconDiv.classList.add('feature-icon'); + iconDiv.setAttribute('icon', icon); + this.featureIconsContainer_.appendChild(iconDiv); + + ShakaDemoTooltips.make(this.featureIconsContainer_, iconDiv, title); + } + + /** + * @param {!ShakaDemoAssetInfo} asset + * @private + */ + addFeatureIcons_(asset) { + const Feature = shakaAssets.Feature; + const KeySystem = shakaAssets.KeySystem; + + const icons = new Map() + .set(Feature.SUBTITLES, 'subtitles') + .set(Feature.CAPTIONS, 'closed_caption') + .set(Feature.LIVE, 'live') + .set(Feature.TRICK_MODE, 'trick_mode') + .set(Feature.HIGH_DEFINITION, 'high_definition') + .set(Feature.ULTRA_HIGH_DEFINITION, 'ultra_high_definition') + .set(Feature.SURROUND, 'surround_sound') + .set(Feature.MULTIPLE_LANGUAGES, 'multiple_languages') + .set(Feature.AUDIO_ONLY, 'audio_only'); + + for (const feature of asset.features) { + const icon = icons.get(feature); + if (icon) { + this.addFeatureIcon_(icon, feature); + } + } + + for (let drm of asset.drm) { + switch (drm) { + case KeySystem.WIDEVINE: + this.addFeatureIcon_('widevine', 'Widevine DRM'); + break; + case KeySystem.CLEAR_KEY: + this.addFeatureIcon_('clear_key', 'Clear Key DRM'); + break; + case KeySystem.PLAYREADY: + this.addFeatureIcon_('playready', 'PlayReady DRM'); + break; + } + } + } + + /** + * Modify an asset to make it clear that it is unsupported. + * @param {?string} unsupportedReason + */ + markAsUnsupported(unsupportedReason) { + this.card_.classList.add('asset-card-unsupported'); + this.makeUnsupportedButton_('Not Available', unsupportedReason); + } + + /** + * Make a button that represents the lack of a working button. + * @param {string} buttonName + * @param {?string} unsupportedReason + * @private + */ + makeUnsupportedButton_(buttonName, unsupportedReason) { + const button = this.addButton(buttonName, () => {}); + button.setAttribute('disabled', ''); + + // Place the tooltip into the parent container of the card, so that the + // tooltip won't be clipped by other asset cards. + // Also, tooltips don't work on disabled buttons (on some platforms), so + // the button itself has to be "uprooted" and placed in a synthetic div + // specifically to attach the tooltip to. + const tooltipContainer = this.card_.parentElement; + if (tooltipContainer && unsupportedReason) { + const attachPoint = document.createElement('div'); + if (button.parentElement) { + button.parentElement.removeChild(button); + } + attachPoint.classList.add('tooltip-attach-point'); + attachPoint.appendChild(button); + this.actions_.appendChild(attachPoint); + ShakaDemoTooltips.make(tooltipContainer, attachPoint, unsupportedReason); + } + } + + /** + * Select this card if the card's asset matches |asset|. + * Used to simplify the implementation of "shaka-main-selected-asset-changed" + * handlers. + * @param {!ShakaDemoAssetInfo} asset + */ + selectByAsset(asset) { + this.card_.classList.remove('selected'); + if (this.asset_ == asset) { + this.card_.classList.add('selected'); + } + } + + /** + * Adds a button to the bottom of the card that controls storage behavior. + * This is a separate function because it involves a significant amount of + * custom behavior, such as the download bar. + */ + addStoreButton() { + const unsupportedReason = shakaDemoMain.getAssetUnsupportedReason( + this.asset_, /* needOffline= */ true); + if (unsupportedReason || !this.asset_.storeCallback) { + // This can't be stored. + this.makeUnsupportedButton_('Download', unsupportedReason); + return; + } + if (this.asset_.isStored()) { + this.addButton('Delete', async () => { + await this.asset_.unstoreCallback(); + }); + } else { + this.addButton('Download', async () => { + await this.asset_.storeCallback(); + }); + } + } + + /** Updates the progress bar on the card. */ + updateProgress() { + if (this.asset_.storedProgress < 1) { + this.progressBar_.classList.remove('hidden'); + for (const button of this.actions_.childNodes) { + button.disabled = true; + } + } else { + this.progressBar_.classList.add('hidden'); + for (const button of this.actions_.childNodes) { + button.disabled = false; + } + } + this.progressBar_.setAttribute('value', this.asset_.storedProgress); + } + + /** + * Adds a button to the bottom of the card that will call |onClick| when + * clicked. For example, a play or delete button. + * @param {string} name + * @param {function()} onclick + * @return {!Element} + */ + addButton(name, onclick) { + const button = document.createElement('button'); + button.classList.add('mdl-button'); + button.classList.add('mdl-button--colored'); + button.classList.add('mdl-js-button'); + button.classList.add('mdl-js-ripple-effect'); + button.textContent = name; + button.addEventListener('click', () => { + if (!button.hasAttribute('disabled')) { + onclick(); + } + }); + this.actions_.appendChild(button); + return button; + } +} diff --git a/demo/asset_section.js b/demo/asset_section.js deleted file mode 100644 index 3eed041b4..000000000 --- a/demo/asset_section.js +++ /dev/null @@ -1,416 +0,0 @@ -/** - * @license - * Copyright 2016 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Shaka Player demo, main section. - * - * @suppress {visibility} to work around compiler errors until we can - * refactor the demo into classes that talk via public method. TODO - */ - - -/** @suppress {duplicate} */ -var shakaDemo = shakaDemo || {}; // eslint-disable-line no-var - - -/** @private {!Array.} */ -shakaDemo.onlineOptGroups_ = []; - - -/** - * @return {!Promise} - * @private - */ -shakaDemo.setupAssets_ = function() { - // Populate the asset list. - let assetList = document.getElementById('assetList'); - /** @type {!Object.} */ - let groups = {}; - let first = null; - shakaAssets.testAssets.forEach(function(asset) { - if (asset.disabled) return; - - let group = groups[asset.source]; - if (!group) { - group = /** @type {!HTMLOptGroupElement} */( - document.createElement('optgroup')); - group.label = asset.source; - group.disabled = !navigator.onLine; - groups[asset.source] = group; - assetList.appendChild(group); - shakaDemo.onlineOptGroups_.push(group); - } - - let option = document.createElement('option'); - option.textContent = asset.name; - option.asset = asset; // A custom attribute to map back to the asset. - group.appendChild(option); - - if (asset.drm.length && !asset.drm.some( - function(keySystem) { return shakaDemo.support_.drm[keySystem]; })) { - option.disabled = true; - } - - let 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'); - } - if (!mimeTypes.some( - function(type) { return shakaDemo.support_.media[type]; })) { - option.disabled = true; - } - - if (asset.features.includes(shakaAssets.Feature.DASH) && - !shakaDemo.support_.manifest['mpd']) { - option.disabled = true; - } - if (asset.features.includes(shakaAssets.Feature.HLS) && - !shakaDemo.support_.manifest['m3u8']) { - option.disabled = true; - } - - if (!option.disabled && !group.disabled) { - first = first || option; - if (asset.focus) first = option; - } - }); - - if (first) { - first.selected = true; - } - - // This needs to be started before we add the custom asset option. - let asyncOfflineSetup = shakaDemo.setupOfflineAssets_(); - - // Add an extra option for custom assets. - let option = document.createElement('option'); - option.textContent = '(custom asset)'; - assetList.appendChild(option); - - assetList.addEventListener('change', function() { - // Show/hide the custom asset fields based on the selection. - let asset = assetList.options[assetList.selectedIndex].asset; - let customAsset = document.getElementById('customAsset'); - customAsset.style.display = asset ? 'none' : 'block'; - - // Update the hash to reflect this change. - shakaDemo.hashShouldChange_(); - }); - - document.getElementById('loadButton').addEventListener( - 'click', shakaDemo.load); - document.getElementById('unloadButton').addEventListener( - 'click', shakaDemo.unload); - - const assetInputs = [ - document.getElementById('licenseServerInput'), - document.getElementById('manifestInput'), - document.getElementById('certificateInput'), - ]; - for (const input of assetInputs) { - input.addEventListener('input', shakaDemo.onAssetInput_); - input.addEventListener('keydown', shakaDemo.onAssetKeyDown_); - } - - return asyncOfflineSetup; -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onAssetKeyDown_ = function(event) { - if (event.key == 'Enter') { - shakaDemo.load(); - } -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onAssetInput_ = function(event) { - // Mirror the users input as they type. - shakaDemo.hashShouldChange_(); -}; - - -/** - * @param {string} uri - * @return {!Promise.} - * @private - */ -shakaDemo.requestCertificate_ = function(uri) { - let netEngine = shakaDemo.player_.getNetworkingEngine(); - const requestType = shaka.net.NetworkingEngine.RequestType.APP; - let request = /** @type {shaka.extern.Request} */ ({uris: [uri]}); - - return netEngine.request(requestType, request).promise - .then((response) => response.data); -}; - - -/** - * @param {ArrayBuffer} certificate - * @private - */ -shakaDemo.configureCertificate_ = function(certificate) { - let player = shakaDemo.player_; - let config = player.getConfiguration(); - let certConfig = {}; - - for (let keySystem in config.drm.advanced) { - certConfig[keySystem] = { - serverCertificate: new Uint8Array(certificate), - }; - } - - player.configure({ - drm: { - advanced: certConfig, - }, - }); -}; - - -/** - * Prepares the Player to load the given assets by setting the configuration - * values. This does not load the asset. - * - * @param {?shakaAssets.AssetInfo} asset - * @return {shakaAssets.AssetInfo} - * @private - */ -shakaDemo.preparePlayer_ = function(asset) { - shakaDemo.closeError(); - - let player = shakaDemo.player_; - - let videoRobustness = - document.getElementById('drmSettingsVideoRobustness').value; - let audioRobustness = - document.getElementById('drmSettingsAudioRobustness').value; - - let commonDrmSystems = [ - 'com.widevine.alpha', - 'com.microsoft.playready', - 'com.apple.fps.1_0', - 'com.adobe.primetime', - 'org.w3.clearkey', - ]; - let config = /** @type {shaka.extern.PlayerConfiguration} */( - {abr: {}, streaming: {}, manifest: {dash: {}}, offline: {}}); - config.drm = /** @type {shaka.extern.DrmConfiguration} */({ - advanced: {}}); - commonDrmSystems.forEach(function(system) { - config.drm.advanced[system] = - /** @type {shaka.extern.AdvancedDrmConfiguration} */({}); - }); - config.manifest.dash.clockSyncUri = - 'https://shaka-player-demo.appspot.com/time.txt'; - - if (!asset) { - // Use the custom fields. - let licenseServerUri = document.getElementById('licenseServerInput').value; - let licenseServers = {}; - if (licenseServerUri) { - commonDrmSystems.forEach(function(system) { - licenseServers[system] = licenseServerUri; - }); - } - - asset = /** @type {shakaAssets.AssetInfo} */ ({ - manifestUri: document.getElementById('manifestInput').value, - // Use the custom license server for all key systems. - // This simplifies configuration for the user. - // They will simply fill in a Widevine license server on Chrome, etc. - licenseServers: licenseServers, - // Use a custom certificate for all key systems as well - certificateUri: document.getElementById('certificateInput').value, - }); - } - - // Any storage operation should update our progress label. - config.offline.progressCallback = function(data, percent) { - let progress = document.getElementById('progress'); - progress.textContent = (percent * 100).toFixed(2); - }; - - player.resetConfiguration(); - - // Add configuration from this asset. - ShakaDemoUtils.setupAssetMetadata(asset, player); - shakaDemo.castProxy_.setAppData({'asset': asset}); - - // Add drm configuration from the UI. - if (videoRobustness) { - commonDrmSystems.forEach(function(system) { - config.drm.advanced[system].videoRobustness = videoRobustness; - }); - } - if (audioRobustness) { - commonDrmSystems.forEach(function(system) { - config.drm.advanced[system].audioRobustness = audioRobustness; - }); - } - - // Add other configuration from the UI. - config.preferredAudioLanguage = - document.getElementById('preferredAudioLanguage').value; - config.preferredTextLanguage = - document.getElementById('preferredTextLanguage').value; - const preferredAudioChannelCount = - Number(document.getElementById('preferredAudioChannelCount').value); - if (!isNaN(preferredAudioChannelCount)) { - config.preferredAudioChannelCount = preferredAudioChannelCount; - } - let availabilityWindowOverrideRaw = - document.getElementById('availabilityWindowOverride').value; - let availabilityWindowOverride = Number(availabilityWindowOverrideRaw); - if (!isNaN(availabilityWindowOverride) && - availabilityWindowOverrideRaw.length) { - // Don't configure if the field contains an empty string; this is because - // Number('') evaluates to 0, which is a valid (if fairly useless) override - // value, while we would rather it mean "don't override". - config.manifest.availabilityWindowOverride = availabilityWindowOverride; - } - - config.abr.enabled = - document.getElementById('enableAdaptation').checked; - let smallGapLimit = document.getElementById('smallGapLimit').value; - if (!isNaN(Number(smallGapLimit)) && smallGapLimit.length > 0) { - config.streaming.smallGapLimit = Number(smallGapLimit); - } - config.streaming.jumpLargeGaps = - document.getElementById('jumpLargeGaps').checked; - - // When we use native controls, we must always stream text. - // See comments in onNativeChange_ for details. - config.streaming.alwaysStreamText = - document.getElementById('showNative').checked; - - const videoContainer = shakaDemo.controls_.getVideoContainer(); - config.textDisplayFactory = function() { - return new shaka.ui.TextDisplayer(shakaDemo.video_, videoContainer); - }; - - player.configure(config); - - // TODO: Document demo app debugging features. - if (window.debugConfig) { - player.configure(window.debugConfig); - } - - return asset; -}; - - -/** Compute which assets should be disabled. */ -shakaDemo.computeDisabledAssets = function() { - // TODO: Use a remote support probe, recompute asset disabled when casting? - shakaDemo.onlineOptGroups_.forEach(function(group) { - group.disabled = !navigator.onLine; - }); -}; - - -/** Load the selected asset. */ -shakaDemo.load = function() { - let assetList = document.getElementById('assetList'); - let option = assetList.options[assetList.selectedIndex]; - let player = shakaDemo.player_; - - let asset = shakaDemo.preparePlayer_(option.asset); - - // Revert to default poster while we load. - shakaDemo.localVideo_.poster = shakaDemo.mainPoster_; - - let configureCertificate = Promise.resolve(); - - if (asset.certificateUri) { - configureCertificate = shakaDemo.requestCertificate_(asset.certificateUri) - .then(shakaDemo.configureCertificate_); - } - - configureCertificate.then(function() { - // Load the manifest. - return player.load(asset.manifestUri, shakaDemo.startTime_); - }).then(function() { - // Update the control state in case autoplay is disabled. - shakaDemo.controls_.loadComplete(); - - if (shakaDemo.video_.controls) { - shakaDemo.controls_.setEnabledNativeControls(true); - } else { - shakaDemo.controls_.setEnabledShakaControls(true); - } - - shakaDemo.hashShouldChange_(); - - // Set a different poster for audio-only assets. - if (player.isAudioOnly()) { - shakaDemo.localVideo_.poster = shakaDemo.audioOnlyPoster_; - } - - // Disallow the casting of offline content. - let isOffline = asset.manifestUri.startsWith('offline:'); - shakaDemo.controls_.allowCast(!isOffline); - - (asset.extraText || []).forEach(function(extraText) { - player.addTextTrack(extraText.uri, extraText.language, extraText.kind, - extraText.mime, extraText.codecs); - }); - - // Check if browser supports Media Session first. - if ('mediaSession' in navigator) { - // Set media session title. - navigator.mediaSession.metadata = new MediaMetadata({title: asset.name}); - } - }, function(reason) { - let 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 { - shakaDemo.onError_(error); - } - }); - - // While the manifest is being loaded in parallel, go ahead and ask the video - // to play. This can help with autoplay on Android, since Android requires - // user interaction to play a video and this function is called from a click - // event. This seems to work only because Shaka Player has already created a - // MediaSource object and set video.src. - shakaDemo.video_.play(); -}; - - -/** Unload any current asset. */ -shakaDemo.unload = function() { - shakaDemo.player_.unload(); - if (!shakaDemo.castProxy_.isCasting()) { - shakaDemo.controls_.setEnabledShakaControls(false); - } -}; diff --git a/demo/cast_receiver/index.html b/demo/cast_receiver/index.html index 90aeb0107..f7bece32c 100644 --- a/demo/cast_receiver/index.html +++ b/demo/cast_receiver/index.html @@ -47,8 +47,8 @@ UNCOMPILED_JS = [ // This file contains goog.require calls for all exported library classes. '../../shaka-player.uncompiled.js', // These are the individual parts of the receiver app. + '../common/asset.js', '../common/assets.js', - '../common/demo_utils.js', 'receiver_app.js', ]; diff --git a/demo/cast_receiver/receiver_app.js b/demo/cast_receiver/receiver_app.js index e9132e9bf..3c8d594ec 100644 --- a/demo/cast_receiver/receiver_app.js +++ b/demo/cast_receiver/receiver_app.js @@ -115,8 +115,10 @@ ShakaReceiver.prototype.appDataCallback_ = function(appData) { // appData is null if we start the app without any media loaded. if (!appData) return; - let asset = /** @type {shakaAssets.AssetInfo} */(appData['asset']); - ShakaDemoUtils.setupAssetMetadata(asset, this.player_); + const asset = ShakaDemoAssetInfo.fromJSON(appData['asset']); + asset.applyFilters(this.player_.getNetworkingEngine()); + const config = asset.getConfiguration(); + this.player_.configure(config); }; diff --git a/demo/close_button.js b/demo/close_button.js new file mode 100644 index 000000000..5cdeaa2c6 --- /dev/null +++ b/demo/close_button.js @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** + * A custom UI button, to allow users to close the video element. + * This cannot actually extend shaka.ui.Element, as that class does not exist + * at load-time when in uncompiled mode. + * @implements {shaka.extern.IUIElement} + */ +class CloseButton { + /** + * @param {!HTMLElement} parent + * @param {!shaka.ui.Controls} controls + */ + constructor(parent, controls) { + /** @protected {!HTMLElement} */ + this.parent = parent; + + this.button_ = document.createElement('button'); + this.button_.classList.add('material-icons'); + this.button_.classList.add('close-button'); + this.button_.textContent = 'close'; // Close icon. + this.parent.appendChild(this.button_); + + this.button_.addEventListener('click', () => { + shakaDemoMain.unload(); + }); + + // TODO: Make sure that the screenreader description of this control is + // localized! + } + + /** @override */ + destroy() { + return Promise.resolve(); + } +} + +/** + * @implements {shaka.extern.IUIElement.Factory} + * @final + */ +CloseButton.Factory = class { + /** @override */ + create(rootElement, controls) { + return new CloseButton(rootElement, controls); + } +}; + +// This button is registered inside setup in ShakaDemoMain, rather than +// statically here, since shaka.ui.Controls does not exist in this stage of the +// load process. diff --git a/demo/common/asset.js b/demo/common/asset.js new file mode 100644 index 000000000..47d7de17a --- /dev/null +++ b/demo/common/asset.js @@ -0,0 +1,383 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +goog.provide('ShakaDemoAssetInfo'); + +goog.require('goog.asserts'); + + +/** + * An object that contains information about an asset. + */ +const ShakaDemoAssetInfo = class { + /** + * @param {string} name + * @param {string} iconUri + * @param {string} manifestUri + * @param {shakaAssets.Source} source + */ + constructor(name, iconUri, manifestUri, source) { + // Required members. + /** @type {string} */ + this.name = name; + /** @type {string} */ + this.shortName = ''; + /** @type {string} */ + this.iconUri = iconUri; + /** @type {string} */ + this.manifestUri = manifestUri; + /** @type {string} */ + this.source = source; + + // Optional members. + /** @type {boolean} */ + this.focus = false; + /** @type {boolean} */ + this.disabled = false; + /** @type {!Array.} */ + this.extraText = []; + /** @type {?string} */ + this.certificateUri = null; + /** @type {string} */ + this.description = ''; + /** @type {boolean} */ + this.isFeatured = false; + /** @type {!Array.} */ + this.drm = [shakaAssets.KeySystem.CLEAR]; + /** @type {!Array.} */ + this.features = []; + /** @type {!Map.} */ + this.licenseServers = new Map(); + /** @type {!Map.} */ + this.licenseRequestHeaders = new Map(); + /** @type {?shaka.extern.RequestFilter} */ + this.requestFilter = null; + /** @type {?shaka.extern.ResponseFilter} */ + this.responseFilter = null; + /** @type {?shaka.extern.DashContentProtectionCallback} */ + this.drmCallback = null; // TODO: Setter method? + /** @type {!Map.} */ + this.clearKeys = new Map(); // TODO: Setter method? + /** @type {?Object} */ + this.extraConfig = null; + + // Offline storage values. + /** @type {?function()} */ + this.storeCallback; + /** @type {?function()} */ + this.unstoreCallback; + /** @type {?shaka.extern.StoredContent} */ + this.storedContent; + /** @type {number} */ + this.storedProgress = 1; + } + + /** + * @param {string} description + * @return {!ShakaDemoAssetInfo} + */ + addDescription(description) { + this.description = description; + return this; + } + + /** + * @param {string} certificateUri + * @return {!ShakaDemoAssetInfo} + */ + addCertificateUri(certificateUri) { + this.certificateUri = certificateUri; + return this; + } + + /** + * A sort comparator for comparing two strings, ignoring case. + * @param {string} a + * @param {string} b + * @return {number} + * @private + */ + static caseLessAlphaComparator_(a, b) { + if (a.toLowerCase() < b.toLowerCase()) { + return -1; + } + if (a.toLowerCase() > b.toLowerCase()) { + return 1; + } + return 0; + } + + /** + * @param {shakaAssets.Feature} feature + * @return {!ShakaDemoAssetInfo} + */ + addFeature(feature) { + goog.asserts.assert(feature != shakaAssets.Feature.STORED, + 'Assets should not be given the synthetic "STORED" ' + + 'property!'); + this.features.push(feature); + // Sort the features list, so that features are in a predictable order. + this.features.sort(ShakaDemoAssetInfo.caseLessAlphaComparator_); + return this; + } + + /** + * @param {shakaAssets.KeySystem} keySystem + * @return {!ShakaDemoAssetInfo} + */ + addKeySystem(keySystem) { + if (this.drm.length == 1 && this.drm[0] == shakaAssets.KeySystem.CLEAR) { + // Once an asset has an actual key system, it's no longer a CLEAR asset. + this.drm = []; + } + this.drm.push(keySystem); + // Sort the drm list, so that key systems are in a predictable order. + this.drm.sort(ShakaDemoAssetInfo.caseLessAlphaComparator_); + return this; + } + + /** + * @param {!Object} extraConfig + * @return {!ShakaDemoAssetInfo} + */ + setExtraConfig(extraConfig) { + this.extraConfig = extraConfig; + return this; + } + + /** + * @param {!shaka.extern.RequestFilter} requestFilter + * @return {!ShakaDemoAssetInfo} + */ + setRequestFilter(requestFilter) { + this.requestFilter = requestFilter; + return this; + } + + /** + * @param {!shaka.extern.ResponseFilter} responseFilter + * @return {!ShakaDemoAssetInfo} + */ + setResponseFilter(responseFilter) { + this.responseFilter = responseFilter; + return this; + } + + /** + * @param {string} keySystem + * @param {string} licenseServer + * @return {!ShakaDemoAssetInfo} + */ + addLicenseServer(keySystem, licenseServer) { + this.licenseServers.set(keySystem, licenseServer); + return this; + } + + /** + * @param {string} keySystem + * @param {string} licenseRequestHeader + * @return {!ShakaDemoAssetInfo} + */ + addLicenseRequestHeader(keySystem, licenseRequestHeader) { + this.licenseRequestHeaders.set(keySystem, licenseRequestHeader); + return this; + } + + /** + * @param {shakaAssets.ExtraText} extraText + * @return {!ShakaDemoAssetInfo} + */ + addExtraText(extraText) { + // TODO: At no point do we actually use the extraText... why does it exist? + this.extraText.push(extraText); + return this; + } + + /** + * If this is called, the asset will be focused on by the integration tests. + * @return {!ShakaDemoAssetInfo} + */ + markAsFocused() { + this.focus = true; + return this; + } + + /** + * If this is called, the asset will appear on the main page of the demo. + * Also, this allows you to provide a shorter name to be used in the feature + * card. + * @param {string=} shortName + * @return {!ShakaDemoAssetInfo} + */ + markAsFeatured(shortName) { + this.isFeatured = true; + this.shortName = shortName || this.shortName; + return this; + } + + /** + * If this is called, the asset is disabled in tests and in the demo app. + * @return {!ShakaDemoAssetInfo} + */ + markAsDisabled() { + this.disabled = true; + return this; + } + + /** + * @return {!Object} + * @override + * + * Suppress checkTypes warnings, so that we can access properties of this + * object as though it were a struct. + * @suppress {checkTypes} + */ + toJSON() { + // Construct a generic object with the values of this object, but with the + // proper formatting. + const raw = {}; + for (let key in this) { + const value = this[key]; + if (value instanceof Map) { + // The built-in JSON functions cannot convert Maps; this converts Maps + // to objects. + const replacement = {}; + replacement['__type__'] = 'map'; + for (let entry of value.entries()) { + replacement[entry[0]] = entry[1]; + } + raw[key] = replacement; + } else { + raw[key] = value; + } + } + return raw; + } + + /** + * Applies appropriate request or response filters to the player. + * @param {shaka.net.NetworkingEngine} networkingEngine + */ + applyFilters(networkingEngine) { + networkingEngine.clearAllRequestFilters(); + networkingEngine.clearAllResponseFilters(); + + if (this.licenseRequestHeaders.size) { + const filter = (requestType, request) => { + return this.addLicenseRequestHeaders_(this.licenseRequestHeaders, + requestType, + request); + }; + networkingEngine.registerRequestFilter(filter); + } + + if (this.requestFilter) { + networkingEngine.registerRequestFilter(this.requestFilter); + } + if (this.responseFilter) { + networkingEngine.registerResponseFilter(this.responseFilter); + } + } + + /** + * Gets the configuration object for the asset. + * @return {!shaka.extern.PlayerConfiguration} + */ + getConfiguration() { + const config = /** @type {shaka.extern.PlayerConfiguration} */( + {drm: {}, manifest: {dash: {}}}); + if (this.licenseServers.size) { + config.drm.servers = {}; + this.licenseServers.forEach((value, key) => { + config.drm.servers[key] = value; + }); + } + if (this.drmCallback) { + config.manifest.dash.customScheme = this.drmCallback; + } + if (this.clearKeys.size) { + config.drm.clearKeys = {}; + this.clearKeys.forEach((value, key) => { + config.drm.clearKeys[key] = value; + }); + } + if (this.extraConfig) { + for (let key in this.extraConfig) { + config[key] = this.extraConfig[key]; + } + } + return config; + } + + /** + * @param {!Map.} headers + * @param {shaka.net.NetworkingEngine.RequestType} requestType + * @param {shaka.extern.Request} request + * @private + */ + addLicenseRequestHeaders_(headers, requestType, request) { + if (requestType != shaka.net.NetworkingEngine.RequestType.LICENSE) return; + + // Add these to the existing headers. Do not clobber them! + // For PlayReady, there will already be headers in the request. + headers.forEach((value, key) => { + request.headers[key] = value; + }); + } + + /** @return {boolean} */ + isStored() { + return this.storedContent != null; + } +}; + + +/** @return {!ShakaDemoAssetInfo} */ +ShakaDemoAssetInfo.makeBlankAsset = function() { + return new ShakaDemoAssetInfo( + /* name= */ '', + /* iconUri= */ '', + /* manifestUri= */ '', + /* source= */ shakaAssets.Source.CUSTOM); +}; + +/** + * @param {!Object} raw + * @return {!ShakaDemoAssetInfo} + */ +ShakaDemoAssetInfo.fromJSON = function(raw) { + // This handles the special case for Maps in toJSON. + const parsed = {}; + for (let key in raw) { + const value = raw[key]; + if (value && typeof value == 'object' && value['__type__'] == 'map') { + const replacement = new Map(); + for (let key in value) { + if (key != '__type__') { + replacement.set(key, value[key]); + } + } + parsed[key] = replacement; + } else { + parsed[key] = value; + } + } + const asset = ShakaDemoAssetInfo.makeBlankAsset(); + Object.assign(asset, parsed); + return asset; +}; diff --git a/demo/common/assets.js b/demo/common/assets.js index 44f3abd73..19088cbed 100644 --- a/demo/common/assets.js +++ b/demo/common/assets.js @@ -17,6 +17,9 @@ */ +goog.require('ShakaDemoAssetInfo'); + + // Types and enums {{{ /** * A container for demo assets. @@ -29,22 +32,6 @@ var shakaAssets = {}; // eslint-disable-line no-var -/** @enum {string} */ -shakaAssets.Encoder = { - UNKNOWN: 'Unknown', - SHAKA_PACKAGER: 'Shaka packager', - AXINOM: 'Axinom', - UNIFIED_STREAMING: 'Unified Streaming', - WOWZA: 'Wowza', - BITCODIN: 'Bitcodin', - NIMBLE_STREAMER: 'Nimble Streamer', - AZURE_MEDIA_SERVICES: 'Azure Media Services', - MP4BOX: 'MP4Box', - APPLE: 'Apple', - UPLYNK: 'Verizon Digital Media Services', -}; - - /** @enum {string} */ shakaAssets.Source = { CUSTOM: 'Custom', @@ -67,48 +54,60 @@ shakaAssets.KeySystem = { FAIRPLAY: 'com.apple.fps.1_0', PLAYREADY: 'com.microsoft.playready', WIDEVINE: 'com.widevine.alpha', + CLEAR: 'no drm protection', }; /** @enum {string} */ shakaAssets.Feature = { - SEGMENT_BASE: 'SegmentBase', - SEGMENT_LIST_DURATION: 'SegmentList w/ @duration', - SEGMENT_LIST_TIMELINE: 'SegmentList w/ SegmentTimeline', - SEGMENT_TEMPLATE_DURATION: 'SegmentTemplate w/ @duration', - SEGMENT_TEMPLATE_TIMELINE: 'SegmentTemplate w/ SegmentTimeline', - SEGMENT_TEMPLATE_TIMELINE_TIME: 'SegmentTemplate w/ SegmentTimeline $Time$', - SEGMENT_TEMPLATE_TIMELINE_NUMBER: 'SegmentTemplate w/ SegTimeline $Number$', - - PSSH: 'embedded PSSH', + // Set if the asset has more than one drm key defined. MULTIKEY: 'multiple keys', + // Set if the asset has multiple periods. MULTIPERIOD: 'multiple Periods', ENCRYPTED_WITH_CLEAR: 'mixing encrypted and unencrypted periods', AESCTR_16_BYTE_IV: 'encrypted with AES CTR Mode using a 16 byte IV', AESCTR_8_BYTE_IV: 'encrypted with AES CTR Mode using a 8 byte IV', - TRICK_MODE: 'special trick mode track', - XLINK: 'xlink', + // Set if the asset has a special trick mode track, for rewinding effects. + TRICK_MODE: 'Special trick mode track', + XLINK: 'XLink', - SUBTITLES: 'subtitles', - CAPTIONS: 'captions', - SEGMENTED_TEXT: 'segmented text', + // Set if the asset has any subtitle tracks. + SUBTITLES: 'Subtitles', + // Set if the asset has any closed caption tracks. + CAPTIONS: 'Captions', EMBEDDED_TEXT: 'embedded text', + // Set if the asset has multiple audio languages. MULTIPLE_LANGUAGES: 'multiple languages', - OFFLINE: 'offline', + // Set if the asset is audio-only. + AUDIO_ONLY: 'audio only', + OFFLINE: 'downloadable', + // A synthetic property used in the search tab. Should not be given to assets. + STORED: 'downloaded', - LIVE: 'live', + // Set if the asset is a livestream. + LIVE: 'Live', + // Set if the asset has at least one WebM stream. WEBM: 'WebM', - MP4: 'mp4', + // Set if the asset has at least one mp4 stream. + MP4: 'MP4', + // Set if the asset has at least one MPEG-2 TS stream. MP2TS: 'MPEG-2 TS', + // Set if the asset has at least one TTML text track. TTML: 'TTML', + // Set if the asset has at least one WEBVTT text track. WEBVTT: 'WebVTT', - HIGH_DEFINITION: 'high definition', - ULTRA_HIGH_DEFINITION: 'ultra-high definition', + // Set if the asset has at least one stream that is at least 720p. + HIGH_DEFINITION: 'High definition', + // Set if the asset has at least one stream that is at least 4k. + ULTRA_HIGH_DEFINITION: 'Ultra-high definition', - SURROUND: 'surround sound', + // Set if the asset has at least one stream that is surround sound. + SURROUND: 'Surround sound', + // Set if the asset is a MPEG-DASH manifest. DASH: 'DASH', + // Set if the asset is an HLS manifest. HLS: 'HLS', }; @@ -136,92 +135,6 @@ shakaAssets.Feature = { shakaAssets.ExtraText; -/** - * @typedef {{ - * name: string, - * manifestUri: string, - * certificateUri: (string|undefined), - * focus: (boolean|undefined), - * disabled: (boolean|undefined), - * extraText: (!Array.|undefined), - * - * iconUri: (string|undefined), - * shortName: (string|undefined), - * description: (string|undefined), - * isFeatured: (boolean|undefined), - * - * encoder: shakaAssets.Encoder, - * source: shakaAssets.Source, - * drm: !Array., - * features: !Array., - * - * licenseServers: (!Object.|undefined), - * licenseRequestHeaders: (!Object.|undefined), - * requestFilter: (shaka.extern.RequestFilter|undefined), - * responseFilter: (shaka.extern.ResponseFilter|undefined), - * drmCallback: (shaka.extern.DashContentProtectionCallback|undefined), - * clearKeys: (!Object.|undefined), - * - * extraConfig: (Object|undefined) - * }} - * - * @property {string} name - * The name of the asset. This does not have to be unique and can be the - * same if the asset is encoded different ways (or by different encoders). - * @property {string} manifestUri - * The URI of the manifest. - * @property {(string|undefined)} certificateUri - * The URI of the DRM server certificate, if required to play this asset. - * @property {(boolean|undefined)} focus - * (optional) If true, focuses the integration test for this asset and selects - * this asset in the demo app. - * @property {(boolean|undefined)} disabled - * (optional) If true, disables tests for this asset and hides it in the demo - * app. - * @property {(!Array.|undefined)} extraText - * (optional) An array of extra text sources (e.g. external captions). - * - * @property {string} iconUri - * An URI pointing to an icon. - * @property {string} shortName - * A shorter, snappier name for the asset. - * @property {string} description - * A line or two of text describing the asset. - * @property {(boolean|undefined)} isFeatured - * (optional) If this is true, the asset will appear in the main page. - * - * @property {shakaAssets.Encoder} encoder - * The encoder that created the asset. - * @property {shakaAssets.Source} source - * The source of the asset. - * @property {!Array.} drm - * An array of key-systems that the asset uses. - * @property {!Array.} features - * An array of features that this asset has. - * - * @property {(!Object.|undefined)} licenseServers - * (optional) A map of key-system to license server. - * @property {(!Object.|undefined)} licenseRequestHeaders - * (optional) A map of headers to add to license requests. - * @property {(shaka.extern.RequestFilter|undefined)} - * requestFilter - * A filter on license requests before they are passed to the server. - * @property {(shaka.extern.ResponseFilter|undefined)} - * responseFilter - * A filter on license responses before they are passed to the CDM. - * @property {(shaka.extern.DashContentProtectionCallback|undefined)} - * drmCallback - * A callback to use to interpret ContentProtection elements. - * @property {(!Object.|undefined)} clearKeys - * A map of key-id to key to use with clear-key encryption. - * - * @property {(Object|undefined)} extraConfig - * Arbitrary player config to be applied after all other settings. - */ -shakaAssets.AssetInfo; -// }}} - - // Custom callbacks {{{ /** * A response filter for VDMS Uplynk manifest responses. @@ -270,1303 +183,778 @@ shakaAssets.UplynkRequestFilter = function(type, request) { // }}} -/** @const {!Array.} */ +/** @const {!Array.} */ shakaAssets.testAssets = [ // Shaka assets {{{ - { - name: 'Angel One (multicodec, multilingual)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', - shortName: 'Angel One', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPLE_LANGUAGES, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Angel One (multicodec, multilingual, Widevine)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', - shortName: 'Angel One', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [shakaAssets.KeySystem.WIDEVINE], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPLE_LANGUAGES, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - - licenseServers: { - 'com.widevine.alpha': 'https://cwip-shaka-proxy.appspot.com/no_auth', - }, - }, - { - name: 'Angel One (multicodec, multilingual, ClearKey server)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-clearkey/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', - shortName: 'Angel One', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [shakaAssets.KeySystem.CLEAR_KEY], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPLE_LANGUAGES, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - - licenseServers: { - 'org.w3.clearkey': 'https://cwip-shaka-proxy.appspot.com/clearkey?_u3wDe7erb7v8Lqt8A3QDQ=ABEiM0RVZneImaq7zN3u_w', - }, - }, - { - name: 'Angel One (HLS, MP4, multilingual)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', - shortName: 'Angel One', - description: 'A clip from a classic Star Trek TNG episode, presented in ' + - 'HLS.', - isFeatured: true, - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.HLS, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPLE_LANGUAGES, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Angel One (HLS, MP4, multilingual, Widevine)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine-hls/hls.m3u8', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', - shortName: 'Angel One', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [shakaAssets.KeySystem.WIDEVINE], - features: [ - shakaAssets.Feature.HLS, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPLE_LANGUAGES, - shakaAssets.Feature.OFFLINE, - ], - - licenseServers: { - 'com.widevine.alpha': 'https://cwip-shaka-proxy.appspot.com/no_auth', - }, - }, - { - name: 'Sintel 4k (multicodec)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/sintel/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - shortName: 'Sintel', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Sintel w/ trick mode (MP4 only, 720p)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/sintel-trickplay/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - shortName: 'Sintel', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.TRICK_MODE, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Sintel 4k (WebM only)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/sintel-webm-only/dash.mpd', - // NOTE: hanging in Firefox - // https://bugzilla.mozilla.org/show_bug.cgi?id=1291451 - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - shortName: 'Sintel', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Sintel 4k (MP4 only)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-only/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - shortName: 'Sintel', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Sintel 4k (multicodec, Widevine)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/sintel-widevine/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - shortName: 'Sintel', - description: 'A Blender Foundation short film, protected by Widevine ' + - 'encryption.', - isFeatured: true, - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [shakaAssets.KeySystem.WIDEVINE], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.PSSH, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - - licenseServers: { - 'com.widevine.alpha': 'https://cwip-shaka-proxy.appspot.com/no_auth', - }, - }, - { - name: 'Sintel 4k (multicodec, VTT in MP4)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-wvtt/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - shortName: 'Sintel', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Sintel w/ 44 subtitle languages', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/sintel-many-subs/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - shortName: 'Sintel', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Heliocentrism (multicodec, multiperiod)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/heliocentrism/heliocentrism.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/heliocentricism.png', - shortName: 'Heliocentrism', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPERIOD, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Heliocentrism (multicodec, multiperiod, xlink)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/heliocentrism-xlink/heliocentrism.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/heliocentricism.png', - shortName: 'Heliocentrism', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPERIOD, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.XLINK, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: '"Dig the Uke" by Stefan Kartenberg (audio only, multicodec)', - // From: http://dig.ccmixter.org/files/JeffSpeed68/53327 - // Licensed under Creative Commons BY-NC 3.0. - // Free for non-commercial use with attribution. - // http://creativecommons.org/licenses/by-nc/3.0/ - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/dig-the-uke-clear/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/audio_only.png', - shortName: 'Dig the Uke', - description: 'An audio-only presentation performed by Stefan Kartenberg.', - isFeatured: true, - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: '"Dig the Uke" by Stefan Kartenberg (audio only, multicodec, Widevine)', // eslint-disable-line max-len - // From: http://dig.ccmixter.org/files/JeffSpeed68/53327 - // Licensed under Creative Commons BY-NC 3.0. - // Free for non-commercial use with attribution. - // http://creativecommons.org/licenses/by-nc/3.0/ - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/dig-the-uke/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/audio_only.png', - shortName: 'Dig the Uke', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [shakaAssets.KeySystem.WIDEVINE], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.OFFLINE, - ], - - licenseServers: { - 'com.widevine.alpha': 'https://cwip-shaka-proxy.appspot.com/no_auth', - }, - }, - { - name: 'Tears of Steel (multicodec, TTML)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.TTML, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Tears of Steel (multicodec, surround + stereo)', - manifestUri: 'https://storage.googleapis.com/shaka-demo-assets/tos-surround/dash.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.SURROUND, - shakaAssets.Feature.WEBM, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Shaka Player History (multicodec, live, DASH)', - manifestUri: 'https://storage.googleapis.com/shaka-live-assets/player-source.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/shaka.png', - shortName: 'Shaka Player History', - description: 'A self-indulgent DASH livestream.', - isFeatured: true, - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - shakaAssets.Feature.WEBM, - ], - }, - { - name: 'Shaka Player History (live, HLS)', - manifestUri: 'https://storage.googleapis.com/shaka-live-assets/player-source.m3u8', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/shaka.png', - shortName: 'Shaka Player History', - - encoder: shakaAssets.Encoder.SHAKA_PACKAGER, - source: shakaAssets.Source.SHAKA, - drm: [], - features: [ - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.HLS, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - ], - }, + new ShakaDemoAssetInfo( + /* name= */ 'Big Buck Bunny: the Dark Truths of a Video Dev Cartoon', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dark_truth.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/bbb-dark-truths/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addDescription('A serious documentary about a problem plaguing video developers.') // eslint-disable-line max-len + .markAsFeatured('Big Buck Bunny: the Dark Truths') + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Angel One (multicodec, multilingual)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Angel One (multicodec, multilingual, Widevine)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.MULTIKEY) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE) + .addLicenseServer('com.widevine.alpha', 'https://cwip-shaka-proxy.appspot.com/no_auth'), + new ShakaDemoAssetInfo( + /* name= */ 'Angel One (multicodec, multilingual, ClearKey server)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/angel-one-clearkey/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addKeySystem(shakaAssets.KeySystem.CLEAR_KEY) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE) + .addLicenseServer('org.w3.clearkey', 'https://cwip-shaka-proxy.appspot.com/clearkey?_u3wDe7erb7v8Lqt8A3QDQ=ABEiM0RVZneImaq7zN3u_w'), + new ShakaDemoAssetInfo( + /* name= */ 'Angel One (HLS, MP4, multilingual)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/angel-one-hls/hls.m3u8', + /* source= */ shakaAssets.Source.SHAKA) + .addDescription('A clip from a classic Star Trek TNG episode, presented in HLS.') // eslint-disable-line max-len + .markAsFeatured('Angel One') + .addFeature(shakaAssets.Feature.HLS) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.SURROUND) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Angel One (HLS, MP4, multilingual, Widevine)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/angel_one.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine-hls/hls.m3u8', + /* source= */ shakaAssets.Source.SHAKA) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.HLS) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.SURROUND) + .addFeature(shakaAssets.Feature.MULTIKEY) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE) + .addLicenseServer('com.widevine.alpha', 'https://cwip-shaka-proxy.appspot.com/no_auth'), + new ShakaDemoAssetInfo( + /* name= */ 'Sintel 4k (multicodec)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/sintel/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Sintel w/ trick mode (MP4 only, 720p)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/sintel-trickplay/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.TRICK_MODE) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + // NOTE: hanging in Firefox + // https://bugzilla.mozilla.org/show_bug.cgi?id=1291451 + new ShakaDemoAssetInfo( + /* name= */ 'Sintel 4k (WebM only)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/sintel-webm-only/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Sintel 4k (MP4 only)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-only/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Sintel 4k (multicodec, Widevine)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/sintel-widevine/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addDescription('A Blender Foundation short film, protected by Widevine encryption.') // eslint-disable-line max-len + .markAsFeatured('Sintel') + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIKEY) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE) + .addLicenseServer('com.widevine.alpha', 'https://cwip-shaka-proxy.appspot.com/no_auth'), + new ShakaDemoAssetInfo( + /* name= */ 'Sintel 4k (multicodec, VTT in MP4)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/sintel-mp4-wvtt/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Sintel w/ 44 subtitle languages', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/sintel-many-subs/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.SURROUND) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Heliocentrism (multicodec, multiperiod)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/heliocentricism.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/heliocentrism/heliocentrism.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPERIOD) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Heliocentrism (multicodec, multiperiod, xlink)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/heliocentricism.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/heliocentrism-xlink/heliocentrism.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPERIOD) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.XLINK) + .addFeature(shakaAssets.Feature.OFFLINE), + // From: http://dig.ccmixter.org/files/JeffSpeed68/53327 + // Licensed under Creative Commons BY-NC 3.0. + // Free for non-commercial use with attribution. + // http://creativecommons.org/licenses/by-nc/3.0/ + new ShakaDemoAssetInfo( + /* name= */ '"Dig the Uke" by Stefan Kartenberg (audio only, multicodec)', // eslint-disable-line max-len + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/audio_only.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/dig-the-uke-clear/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addDescription('An audio-only presentation performed by Stefan Kartenberg.') // eslint-disable-line max-len + .markAsFeatured('Dig the Uke') + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.AUDIO_ONLY) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.OFFLINE), + // From: http://dig.ccmixter.org/files/JeffSpeed68/53327 + // Licensed under Creative Commons BY-NC 3.0. + // Free for non-commercial use with attribution. + // http://creativecommons.org/licenses/by-nc/3.0/ + new ShakaDemoAssetInfo( + /* name= */ '"Dig the Uke" by Stefan Kartenberg (audio only, multicodec, Widevine)', // eslint-disable-line max-len + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/audio_only.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/dig-the-uke/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.AUDIO_ONLY) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.OFFLINE) + .addLicenseServer('com.widevine.alpha', 'https://cwip-shaka-proxy.appspot.com/no_auth'), + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel (multicodec, TTML)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-ttml/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.TTML) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel (multicodec, surround + stereo)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-demo-assets/tos-surround/dash.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SURROUND) + .addFeature(shakaAssets.Feature.WEBM) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Shaka Player History (multicodec, live, DASH)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/shaka.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-live-assets/player-source.mpd', + /* source= */ shakaAssets.Source.SHAKA) + .addDescription('A self-indulgent DASH livestream.') + .markAsFeatured('Shaka Player History') + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.WEBM), + new ShakaDemoAssetInfo( + /* name= */ 'Shaka Player History (live, HLS)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/shaka.png', + /* manifestUri= */ 'https://storage.googleapis.com/shaka-live-assets/player-source.m3u8', + /* source= */ shakaAssets.Source.SHAKA) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.HLS) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MP4), // }}} // Axinom assets {{{ // Src: https://github.com/Axinom/dash-test-vectors - { - name: 'Multi-DRM', - manifestUri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.AXINOM, - source: shakaAssets.Source.AXINOM, - drm: [ - shakaAssets.KeySystem.PLAYREADY, - shakaAssets.KeySystem.WIDEVINE, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - shakaAssets.Feature.TTML, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBVTT, - ], - - licenseServers: { - 'com.widevine.alpha': 'https://drm-widevine-licensing.axtest.net/AcquireLicense', - 'com.microsoft.playready': 'https://drm-playready-licensing.axtest.net/AcquireLicense', - }, - licenseRequestHeaders: { - 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.4lWwW46k-oWcah8oN18LPj5OLS5ZU-_AQv7fe0JhNjA', // eslint-disable-line max-len - }, - }, - { - name: 'Multi-DRM, multi-key', - manifestUri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-MultiKey/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.AXINOM, - source: shakaAssets.Source.AXINOM, - drm: [ - shakaAssets.KeySystem.PLAYREADY, - shakaAssets.KeySystem.WIDEVINE, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIKEY, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - shakaAssets.Feature.TTML, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBVTT, - ], - - licenseServers: { - 'com.widevine.alpha': 'https://drm-widevine-licensing.axtest.net/AcquireLicense', - 'com.microsoft.playready': 'https://drm-playready-licensing.axtest.net/AcquireLicense', - }, - licenseRequestHeaders: { - 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7ImlkIjoiODAzOTliZjUtOGEyMS00MDE0LTgwNTMtZTI3ZTc0OGU5OGMwIiwiZW5jcnlwdGVkX2tleSI6ImxpTkpxVmFZa05oK01LY3hKRms3SWc9PSJ9LHsiaWQiOiI5MDk1M2UwOS02Y2IyLTQ5YTMtYTI2MC03YTVmZWZlYWQ0OTkiLCJlbmNyeXB0ZWRfa2V5Ijoia1l0SEh2cnJmQ01lVmRKNkxrYmtuZz09In0seyJpZCI6IjBlNGRhOTJiLWQwZTgtNGE2Ni04YzNmLWMyNWE5N2ViNjUzMiIsImVuY3J5cHRlZF9rZXkiOiI3dzdOWkhITE1nSjRtUUtFSzVMVE1RPT0ifSx7ImlkIjoiNTg1ZjIzM2YtMzA3Mi00NmYxLTlmYTQtNmRjMjJjNjZhMDE0IiwiZW5jcnlwdGVkX2tleSI6IkFjNFVVbVl0Qko1blBROU4xNXJjM2c9PSJ9LHsiaWQiOiI0MjIyYmQ3OC1iYzQ1LTQxYmYtYjYzZS02ZjgxNGRjMzkxZGYiLCJlbmNyeXB0ZWRfa2V5IjoiTzZGTzBmcVNXb3BwN2JqYy9ENGxNQT09In1dfX0.uF6YlKAREOmbniAeYiH070HSJhV0YS7zSKjlCtiDR5Y', // eslint-disable-line max-len - }, - }, - { - name: 'Multi-DRM, multi-key, multi-Period', - manifestUri: 'https://media.axprod.net/TestVectors/v7-MultiDRM-MultiKey-MultiPeriod/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.AXINOM, - source: shakaAssets.Source.AXINOM, - drm: [ - shakaAssets.KeySystem.PLAYREADY, - shakaAssets.KeySystem.WIDEVINE, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIKEY, - shakaAssets.Feature.MULTIPERIOD, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - shakaAssets.Feature.TTML, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBVTT, - ], - - licenseServers: { - 'com.widevine.alpha': 'https://drm-widevine-licensing.axtest.net/AcquireLicense', - 'com.microsoft.playready': 'https://drm-playready-licensing.axtest.net/AcquireLicense', - }, - licenseRequestHeaders: { - 'X-AxDRM-Message': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7ImlkIjoiMDg3Mjc4NmUtZjllNy00NjVmLWEzYTItNGU1YjBlZjhmYTQ1IiwiZW5jcnlwdGVkX2tleSI6IlB3NitlRVlOY3ZqWWJmc2gzWDNmbWc9PSJ9LHsiaWQiOiJjMTRmMDcwOS1mMmI5LTQ0MjctOTE2Yi02MWI1MjU4NjUwNmEiLCJlbmNyeXB0ZWRfa2V5IjoiLzErZk5paDM4bXFSdjR5Y1l6bnQvdz09In0seyJpZCI6IjhiMDI5ZTUxLWQ1NmEtNDRiZC05MTBmLWQ0YjVmZDkwZmJhMiIsImVuY3J5cHRlZF9rZXkiOiJrcTBKdVpFanBGTjhzYVRtdDU2ME9nPT0ifSx7ImlkIjoiMmQ2ZTkzODctNjBjYS00MTQ1LWFlYzItYzQwODM3YjRiMDI2IiwiZW5jcnlwdGVkX2tleSI6IlRjUlFlQld4RW9IT0tIcmFkNFNlVlE9PSJ9LHsiaWQiOiJkZTAyZjA3Zi1hMDk4LTRlZTAtYjU1Ni05MDdjMGQxN2ZiYmMiLCJlbmNyeXB0ZWRfa2V5IjoicG9lbmNTN0dnbWVHRmVvSjZQRUFUUT09In0seyJpZCI6IjkxNGU2OWY0LTBhYjMtNDUzNC05ZTlmLTk4NTM2MTVlMjZmNiIsImVuY3J5cHRlZF9rZXkiOiJlaUkvTXNsbHJRNHdDbFJUL0xObUNBPT0ifSx7ImlkIjoiZGE0NDQ1YzItZGI1ZS00OGVmLWIwOTYtM2VmMzQ3YjE2YzdmIiwiZW5jcnlwdGVkX2tleSI6IjJ3K3pkdnFycERWM3hSMGJKeTR1Z3c9PSJ9LHsiaWQiOiIyOWYwNWU4Zi1hMWFlLTQ2ZTQtODBlOS0yMmRjZDQ0Y2Q3YTEiLCJlbmNyeXB0ZWRfa2V5IjoiL3hsU0hweHdxdTNnby9nbHBtU2dhUT09In0seyJpZCI6IjY5ZmU3MDc3LWRhZGQtNGI1NS05NmNkLWMzZWRiMzk5MTg1MyIsImVuY3J5cHRlZF9rZXkiOiJ6dTZpdXpOMnBzaTBaU3hRaUFUa1JRPT0ifV19fQ.BXr93Et1krYMVs-CUnf7F3ywJWFRtxYdkR7Qn4w3-to', // eslint-disable-line max-len - }, - }, - { - name: 'Clear, single-Period', - manifestUri: 'https://media.axprod.net/TestVectors/v7-Clear/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.AXINOM, - source: shakaAssets.Source.AXINOM, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - shakaAssets.Feature.TTML, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Clear, multi-Period', - manifestUri: 'https://media.axprod.net/TestVectors/v7-Clear/Manifest_MultiPeriod.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.AXINOM, - source: shakaAssets.Source.AXINOM, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPERIOD, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - shakaAssets.Feature.TTML, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Clear, Live DASH', - manifestUri: 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/weird_rainbow_test_pattern.png', - shortName: 'Test Pattern', - - encoder: shakaAssets.Encoder.AXINOM, - source: shakaAssets.Source.AXINOM, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - shakaAssets.Feature.WEBVTT, - ], - }, - { - name: 'Clear, Live HLS', - manifestUri: 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.m3u8', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/weird_rainbow_test_pattern.png', - shortName: 'Test Pattern', - - encoder: shakaAssets.Encoder.AXINOM, - source: shakaAssets.Source.AXINOM, - drm: [], - features: [ - shakaAssets.Feature.HLS, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.WEBVTT, - ], - }, + new ShakaDemoAssetInfo( + /* name= */ 'Multi-DRM', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://media.axprod.net/TestVectors/v7-MultiDRM-SingleKey/Manifest.mpd', + /* source= */ shakaAssets.Source.AXINOM) + .addKeySystem(shakaAssets.KeySystem.PLAYREADY) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.TTML) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBVTT) + .addLicenseServer('com.widevine.alpha', 'https://drm-widevine-licensing.axtest.net/AcquireLicense') + .addLicenseServer('com.microsoft.playready', 'https://drm-playready-licensing.axtest.net/AcquireLicense') + .addLicenseRequestHeader('X-AxDRM-Message', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7ImlkIjoiOWViNDA1MGQtZTQ0Yi00ODAyLTkzMmUtMjdkNzUwODNlMjY2IiwiZW5jcnlwdGVkX2tleSI6ImxLM09qSExZVzI0Y3Iya3RSNzRmbnc9PSJ9XX19.4lWwW46k-oWcah8oN18LPj5OLS5ZU-_AQv7fe0JhNjA'), // eslint-disable-line max-len + new ShakaDemoAssetInfo( + /* name= */ 'Multi-DRM, multi-key', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://media.axprod.net/TestVectors/v7-MultiDRM-MultiKey/Manifest.mpd', + /* source= */ shakaAssets.Source.AXINOM) + .addKeySystem(shakaAssets.KeySystem.PLAYREADY) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.MULTIKEY) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.TTML) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBVTT) + .addLicenseServer('com.widevine.alpha', 'https://drm-widevine-licensing.axtest.net/AcquireLicense') + .addLicenseServer('com.microsoft.playready', 'https://drm-playready-licensing.axtest.net/AcquireLicense') + .addLicenseRequestHeader('X-AxDRM-Message', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7ImlkIjoiODAzOTliZjUtOGEyMS00MDE0LTgwNTMtZTI3ZTc0OGU5OGMwIiwiZW5jcnlwdGVkX2tleSI6ImxpTkpxVmFZa05oK01LY3hKRms3SWc9PSJ9LHsiaWQiOiI5MDk1M2UwOS02Y2IyLTQ5YTMtYTI2MC03YTVmZWZlYWQ0OTkiLCJlbmNyeXB0ZWRfa2V5Ijoia1l0SEh2cnJmQ01lVmRKNkxrYmtuZz09In0seyJpZCI6IjBlNGRhOTJiLWQwZTgtNGE2Ni04YzNmLWMyNWE5N2ViNjUzMiIsImVuY3J5cHRlZF9rZXkiOiI3dzdOWkhITE1nSjRtUUtFSzVMVE1RPT0ifSx7ImlkIjoiNTg1ZjIzM2YtMzA3Mi00NmYxLTlmYTQtNmRjMjJjNjZhMDE0IiwiZW5jcnlwdGVkX2tleSI6IkFjNFVVbVl0Qko1blBROU4xNXJjM2c9PSJ9LHsiaWQiOiI0MjIyYmQ3OC1iYzQ1LTQxYmYtYjYzZS02ZjgxNGRjMzkxZGYiLCJlbmNyeXB0ZWRfa2V5IjoiTzZGTzBmcVNXb3BwN2JqYy9ENGxNQT09In1dfX0.uF6YlKAREOmbniAeYiH070HSJhV0YS7zSKjlCtiDR5Y'), // eslint-disable-line max-len + new ShakaDemoAssetInfo( + /* name= */ 'Multi-DRM, multi-key, multi-Period', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://media.axprod.net/TestVectors/v7-MultiDRM-MultiKey-MultiPeriod/Manifest.mpd', + /* source= */ shakaAssets.Source.AXINOM) + .addKeySystem(shakaAssets.KeySystem.PLAYREADY) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.MULTIKEY) + .addFeature(shakaAssets.Feature.MULTIPERIOD) + .addFeature(shakaAssets.Feature.TTML) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBVTT) + .addLicenseServer('com.widevine.alpha', 'https://drm-widevine-licensing.axtest.net/AcquireLicense') + .addLicenseServer('com.microsoft.playready', 'https://drm-playready-licensing.axtest.net/AcquireLicense') + .addLicenseRequestHeader('X-AxDRM-Message', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiYjMzNjRlYjUtNTFmNi00YWUzLThjOTgtMzNjZWQ1ZTMxYzc4IiwibWVzc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7ImlkIjoiMDg3Mjc4NmUtZjllNy00NjVmLWEzYTItNGU1YjBlZjhmYTQ1IiwiZW5jcnlwdGVkX2tleSI6IlB3NitlRVlOY3ZqWWJmc2gzWDNmbWc9PSJ9LHsiaWQiOiJjMTRmMDcwOS1mMmI5LTQ0MjctOTE2Yi02MWI1MjU4NjUwNmEiLCJlbmNyeXB0ZWRfa2V5IjoiLzErZk5paDM4bXFSdjR5Y1l6bnQvdz09In0seyJpZCI6IjhiMDI5ZTUxLWQ1NmEtNDRiZC05MTBmLWQ0YjVmZDkwZmJhMiIsImVuY3J5cHRlZF9rZXkiOiJrcTBKdVpFanBGTjhzYVRtdDU2ME9nPT0ifSx7ImlkIjoiMmQ2ZTkzODctNjBjYS00MTQ1LWFlYzItYzQwODM3YjRiMDI2IiwiZW5jcnlwdGVkX2tleSI6IlRjUlFlQld4RW9IT0tIcmFkNFNlVlE9PSJ9LHsiaWQiOiJkZTAyZjA3Zi1hMDk4LTRlZTAtYjU1Ni05MDdjMGQxN2ZiYmMiLCJlbmNyeXB0ZWRfa2V5IjoicG9lbmNTN0dnbWVHRmVvSjZQRUFUUT09In0seyJpZCI6IjkxNGU2OWY0LTBhYjMtNDUzNC05ZTlmLTk4NTM2MTVlMjZmNiIsImVuY3J5cHRlZF9rZXkiOiJlaUkvTXNsbHJRNHdDbFJUL0xObUNBPT0ifSx7ImlkIjoiZGE0NDQ1YzItZGI1ZS00OGVmLWIwOTYtM2VmMzQ3YjE2YzdmIiwiZW5jcnlwdGVkX2tleSI6IjJ3K3pkdnFycERWM3hSMGJKeTR1Z3c9PSJ9LHsiaWQiOiIyOWYwNWU4Zi1hMWFlLTQ2ZTQtODBlOS0yMmRjZDQ0Y2Q3YTEiLCJlbmNyeXB0ZWRfa2V5IjoiL3hsU0hweHdxdTNnby9nbHBtU2dhUT09In0seyJpZCI6IjY5ZmU3MDc3LWRhZGQtNGI1NS05NmNkLWMzZWRiMzk5MTg1MyIsImVuY3J5cHRlZF9rZXkiOiJ6dTZpdXpOMnBzaTBaU3hRaUFUa1JRPT0ifV19fQ.BXr93Et1krYMVs-CUnf7F3ywJWFRtxYdkR7Qn4w3-to'), // eslint-disable-line max-len + new ShakaDemoAssetInfo( + /* name= */ 'Clear, single-Period', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://media.axprod.net/TestVectors/v7-Clear/Manifest.mpd', + /* source= */ shakaAssets.Source.AXINOM) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.TTML) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Clear, multi-Period', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://media.axprod.net/TestVectors/v7-Clear/Manifest_MultiPeriod.mpd', + /* source= */ shakaAssets.Source.AXINOM) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.MULTIPERIOD) + .addFeature(shakaAssets.Feature.TTML) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.ULTRA_HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Clear, Live DASH', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/axinom_test.png', + /* manifestUri= */ 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.mpd', + /* source= */ shakaAssets.Source.AXINOM) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MULTIPERIOD) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.WEBVTT), + new ShakaDemoAssetInfo( + /* name= */ 'Clear, Live HLS', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/axinom_test.png', + /* manifestUri= */ 'https://akamai-axtest.akamaized.net/routes/lapd-v1-acceptance/www_c4/Manifest.m3u8', + /* source= */ shakaAssets.Source.AXINOM) + .addFeature(shakaAssets.Feature.HLS) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION), // }}} // Unified Streaming {{{ // Src: http://demo.unified-streaming.com/features.html - { - name: 'Tears of Steel', - manifestUri: 'https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel.ism/.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.UNIFIED_STREAMING, - source: shakaAssets.Source.UNIFIED_STREAMING, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Tears of Steel (Widevine)', - manifestUri: 'https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-dash-widevine.ism/.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.UNIFIED_STREAMING, - source: shakaAssets.Source.UNIFIED_STREAMING, - drm: [ - shakaAssets.KeySystem.WIDEVINE, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.TTML, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - ], - - licenseServers: { - 'com.widevine.alpha': 'https://cwip-shaka-proxy.appspot.com/no_auth', - }, - }, - { - name: 'Tears of Steel (PlayReady)', - manifestUri: 'https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-dash-playready.ism/.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.UNIFIED_STREAMING, - source: shakaAssets.Source.UNIFIED_STREAMING, - drm: [ - shakaAssets.KeySystem.PLAYREADY, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.TTML, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - ], - - licenseServers: { - 'com.microsoft.playready': 'https://test.playready.microsoft.com/service/rightsmanager.asmx?PlayRight=1&UseSimpleNonPersistentLicense=1', - }, - }, - { - name: 'Tears of Steel (subtitles)', - manifestUri: 'https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-en.ism/.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.UNIFIED_STREAMING, - source: shakaAssets.Source.UNIFIED_STREAMING, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.EMBEDDED_TEXT, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - shakaAssets.Feature.SEGMENTED_TEXT, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.TTML, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.OFFLINE, - ], - }, + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel.ism/.mpd', + /* source= */ shakaAssets.Source.UNIFIED_STREAMING) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel (Widevine)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-dash-widevine.ism/.mpd', + /* source= */ shakaAssets.Source.UNIFIED_STREAMING) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.TTML) + .addLicenseServer('com.widevine.alpha', 'https://cwip-shaka-proxy.appspot.com/no_auth'), + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel (PlayReady)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-dash-playready.ism/.mpd', + /* source= */ shakaAssets.Source.UNIFIED_STREAMING) + .addKeySystem(shakaAssets.KeySystem.PLAYREADY) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.TTML) + .addLicenseServer('com.microsoft.playready', 'https://test.playready.microsoft.com/service/rightsmanager.asmx?PlayRight=1&UseSimpleNonPersistentLicense=1'), + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel (subtitles)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-en.ism/.mpd', + /* source= */ shakaAssets.Source.UNIFIED_STREAMING) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.TTML) + .addFeature(shakaAssets.Feature.OFFLINE), // }}} // DASH-IF assets {{{ // Src: http://dashif.org/test-vectors/ - { - name: 'Big Buck Bunny', - manifestUri: 'https://dash.akamaized.net/dash264/TestCases/1c/qualcomm/2/MultiRate.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', - - encoder: shakaAssets.Encoder.UNKNOWN, - source: shakaAssets.Source.DASH_IF, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Live sim (2s segments)', - manifestUri: 'https://livesim.dashif.org/livesim/utc_head/testpic_2s/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', - - encoder: shakaAssets.Encoder.UNKNOWN, - source: shakaAssets.Source.DASH_IF, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - ], - }, - { - name: 'Live sim SegmentTimeline w $Time$ (6s segments)', - manifestUri: 'https://livesim.dashif.org/livesim/segtimeline_1/utc_head/testpic_6s/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', - - encoder: shakaAssets.Encoder.UNKNOWN, - source: shakaAssets.Source.DASH_IF, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE_TIME, - ], - }, - { - name: 'Live sim SegmentTimeline w $Number$ (6s segments)', - manifestUri: 'https://livesim.dashif.org/livesim/segtimelinenr_1/utc_head/testpic_6s/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', - - encoder: shakaAssets.Encoder.UNKNOWN, - source: shakaAssets.Source.DASH_IF, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE_NUMBER, - ], - }, - { - name: 'Live sim SegmentTimeline StartOver [-20s, +20s] (2s segments)', - manifestUri: 'https://livesim.dashif.org/livesim/segtimeline_1/startrel_-20/stoprel_20/timeoffset_0/testpic_2s/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', - - encoder: shakaAssets.Encoder.UNKNOWN, - source: shakaAssets.Source.DASH_IF, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE_TIME, - ], - }, - { - name: 'Live sim StartOver SegTmpl Duration [-20s, +20s] (2s segments)', - manifestUri: 'https://livesim.dashif.org/livesim/startrel_-20/stoprel_20/timeoffset_0/testpic_2s/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', - - encoder: shakaAssets.Encoder.UNKNOWN, - source: shakaAssets.Source.DASH_IF, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - ], - }, - { - name: 'Live sim SegTmpl Duration (multi-period 60s)', - manifestUri: 'https://livesim.dashif.org/livesim/utc_head/periods_60/testpic_2s/Manifest.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', - - encoder: shakaAssets.Encoder.UNKNOWN, - source: shakaAssets.Source.DASH_IF, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPERIOD, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - ], - }, - { - name: 'Live sim TTML Image Subtitles embedded (VoD)', - manifestUri: 'https://livesim.dashif.org/dash/vod/testpic_2s/img_subs.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', - - encoder: shakaAssets.Encoder.UNKNOWN, - source: shakaAssets.Source.DASH_IF, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.TTML, - ], - }, + new ShakaDemoAssetInfo( + /* name= */ 'Big Buck Bunny (DASH-IF)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', + /* manifestUri= */ 'https://dash.akamaized.net/dash264/TestCases/1c/qualcomm/2/MultiRate.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Live sim (2s segments)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim.dashif.org/livesim/utc_head/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.DASH), + new ShakaDemoAssetInfo( + /* name= */ 'Live sim SegmentTimeline w $Time$ (6s segments)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim.dashif.org/livesim/segtimeline_1/utc_head/testpic_6s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MP4), + new ShakaDemoAssetInfo( + /* name= */ 'Live sim SegmentTimeline w $Number$ (6s segments)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim.dashif.org/livesim/segtimelinenr_1/utc_head/testpic_6s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MP4), + new ShakaDemoAssetInfo( + /* name= */ 'Live sim SegmentTimeline StartOver [-20s, +20s] (2s segments)', // eslint-disable-line max-len + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim.dashif.org/livesim/segtimeline_1/startrel_-20/stoprel_20/timeoffset_0/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4), + new ShakaDemoAssetInfo( + /* name= */ 'Live sim StartOver SegTmpl Duration [-20s, +20s] (2s segments)', // eslint-disable-line max-len + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim.dashif.org/livesim/startrel_-20/stoprel_20/timeoffset_0/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4), + new ShakaDemoAssetInfo( + /* name= */ 'Live sim SegTmpl Duration (multi-period 60s)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim.dashif.org/livesim/utc_head/periods_60/testpic_2s/Manifest.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPERIOD), + new ShakaDemoAssetInfo( + /* name= */ 'Live sim TTML Image Subtitles embedded (VoD)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/dash_if_test_pattern.png', + /* manifestUri= */ 'https://livesim.dashif.org/dash/vod/testpic_2s/img_subs.mpd', + /* source= */ shakaAssets.Source.DASH_IF) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.TTML), // }}} // Wowza assets {{{ // Src: http://www.dash-player.com/demo/streaming-server-and-encoder-support/ - { - name: 'Big Buck Bunny (Live)', - manifestUri: 'https://wowzaec2demo.streamlock.net/live/bigbuckbunny/manifest_mpm4sav_mvtime.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', - shortName: 'Big Buck Bunny', - - encoder: shakaAssets.Encoder.WOWZA, - source: shakaAssets.Encoder.WOWZA, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.LIVE, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - ], - }, + new ShakaDemoAssetInfo( + /* name= */ 'Big Buck Bunny (Live)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', + /* manifestUri= */ 'https://wowzaec2demo.streamlock.net/live/bigbuckbunny/manifest_mpm4sav_mvtime.mpd', + /* source= */ shakaAssets.Source.WOWZA) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.LIVE) + .addFeature(shakaAssets.Feature.MP4), // }}} // bitcodin assets {{{ // Src: http://www.dash-player.com/demo/streaming-server-and-encoder-support/ // Src: https://bitmovin.com/mpeg-dash-hls-examples-sample-streams/ - { - name: 'Art of Motion (DASH)', - manifestUri: 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/art_of_motion.png', - shortName: 'Art of Motion', - - encoder: shakaAssets.Encoder.BITCODIN, - source: shakaAssets.Source.BITCODIN, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Art of Motion (HLS, TS)', - manifestUri: 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/art_of_motion.png', - shortName: 'Art of Motion', - - encoder: shakaAssets.Encoder.BITCODIN, - source: shakaAssets.Source.BITCODIN, - drm: [], - features: [ - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.HLS, - shakaAssets.Feature.MP2TS, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Sintel (HLS, TS, 4k)', - manifestUri: 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - shortName: 'Sintel', - - encoder: shakaAssets.Encoder.BITCODIN, - source: shakaAssets.Source.BITCODIN, - drm: [], - features: [ - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.HLS, - shakaAssets.Feature.MP2TS, - shakaAssets.Feature.ULTRA_HIGH_DEFINITION, - shakaAssets.Feature.OFFLINE, - ], - }, + new ShakaDemoAssetInfo( + /* name= */ 'Art of Motion (DASH)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/art_of_motion.png', + /* manifestUri= */ 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/mpds/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.mpd', + /* source= */ shakaAssets.Source.BITCODIN) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Art of Motion (HLS, TS)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/art_of_motion.png', + /* manifestUri= */ 'https://bitdash-a.akamaihd.net/content/MI201109210084_1/m3u8s/f08e80da-bf1d-4e3d-8899-f0f6155f6efa.m3u8', + /* source= */ shakaAssets.Source.BITCODIN) + .markAsDisabled() + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.HLS) + .addFeature(shakaAssets.Feature.MP2TS) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Sintel (HLS, TS, 4k)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8', + /* source= */ shakaAssets.Source.BITCODIN) + .markAsDisabled() + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.HLS) + .addFeature(shakaAssets.Feature.MP2TS) + .addFeature(shakaAssets.Feature.OFFLINE), // }}} // Nimble Streamer assets {{{ // Src: http://www.dash-player.com/demo/streaming-server-and-encoder-support/ - { - name: 'Big Buck Bunny', - manifestUri: 'https://video.wmspanel.com/local/raw/BigBuckBunny_320x180.mp4/manifest.mpd', - // As of 2017-08-04, there is a common name mismatch error with this site's - // SSL certificate. See https://github.com/google/shaka-player/issues/955 - disabled: true, - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', - - encoder: shakaAssets.Encoder.NIMBLE_STREAMER, - source: shakaAssets.Source.NIMBLE_STREAMER, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - ], - }, + // As of 2017-08-04, there is a common name mismatch error with this site's + // SSL certificate. See https://github.com/google/shaka-player/issues/955 + new ShakaDemoAssetInfo( + /* name= */ 'Big Buck Bunny (Nimble)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', + /* manifestUri= */ 'https://video.wmspanel.com/local/raw/BigBuckBunny_320x180.mp4/manifest.mpd', + /* source= */ shakaAssets.Source.NIMBLE_STREAMER) + .markAsDisabled() + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION), // }}} // Azure Media Services assets {{{ // Src: http://amp.azure.net/libs/amp/latest/docs/samples.html - { - name: 'Azure Trailer', - manifestUri: 'https://amssamples.streaming.mediaservices.windows.net/91492735-c523-432b-ba01-faba6c2206a2/AzureMediaServicesPromo.ism/manifest(format=mpd-time-csf)', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/azure.png', - - encoder: shakaAssets.Encoder.AZURE_MEDIA_SERVICES, - source: shakaAssets.Source.AZURE_MEDIA_SERVICES, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'Big Buck Bunny', - manifestUri: 'https://amssamples.streaming.mediaservices.windows.net/622b189f-ec39-43f2-93a2-201ac4e31ce1/BigBuckBunny.ism/manifest(format=mpd-time-csf)', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', - - encoder: shakaAssets.Encoder.AZURE_MEDIA_SERVICES, - source: shakaAssets.Source.AZURE_MEDIA_SERVICES, - drm: [ - shakaAssets.KeySystem.PLAYREADY, - shakaAssets.KeySystem.WIDEVINE, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - shakaAssets.Feature.OFFLINE, - ], - - licenseServers: { - 'com.widevine.alpha': 'https://amssamples.keydelivery.mediaservices.windows.net/Widevine/?KID=1ab45440-532c-4399-94dc-5c5ad9584bac', - 'com.microsoft.playready': 'https://amssamples.keydelivery.mediaservices.windows.net/PlayReady/', - }, - }, - { - name: 'Tears Of Steel (external text)', - manifestUri: 'https://ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TearsOfSteel_WAMEH264SmoothStreaming720p.ism/manifest(format=mpd-time-csf)', - extraText: [ - { - uri: 'https://ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TOS-en.vtt', - language: 'en', - kind: 'subtitle', - mime: 'text/vtt', - }, - { - uri: 'https://ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TOS-es.vtt', - language: 'es', - kind: 'subtitle', - mime: 'text/vtt', - }, - { - uri: 'https://ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TOS-fr.vtt', - language: 'fr', - kind: 'subtitle', - mime: 'text/vtt', - }, - ], - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', - shortName: 'Tears of Steel', - - encoder: shakaAssets.Encoder.AZURE_MEDIA_SERVICES, - source: shakaAssets.Source.AZURE_MEDIA_SERVICES, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE, - shakaAssets.Feature.SUBTITLES, - shakaAssets.Feature.WEBVTT, - shakaAssets.Feature.OFFLINE, - ], - }, + new ShakaDemoAssetInfo( + /* name= */ 'Azure Trailer', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/azure.png', + /* manifestUri= */ 'https://amssamples.streaming.mediaservices.windows.net/91492735-c523-432b-ba01-faba6c2206a2/AzureMediaServicesPromo.ism/manifest(format=mpd-time-csf)', + /* source= */ shakaAssets.Source.AZURE_MEDIA_SERVICES) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'Big Buck Bunny (Azure)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', + /* manifestUri= */ 'https://amssamples.streaming.mediaservices.windows.net/622b189f-ec39-43f2-93a2-201ac4e31ce1/BigBuckBunny.ism/manifest(format=mpd-time-csf)', + /* source= */ shakaAssets.Source.AZURE_MEDIA_SERVICES) + .addKeySystem(shakaAssets.KeySystem.PLAYREADY) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.OFFLINE) + .addLicenseServer('com.widevine.alpha', 'https://amssamples.keydelivery.mediaservices.windows.net/Widevine/?KID=1ab45440-532c-4399-94dc-5c5ad9584bac') + .addLicenseServer('com.microsoft.playready', 'https://amssamples.keydelivery.mediaservices.windows.net/PlayReady/'), + new ShakaDemoAssetInfo( + /* name= */ 'Tears of Steel (external text)', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/tears_of_steel.png', + /* manifestUri= */ 'https://ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TearsOfSteel_WAMEH264SmoothStreaming720p.ism/manifest(format=mpd-time-csf)', + /* source= */ shakaAssets.Source.AZURE_MEDIA_SERVICES) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.OFFLINE) + .addExtraText({ + uri: 'https://ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TOS-en.vtt', + language: 'en', + kind: 'subtitle', + mime: 'text/vtt', + }).addExtraText({ + uri: 'https://ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TOS-es.vtt', + language: 'es', + kind: 'subtitle', + mime: 'text/vtt', + }).addExtraText({ + uri: 'https://ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TOS-fr.vtt', + language: 'fr', + kind: 'subtitle', + mime: 'text/vtt', + }), // }}} // GPAC assets {{{ // Src: https://gpac.wp.mines-telecom.fr/2012/02/23/dash-sequences/ // NOTE: The assets here using the "live profile" are not actually // "live streams". The content is still static, as is the timeline. - { - name: 'live profile', - manifestUri: 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-live/mp4-live-mpd-AV-BS.mpd', - // NOTE: Multiple SPS/PPS in init segment, no sample duration - // NOTE: Decoder errors on Mac - // https://github.com/gpac/gpac/issues/600 - // https://bugs.webkit.org/show_bug.cgi?id=160459 - disabled: true, - // TODO: Get actual icon? - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - - encoder: shakaAssets.Encoder.MP4BOX, - source: shakaAssets.Source.GPAC, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - ], - }, - { - name: 'live profile with five periods', - manifestUri: 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-live-periods/mp4-live-periods-mpd.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', - - encoder: shakaAssets.Encoder.MP4BOX, - source: shakaAssets.Source.GPAC, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPERIOD, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'main profile, single file', - manifestUri: 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-main-single/mp4-main-single-mpd-AV-NBS.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', - - encoder: shakaAssets.Encoder.MP4BOX, - source: shakaAssets.Source.GPAC, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_LIST_DURATION, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'main profile, mutiple files', - manifestUri: 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-main-multi/mp4-main-multi-mpd-AV-BS.mpd', - // NOTE: Multiple SPS/PPS in init segment, no sample duration - // NOTE: Decoder errors on Mac - // https://github.com/gpac/gpac/issues/600 - // https://bugs.webkit.org/show_bug.cgi?id=160459 - disabled: true, - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', - - encoder: shakaAssets.Encoder.MP4BOX, - source: shakaAssets.Source.GPAC, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_LIST_DURATION, - ], - }, - { - name: 'onDemand profile', - manifestUri: 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-onDemand/mp4-onDemand-mpd-AV.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', - - encoder: shakaAssets.Encoder.MP4BOX, - source: shakaAssets.Source.GPAC, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_BASE, - shakaAssets.Feature.OFFLINE, - ], - }, - { - name: 'main profile, open GOP', - manifestUri: 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-main-ogop/mp4-main-ogop-mpd-AV-BS.mpd', - // NOTE: Segments do not start with keyframes - // NOTE: Decoder errors on Safari - // https://bugs.webkit.org/show_bug.cgi?id=160460 - disabled: true, - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', - - encoder: shakaAssets.Encoder.MP4BOX, - source: shakaAssets.Source.GPAC, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - ], - }, - { - name: 'full profile, gradual decoding refresh', - manifestUri: 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-full-gdr/mp4-full-gdr-mpd-AV-BS.mpd', - // NOTE: segments do not start with keyframes - // NOTE: Decoder errors on Safari - // https://bugs.webkit.org/show_bug.cgi?id=160460 - disabled: true, - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', - - encoder: shakaAssets.Encoder.MP4BOX, - source: shakaAssets.Source.GPAC, - drm: [], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.SEGMENT_TEMPLATE_DURATION, - ], - }, + // TODO: Get actual icon? + // NOTE: Multiple SPS/PPS in init segment, no sample duration + // NOTE: Decoder errors on Mac + // https://github.com/gpac/gpac/issues/600 + // https://bugs.webkit.org/show_bug.cgi?id=160459 + new ShakaDemoAssetInfo( + /* name= */ 'live profile', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-live/mp4-live-mpd-AV-BS.mpd', + /* source= */ shakaAssets.Source.GPAC) + .markAsDisabled() + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4), + new ShakaDemoAssetInfo( + /* name= */ 'live profile with five periods', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', + /* manifestUri= */ 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-live-periods/mp4-live-periods-mpd.mpd', + /* source= */ shakaAssets.Source.GPAC) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPERIOD) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.OFFLINE), + new ShakaDemoAssetInfo( + /* name= */ 'main profile, single file', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', + /* manifestUri= */ 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-main-single/mp4-main-single-mpd-AV-NBS.mpd', + /* source= */ shakaAssets.Source.GPAC) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.OFFLINE), + // NOTE: Multiple SPS/PPS in init segment, no sample duration + // NOTE: Decoder errors on Mac + // https://github.com/gpac/gpac/issues/600 + // https://bugs.webkit.org/show_bug.cgi?id=160459 + new ShakaDemoAssetInfo( + /* name= */ 'main profile, multiple files', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', + /* manifestUri= */ 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-main-multi/mp4-main-multi-mpd-AV-BS.mpd', + /* source= */ shakaAssets.Source.GPAC) + .markAsDisabled() + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4), + new ShakaDemoAssetInfo( + /* name= */ 'onDemand profile', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', + /* manifestUri= */ 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-onDemand/mp4-onDemand-mpd-AV.mpd', + /* source= */ shakaAssets.Source.GPAC) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.OFFLINE), + // NOTE: Segments do not start with keyframes + // NOTE: Decoder errors on Safari + // https://bugs.webkit.org/show_bug.cgi?id=160460 + new ShakaDemoAssetInfo( + /* name= */ 'main profile, open GOP', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', + /* manifestUri= */ 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-main-ogop/mp4-main-ogop-mpd-AV-BS.mpd', + /* source= */ shakaAssets.Source.GPAC) + .markAsDisabled() + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4), + // NOTE: segments do not start with keyframes + // NOTE: Decoder errors on Safari + // https://bugs.webkit.org/show_bug.cgi?id=160460 + new ShakaDemoAssetInfo( + /* name= */ 'full profile, gradual decoding refresh', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/gpac_test_pattern.png', + /* manifestUri= */ 'https://download.tsi.telecom-paristech.fr/gpac/DASH_CONFORMANCE/TelecomParisTech/mp4-full-gdr/mp4-full-gdr-mpd-AV-BS.mpd', + /* source= */ shakaAssets.Source.GPAC) + .markAsDisabled() + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4), // }}} // Verizon Digital Media Services (VDMS) assets {{{ - { - name: 'Multi DRM - 8 Byte IV', - // Reliable Playready playback requires Edge 16+ - // The playenabler and sl url parameters allow for playback in VMs - manifestUri: 'https://content.uplynk.com/847859273a4b4a81959d8fea181672a4.mpd?pr.version=2&pr.playenabler=B621D91F-EDCC-4035-8D4B-DC71760D43E9&pr.securitylevel=150', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/azure.png', - - encoder: shakaAssets.Encoder.UPLYNK, - source: shakaAssets.Source.UPLYNK, - drm: [ - shakaAssets.KeySystem.PLAYREADY, - shakaAssets.KeySystem.WIDEVINE, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.PSSH, - shakaAssets.Feature.MULTIKEY, - shakaAssets.Feature.AESCTR_8_BYTE_IV, - shakaAssets.Feature.SEGMENT_LIST_DURATION, - shakaAssets.Feature.HIGH_DEFINITION, - ], - licenseServers: { - 'com.microsoft.playready': 'https://content.uplynk.com/pr', - 'com.widevine.alpha': 'https://content.uplynk.com/wv', - }, - requestFilter: shakaAssets.UplynkRequestFilter, - responseFilter: shakaAssets.UplynkResponseFilter, - }, - { - name: 'Multi DRM - MultiPeriod - 8 Byte IV', - // Reliable Playready playback requires Edge 16+ - // The playenabler and sl url parameters allow for playback in VMs - manifestUri: 'https://content.uplynk.com/054225d59be2454fabdca3e96912d847.mpd?ad=cleardash&pr.version=2&pr.playenabler=B621D91F-EDCC-4035-8D4B-DC71760D43E9&pr.securitylevel=150', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - - encoder: shakaAssets.Encoder.UPLYNK, - source: shakaAssets.Source.UPLYNK, - drm: [ - shakaAssets.KeySystem.PLAYREADY, - shakaAssets.KeySystem.WIDEVINE, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.PSSH, - shakaAssets.Feature.MULTIKEY, - shakaAssets.Feature.MULTIPERIOD, - shakaAssets.Feature.SEGMENT_LIST_DURATION, - shakaAssets.Feature.AESCTR_8_BYTE_IV, - shakaAssets.Feature.HIGH_DEFINITION, - ], - licenseServers: { - 'com.microsoft.playready': 'https://content.uplynk.com/pr', - 'com.widevine.alpha': 'https://content.uplynk.com/wv', - }, - requestFilter: shakaAssets.UplynkRequestFilter, - responseFilter: shakaAssets.UplynkResponseFilter, - }, - { - name: 'Widevine - 16 Byte IV', - manifestUri: 'https://content.uplynk.com/224ac8717e714b68831997ab6cea4015.mpd', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', - - encoder: shakaAssets.Encoder.UPLYNK, - source: shakaAssets.Source.UPLYNK, - drm: [ - shakaAssets.KeySystem.WIDEVINE, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.PSSH, - shakaAssets.Feature.MULTIKEY, - shakaAssets.Feature.AESCTR_16_BYTE_IV, - shakaAssets.Feature.SEGMENT_LIST_DURATION, - shakaAssets.Feature.HIGH_DEFINITION, - ], - licenseServers: { - 'com.widevine.alpha': 'https://content.uplynk.com/wv', - }, - requestFilter: shakaAssets.UplynkRequestFilter, - responseFilter: shakaAssets.UplynkResponseFilter, - }, - { - name: 'Widevine - 16 Byte IV - (mix of encrypted and unencrypted periods)', - // Unencrypted periods interspersed with protected periods - // Doesn't work on Chrome < 58 - manifestUri: 'https://content.uplynk.com/1eb40d8e64234f5c9879db7045c3d48c.mpd?ad=cleardash&rays=cdefg', - - iconUri: 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', - - encoder: shakaAssets.Encoder.UPLYNK, - source: shakaAssets.Source.UPLYNK, - drm: [ - shakaAssets.KeySystem.WIDEVINE, - ], - features: [ - shakaAssets.Feature.DASH, - shakaAssets.Feature.MP4, - shakaAssets.Feature.MULTIPLE_LANGUAGES, - shakaAssets.Feature.SEGMENT_LIST_DURATION, - shakaAssets.Feature.PSSH, - shakaAssets.Feature.HIGH_DEFINITION, - shakaAssets.Feature.MULTIPERIOD, - shakaAssets.Feature.MULTIKEY, - shakaAssets.Feature.AESCTR_16_BYTE_IV, - shakaAssets.Feature.ENCRYPTED_WITH_CLEAR, - ], - licenseServers: { - 'com.widevine.alpha': 'https://content.uplynk.com/wv', - }, - requestFilter: shakaAssets.UplynkRequestFilter, - responseFilter: shakaAssets.UplynkResponseFilter, - }, + // Reliable Playready playback requires Edge 16+ + // The playenabler and sl url parameters allow for playback in VMs + new ShakaDemoAssetInfo( + /* name= */ 'Multi DRM - 8 Byte IV', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/azure.png', + /* manifestUri= */ 'https://content.uplynk.com/847859273a4b4a81959d8fea181672a4.mpd?pr.version=2&pr.playenabler=B621D91F-EDCC-4035-8D4B-DC71760D43E9&pr.securitylevel=150', + /* source= */ shakaAssets.Source.UPLYNK) + .addKeySystem(shakaAssets.KeySystem.PLAYREADY) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIKEY) + .addFeature(shakaAssets.Feature.AESCTR_8_BYTE_IV) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addLicenseServer('com.microsoft.playready', 'https://content.uplynk.com/pr') + .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') + .setRequestFilter(shakaAssets.UplynkRequestFilter) + .setResponseFilter(shakaAssets.UplynkResponseFilter), + // Reliable Playready playback requires Edge 16+ + // The playenabler and sl url parameters allow for playback in VMs + new ShakaDemoAssetInfo( + /* name= */ 'Multi DRM - MultiPeriod - 8 Byte IV', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://content.uplynk.com/054225d59be2454fabdca3e96912d847.mpd?ad=cleardash&pr.version=2&pr.playenabler=B621D91F-EDCC-4035-8D4B-DC71760D43E9&pr.securitylevel=150', + /* source= */ shakaAssets.Source.UPLYNK) + .addKeySystem(shakaAssets.KeySystem.PLAYREADY) + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.SUBTITLES) + .addFeature(shakaAssets.Feature.MULTIKEY) + .addFeature(shakaAssets.Feature.MULTIPERIOD) + .addFeature(shakaAssets.Feature.WEBVTT) + .addFeature(shakaAssets.Feature.AESCTR_8_BYTE_IV) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addLicenseServer('com.microsoft.playready', 'https://content.uplynk.com/pr') + .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') + .setRequestFilter(shakaAssets.UplynkRequestFilter) + .setResponseFilter(shakaAssets.UplynkResponseFilter), + new ShakaDemoAssetInfo( + /* name= */ 'Widevine - 16 Byte IV', + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/big_buck_bunny.png', + /* manifestUri= */ 'https://content.uplynk.com/224ac8717e714b68831997ab6cea4015.mpd', + /* source= */ shakaAssets.Source.UPLYNK) + // Disabled until we figure out the CORS errors and PlayReady status. + .markAsDisabled() + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIKEY) + .addFeature(shakaAssets.Feature.AESCTR_16_BYTE_IV) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') + .setRequestFilter(shakaAssets.UplynkRequestFilter) + .setResponseFilter(shakaAssets.UplynkResponseFilter), + // Unencrypted periods interspersed with protected periods + // Doesn't work on Chrome < 58 + new ShakaDemoAssetInfo( + /* name= */ 'Widevine - 16 Byte IV - (mix of encrypted and unencrypted periods)', // eslint-disable-line max-len + /* iconUri= */ 'https://storage.googleapis.com/shaka-asset-icons/sintel.png', + /* manifestUri= */ 'https://content.uplynk.com/1eb40d8e64234f5c9879db7045c3d48c.mpd?ad=cleardash&rays=cdefg', + /* source= */ shakaAssets.Source.UPLYNK) + // Disabled until we figure out the CORS errors and PlayReady status. + .markAsDisabled() + .addKeySystem(shakaAssets.KeySystem.WIDEVINE) + .addFeature(shakaAssets.Feature.DASH) + .addFeature(shakaAssets.Feature.MP4) + .addFeature(shakaAssets.Feature.MULTIPLE_LANGUAGES) + .addFeature(shakaAssets.Feature.MULTIPERIOD) + .addFeature(shakaAssets.Feature.MULTIKEY) + .addFeature(shakaAssets.Feature.HIGH_DEFINITION) + .addFeature(shakaAssets.Feature.AESCTR_16_BYTE_IV) + .addFeature(shakaAssets.Feature.ENCRYPTED_WITH_CLEAR) + .addLicenseServer('com.widevine.alpha', 'https://content.uplynk.com/wv') + .setRequestFilter(shakaAssets.UplynkRequestFilter) + .setResponseFilter(shakaAssets.UplynkResponseFilter), // }}} ]; diff --git a/demo/config.js b/demo/config.js new file mode 100644 index 000000000..306daa773 --- /dev/null +++ b/demo/config.js @@ -0,0 +1,584 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** @type {?ShakaDemoConfig} */ +let shakaDemoConfig; + + +/** + * Shaka Player demo, configuration page layout. + */ +class ShakaDemoConfig { + /** + * Register the page configuration. + */ + static init() { + const container = shakaDemoMain.getHamburgerMenu(); + shakaDemoConfig = new ShakaDemoConfig(container); + } + + /** @param {!Element} container */ + constructor(container) { + /** @private {!Element} */ + this.container_ = container; + + /** + * A list of all sections. + * @private {!Array.} + */ + this.sections_ = []; + + /** + * The input object for the control currently being constructed. + * @private {?ShakaDemoInput} + */ + this.latestInput_ = null; + + /** @private {!Set.} */ + this.addedFields_ = new Set(); + + this.reload_(); + + // Listen to external config changes (i.e. from hash changes). + document.addEventListener('shaka-main-config-change', () => { + // Respond to them by remaking. This is to avoid triggering any config + // changes based on the config changes. + this.reloadAndSaveState_(); + }); + } + + /** @private */ + reload_() { + shaka.ui.Utils.removeAllChildren(this.container_); + this.sections_ = []; + + this.addMetaSection_(); + this.addLanguageSection_(); + this.addAbrSection_(); + this.addDrmSection_(); + this.addStreamingSection_(); + this.addManifestSection_(); + this.addRetrictionsSection_('', ''); + this.checkSectionCompleteness_(); + this.checkSectionValidity_(); + } + + /** + * Remake the contents of the div. Unlike |reload_|, this will also remember + * which sections were open. + * @private + */ + reloadAndSaveState_() { + const wasOpenArray = this.sections_.map((section) => section.getIsOpen()); + this.reload_(); + for (let i = 0; i < wasOpenArray.length; i++) { + const wasOpen = wasOpenArray[i]; + const section = this.sections_[i]; + if (wasOpen) { + section.open(); + } + } + + // Update the componentHandler, to account for any new MDL elements added. + componentHandler.upgradeDom(); + } + + /** @private */ + addDrmSection_() { + const docLink = this.resolveExternLink_('.DrmConfiguration'); + this.addSection_('DRM', docLink) + .addBoolInput_('Delay License Request Until Played', + 'drm.delayLicenseRequestUntilPlayed'); + const advanced = shakaDemoMain.getConfiguration().drm.advanced; + const robustnessSuggestions = [ + 'SW_SECURE_CRYPTO', + 'SW_SECURE_DECODE', + 'HW_SECURE_CRYPTO', + 'HW_SECURE_DECODE', + 'HW_SECURE_ALL', + ]; + const commonDrmSystems = [ + 'com.widevine.alpha', + 'com.microsoft.playready', + 'com.apple.fps.1_0', + 'com.adobe.primetime', + 'org.w3.clearkey', + ]; + const addRobustnessField = (name, valueName) => { + // All robustness fields of a given type are set at once. + this.addDatalistInput_(name, robustnessSuggestions, (input) => { + // Add in any common drmSystem not currently in advanced. + for (let drmSystem of commonDrmSystems) { + if (!(drmSystem in advanced)) { + advanced[commonDrmSystems] = {}; + } + } + // Set the robustness. + for (let drmSystem in advanced) { + advanced[drmSystem][valueName] = input.value; + } + shakaDemoMain.configure('drm.advanced', advanced); + shakaDemoMain.remakeHash(); + }); + const keySystem = Object.keys(advanced)[0]; + if (keySystem) { + const currentRobustness = advanced[keySystem][valueName]; + this.latestInput_.input().value = currentRobustness; + } + }; + addRobustnessField('Video Robustness', 'videoRobustness'); + addRobustnessField('Audio Robustness', 'audioRobustness'); + this.addRetrySection_('drm', 'DRM'); + } + + /** @private */ + addManifestSection_() { + const docLink = this.resolveExternLink_('.ManifestConfiguration'); + this.addSection_('Manifest', docLink) + .addBoolInput_('Ignore DASH DRM Info', 'manifest.dash.ignoreDrmInfo') + .addBoolInput_('Xlink Should Fail Gracefully', + 'manifest.dash.xlinkFailGracefully') + .addNumberInput_('Availability Window Override', + 'manifest.availabilityWindowOverride', + /* canBeDecimal = */ true, + /* canBeZero = */ false, + /* canBeUnset = */ true) + .addTextInput_('Clock Sync URI', 'manifest.dash.clockSyncUri') + .addBoolInput_('Ignore DRM Info', 'manifest.dash.ignoreDrmInfo') + .addNumberInput_('Default Presentation Delay', + 'manifest.dash.defaultPresentationDelay') + .addBoolInput_('Ignore Min Buffer Time', + 'manifest.dash.ignoreMinBufferTime'); + + this.addRetrySection_('manifest', 'Manifest'); + } + + /** @private */ + addAbrSection_() { + const docLink = this.resolveExternLink_('.AbrConfiguration'); + this.addSection_('Adaptation', docLink) + .addBoolInput_('Enabled', 'abr.enabled') + .addNumberInput_('Default Bandwidth Estimate', + 'abr.defaultBandwidthEstimate') + .addNumberInput_('Bandwidth Downgrade Target', + 'abr.bandwidthDowngradeTarget', + /* canBeDecimal = */ true) + .addNumberInput_('Bandwidth Upgrade Target', + 'abr.bandwidthUpgradeTarget', + /* canBeDecimal = */ true) + .addNumberInput_('Switch Interval', + 'abr.switchInterval', + /* canBeDecimal = */ true); + this.addRetrictionsSection_('abr', 'Adaptation'); + } + + /** + * @param {string} category + * @param {string} categoryName + * @private + */ + addRetrictionsSection_(category, categoryName) { + const prefix = (category ? category + '.' : '') + 'restrictions.'; + const sectionName = (categoryName ? categoryName + ' ' : '') + + 'Restrictions'; + const docLink = this.resolveExternLink_('.Restrictions'); + this.addSection_(sectionName, docLink) + .addNumberInput_('Min Width', prefix + 'minWidth') + .addNumberInput_('Max Width', prefix + 'maxWidth') + .addNumberInput_('Min Height', prefix + 'minHeight') + .addNumberInput_('Max Height', prefix + 'maxHeight') + .addNumberInput_('Min Pixels', prefix + 'minPixels') + .addNumberInput_('Max Pixels', prefix + 'maxPixels') + .addNumberInput_('Min Bandwidth', prefix + 'minBandwidth') + .addNumberInput_('Max Bandwidth', prefix + 'maxBandwidth'); + } + + /** + * @param {string} category + * @param {string} categoryName + * @private + */ + addRetrySection_(category, categoryName) { + const prefix = category + '.retryParameters.'; + const docLink = this.resolveExternLink_('.RetryParameters'); + this.addSection_(categoryName + ' Retry Parameters', docLink) + .addNumberInput_('Max Attempts', prefix + 'maxAttempts') + .addNumberInput_('Base Delay', + prefix + 'baseDelay', + /* canBeDecimal = */ true) + .addNumberInput_('Backoff Factor', + prefix + 'backoffFactor', + /* canBeDecimal = */ true) + .addNumberInput_('Fuzz Factor', + prefix + 'fuzzFactor', + /* canBeDecimal = */ true) + .addNumberInput_('Timeout', + prefix + 'timeout', + /* canBeDecimal = */ true); + } + + /** @private */ + addStreamingSection_() { + const docLink = this.resolveExternLink_('.StreamingConfiguration'); + this.addSection_('Streaming', docLink) + .addNumberInput_('Maximum Small Gap Size', 'streaming.smallGapLimit', + /* canBeDecimal = */ true) + .addNumberInput_('Buffering Goal', 'streaming.bufferingGoal', + /* canBeDecimal = */ true) + .addNumberInput_('Duration Backoff', 'streaming.durationBackoff', + /* canBeDecimal = */ true) + .addNumberInput_('Rebuffering Goal', 'streaming.rebufferingGoal', + /* canBeDecimal = */ true) + .addNumberInput_('Buffer Behind', 'streaming.bufferBehind', + /* canBeDecimal = */ true); + + if (!shakaDemoMain.getNativeControlsEnabled()) { + this.addBoolInput_('Always Stream Text', 'streaming.alwaysStreamText'); + } else { + // Add a fake custom fixed "input" that warns the users not to change it. + const noop = (input) => {}; + const tooltipMessage = 'Text must always be streamed while native ' + + 'controls are enabled, for captions to work.'; + this.addCustomBoolInput_('Always Stream Text', noop, tooltipMessage); + this.latestInput_.input().disabled = true; + this.latestInput_.input().checked = true; + } + + this.addBoolInput_('Jump Large Gaps', 'streaming.jumpLargeGaps') + .addBoolInput_('Force Transmux TS', 'streaming.forceTransmuxTS') + .addBoolInput_('Start At Segment Boundary', + 'streaming.startAtSegmentBoundary') + .addBoolInput_('Ignore Text Stream Failures', + 'streaming.ignoreTextStreamFailures'); + this.addRetrySection_('streaming', 'Streaming'); + } + + /** @private */ + addLanguageSection_() { + const docLink = this.resolveExternLink_('.PlayerConfiguration'); + this.addSection_('Language', docLink) + .addTextInput_('Preferred Audio Language', 'preferredAudioLanguage') + .addTextInput_('Preferred Text Language', 'preferredTextLanguage'); + const onChange = (input) => { + shakaDemoMain.setUILocale(input.value); + }; + this.addCustomTextInput_('Preferred UI Locale', onChange); + this.latestInput_.input().value = shakaDemoMain.getUILocale(); + this.addNumberInput_('Preferred Audio Channel Count', + 'preferredAudioChannelCount'); + } + + /** @private */ + addMetaSection_() { + this.addSection_(/* name = */ '', /* docLink = */ null); + + this.addCustomBoolInput_('Shaka Controls', (input) => { + shakaDemoMain.setNativeControlsEnabled(!input.checked); + if (input.checked) { + // Forcibly set |streaming.alwaysStreamText| to true. + shakaDemoMain.configure('streaming.alwaysStreamText', true); + shakaDemoMain.remakeHash(); + } + // Enabling/disabling Shaka Controls will change how other controls in + // the config work, so reload the page. + this.reloadAndSaveState_(); + }); + // TODO: Re-add the tooltipMessage of 'Takes effect next load.' once we + // are ready to add ALL of the tooltip messages. + if (!shakaDemoMain.getNativeControlsEnabled()) { + this.latestInput_.input().checked = true; + } + + // shaka.log is not set if logging isn't enabled. + // I.E. if using the release version of shaka. + if (!shaka['log']) return; + + // Access shaka.log using bracket syntax because shaka.log is not exported. + // Exporting the logging methods proved to be a bad solution, both in terms + // of difficulty and in terms of what changes it would require of the + // architectural design of Shaka Player, so this non-type-safe solution is + // the best remaining way to get the Closure compiler to compile this + // method. + const Level = shaka['log']['Level']; + const setLevel = shaka['log']['setLevel']; + + const logLevels = { + 'info': 'Info', 'debug': 'Debug', 'v': 'Verbose', 'vv': 'Very Verbose'}; + const onChange = (input) => { + switch (input.value) { + case 'info': + setLevel(Level['INFO']); + break; + case 'debug': + setLevel(Level['DEBUG']); + break; + case 'vv': + setLevel(Level['V2']); + break; + case 'v': + setLevel(Level['V1']); + break; + } + shakaDemoMain.remakeHash(); + }; + this.addSelectInput_('Log Level', logLevels, onChange); + const input = this.latestInput_.input(); + switch (shaka['log']['currentLevel']) { + case Level['DEBUG']: input.value = 'debug'; break; + case Level['V2']: input.value = 'vv'; break; + case Level['V1']: input.value = 'v'; break; + default: input.value = 'info'; break; + } + } + + /** + * @param {string} suffix + * @return {string} + * @private + */ + resolveExternLink_(suffix) { + return '../docs/api/shakaExtern.html#' + suffix; + } + + /** + * @param {string} name + * @param {?string} docLink + * @return {!ShakaDemoConfig} + * @private + */ + addSection_(name, docLink) { + const style = name ? + ShakaDemoInputContainer.Style.ACCORDION : + ShakaDemoInputContainer.Style.VERTICAL; + this.sections_.push(new ShakaDemoInputContainer( + this.container_, name, style, docLink)); + + return this; + } + + /** + * @param {string} name + * @param {string} valueName + * @param {string=} tooltipMessage + * @return {!ShakaDemoConfig} + * @private + */ + addBoolInput_(name, valueName, tooltipMessage) { + const onChange = (input) => { + shakaDemoMain.configure(valueName, input.checked); + shakaDemoMain.remakeHash(); + }; + this.addCustomBoolInput_(name, onChange, tooltipMessage); + if (shakaDemoMain.getCurrentConfigValue(valueName)) { + this.latestInput_.input().checked = true; + } + this.addedFields_.add(valueName); + return this; + } + + /** + * @param {string} name + * @param {function(!Element)} onChange + * @param {string=} tooltipMessage + * @return {!ShakaDemoConfig} + * @private + */ + addCustomBoolInput_(name, onChange, tooltipMessage) { + this.createRow_(name, tooltipMessage); + this.latestInput_ = new ShakaDemoBoolInput( + this.getLatestSection_(), name, onChange); + return this; + } + + /** + * @param {string} name + * @param {string} valueName + * @param {string=} tooltipMessage + * @return {!ShakaDemoConfig} + * @private + */ + addTextInput_(name, valueName, tooltipMessage) { + const onChange = (input) => { + shakaDemoMain.configure(valueName, input.value); + shakaDemoMain.remakeHash(); + }; + this.addCustomTextInput_(name, onChange, tooltipMessage); + this.latestInput_.input().value = + shakaDemoMain.getCurrentConfigValue(valueName); + this.addedFields_.add(valueName); + return this; + } + + /** + * @param {string} name + * @param {function(!Element)} onChange + * @param {string=} tooltipMessage + * @return {!ShakaDemoConfig} + * @private + */ + addCustomTextInput_(name, onChange, tooltipMessage) { + this.createRow_(name, tooltipMessage); + this.latestInput_ = new ShakaDemoTextInput( + this.getLatestSection_(), name, onChange); + return this; + } + + /** + * @param {string} name + * @param {string} valueName + * @param {boolean=} canBeDecimal + * @param {boolean=} canBeZero + * @param {boolean=} canBeUnset + * @param {string=} tooltipMessage + * @return {!ShakaDemoConfig} + * @private + */ + addNumberInput_(name, valueName, canBeDecimal = false, canBeZero = true, + canBeUnset = false, tooltipMessage) { + const onChange = (input) => { + shakaDemoMain.resetConfiguration(valueName); + shakaDemoMain.remakeHash(); + if (input.value == 'Infinity') { + shakaDemoMain.configure(valueName, Infinity); + shakaDemoMain.remakeHash(); + return; + } + if (input.value == '' && canBeUnset) { + return; + } + const valueAsNumber = Number(input.value); + if (valueAsNumber == 0 && !canBeZero) { + return; + } + if (!isNaN(valueAsNumber)) { + if (Math.floor(valueAsNumber) != valueAsNumber && !canBeDecimal) { + return; + } + shakaDemoMain.configure(valueName, valueAsNumber); + shakaDemoMain.remakeHash(); + } + }; + this.createRow_(name, tooltipMessage); + this.latestInput_ = new ShakaDemoNumberInput( + this.getLatestSection_(), name, onChange, canBeDecimal, canBeZero, + canBeUnset); + this.latestInput_.input().value = + shakaDemoMain.getCurrentConfigValue(valueName); + if (isNaN(Number(this.latestInput_.input().value)) && canBeUnset) { + this.latestInput_.input().value = ''; + } + this.addedFields_.add(valueName); + return this; + } + + /** + * @param {string} name + * @param {!Array.} values + * @param {function(!Element)} onChange + * @param {string=} tooltipMessage + * @return {!ShakaDemoConfig} + * @private + */ + addDatalistInput_(name, values, onChange, tooltipMessage) { + this.createRow_(name, tooltipMessage); + this.latestInput_ = new ShakaDemoDatalistInput( + this.getLatestSection_(), name, onChange, values); + return this; + } + + /** + * @param {string} name + * @param {!Object.} values + * @param {function(!Element)} onChange + * @param {string=} tooltipMessage + * @return {!ShakaDemoConfig} + * @private + */ + addSelectInput_(name, values, onChange, tooltipMessage) { + this.createRow_(name, tooltipMessage); + this.latestInput_ = new ShakaDemoSelectInput( + this.getLatestSection_(), name, onChange, values); + return this; + } + + /** + * @param {string} name + * @param {string=} tooltipMessage + * @private + */ + createRow_(name, tooltipMessage) { + this.getLatestSection_().addRow(name, tooltipMessage || null); + } + + /** + * Checks for config values that do not have corresponding fields. + * @private + */ + checkSectionCompleteness_() { + const configPrimitives = new Set(['number', 'string', 'boolean']); + + /** + * Recursively checks all of the sections of the config object. + * @param {!Object} section + * @param {string} accumulatedName + */ + const check = (section, accumulatedName) => { + for (const key in section) { + const name = (accumulatedName) ? (accumulatedName + '.' + key) : (key); + const value = section[key]; + if (configPrimitives.has(typeof value)) { + if (!this.addedFields_.has(name)) { + console.warn('WARNING: Does not have config field for ' + name); + } + } else { + // It's a sub-section. + check(value, name); + } + } + }; + check(shakaDemoMain.getConfiguration(), ''); + } + + /** + * Checks for config fields that point to invalid/obsolete config values. + * @private + */ + checkSectionValidity_() { + for (const field of this.addedFields_) { + const value = shakaDemoMain.getCurrentConfigValue(field); + if (value == undefined) { + console.warn('WARNING: Invalid config field ' + field); + } + } + } + + /** + * Gets the latest section. Results in a failed assert if there is no latest + * section. + * @return {!ShakaDemoInputContainer} + * @private + */ + getLatestSection_() { + goog.asserts.assert(this.sections_.length > 0, + 'Must have at least one section.'); + return this.sections_[this.sections_.length - 1]; + } +} + + +document.addEventListener('shaka-main-loaded', ShakaDemoConfig.init); diff --git a/demo/configuration_section.js b/demo/configuration_section.js deleted file mode 100644 index 86f832f97..000000000 --- a/demo/configuration_section.js +++ /dev/null @@ -1,222 +0,0 @@ -/** - * @license - * Copyright 2016 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Shaka Player demo, main section. - * - * @suppress {visibility} to work around compiler errors until we can - * refactor the demo into classes that talk via public method. TODO - */ - - -/** @suppress {duplicate} */ -var shakaDemo = shakaDemo || {}; // eslint-disable-line no-var - - -/** @private */ -shakaDemo.setupConfiguration_ = function() { - document.getElementById('smallGapLimit').addEventListener( - 'input', shakaDemo.onGapInput_); - document.getElementById('jumpLargeGaps').addEventListener( - 'change', shakaDemo.onJumpLargeGapsChange_); - document.getElementById('preferredAudioLanguage').addEventListener( - 'input', shakaDemo.onConfigInput_); - document.getElementById('preferredTextLanguage').addEventListener( - 'input', shakaDemo.onConfigInput_); - document.getElementById('preferredUILanguage').addEventListener( - 'input', shakaDemo.onConfigInput_); - document.getElementById('preferredAudioChannelCount').addEventListener( - 'input', shakaDemo.onConfigInput_); - document.getElementById('showNative').addEventListener( - 'change', shakaDemo.onNativeChange_); - document.getElementById('enableAdaptation').addEventListener( - 'change', shakaDemo.onAdaptationChange_); - document.getElementById('logLevelList').addEventListener( - 'change', shakaDemo.onLogLevelChange_); - document.getElementById('enableLoadOnRefresh').addEventListener( - 'change', shakaDemo.onLoadOnRefreshChange_); - document.getElementById('drmSettingsVideoRobustness').addEventListener( - 'input', shakaDemo.onDrmSettingsChange_); - document.getElementById('drmSettingsAudioRobustness').addEventListener( - 'input', shakaDemo.onDrmSettingsChange_); - document.getElementById('availabilityWindowOverride').addEventListener( - 'input', shakaDemo.onAvailabilityWindowOverrideChange_); - - let robustnessSuggestions = document.getElementById('robustnessSuggestions'); - if (shakaDemo.support_.drm['com.widevine.alpha']) { - let widevineSuggestions = ['SW_SECURE_CRYPTO', 'SW_SECURE_DECODE', - 'HW_SECURE_CRYPTO', 'HW_SECURE_DECODE', 'HW_SECURE_ALL']; - // Add Widevine robustness suggestions if it is supported. - widevineSuggestions.forEach(function(suggestion) { - let option = document.createElement('option'); - option.value = suggestion; - option.textContent = 'Widevine'; - robustnessSuggestions.appendChild(option); - }); - } -}; - - -/** @private */ -shakaDemo.onLoadOnRefreshChange_ = function() { - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onDrmSettingsChange_ = function(event) { - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onAvailabilityWindowOverrideChange_ = function(event) { - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onLogLevelChange_ = function(event) { - // shaka.log is not set if logging isn't enabled. - // I.E. if using the compiled version of shaka. - if (shaka.log) { - let logLevel = event.target[event.target.selectedIndex]; - switch (logLevel.value) { - case 'info': - shaka.log.setLevel(shaka.log.Level.INFO); - break; - case 'debug': - shaka.log.setLevel(shaka.log.Level.DEBUG); - break; - case 'vv': - shaka.log.setLevel(shaka.log.Level.V2); - break; - case 'v': - shaka.log.setLevel(shaka.log.Level.V1); - break; - } - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); - } -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onJumpLargeGapsChange_ = function(event) { - shakaDemo.player_.configure(({ - streaming: {jumpLargeGaps: event.target.checked}, - })); - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onGapInput_ = function(event) { - let smallGapLimit = Number(event.target.value); - let useDefault = isNaN(smallGapLimit) || event.target.value.length == 0; - shakaDemo.player_.configure(({ - streaming: { - smallGapLimit: useDefault ? undefined : smallGapLimit, - }, - })); - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onConfigInput_ = function(event) { - let preferredAudioChannelCount = - Number(document.getElementById('preferredAudioChannelCount').value) || 2; - shakaDemo.player_.configure(/** @type {shaka.extern.PlayerConfiguration} */({ - preferredAudioLanguage: - document.getElementById('preferredAudioLanguage').value, - preferredTextLanguage: - document.getElementById('preferredTextLanguage').value, - preferredAudioChannelCount: preferredAudioChannelCount, - })); - - // TODO: As an optimization, defer this change until the user stops typing. - // Currently, this is triggered on each keystroke. Combine this with - // lazy-loading of localization data, and we get a fetch on every keypress. - const uiLang = document.getElementById('preferredUILanguage').value; - shakaDemo.controls_.getLocalization().changeLocale([uiLang]); - // TODO(#1591): Support multiple language preferences - - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onAdaptationChange_ = function(event) { - // Update adaptation config. - shakaDemo.player_.configure({ - abr: {enabled: event.target.checked}, - }); - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onNativeChange_ = function(event) { - if (event.target.checked) { - shakaDemo.controls_.setEnabledNativeControls(true); - } else { - shakaDemo.controls_.setEnabledShakaControls(true); - } - - // Update text streaming config. When we use native controls, we must always - // stream text. This is because the native controls can't send an event when - // the text display state changes, so we can't use the display state to choose - // when to stream text. - shakaDemo.player_.configure({ - streaming: {alwaysStreamText: event.target.checked}, - }); - - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); -}; diff --git a/demo/custom.js b/demo/custom.js new file mode 100644 index 000000000..04c8212d9 --- /dev/null +++ b/demo/custom.js @@ -0,0 +1,396 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** @type {?ShakaDemoCustom} */ +let shakaDemoCustom; + + +/** + * Shaka Player demo, custom asset page layout. + */ +class ShakaDemoCustom { + /** + * Register the page configuration. + */ + static init() { + const container = shakaDemoMain.addNavButton('custom'); + shakaDemoCustom = new ShakaDemoCustom(container); + } + + /** @param {!Element} container */ + constructor(container) { + this.dialog_ = document.createElement('dialog'); + this.dialog_.classList.add('mdl-dialog'); + container.appendChild(this.dialog_); + if (!this.dialog_.showModal) { + dialogPolyfill.registerDialog(this.dialog_); + } + + this.assets_ = this.loadAssetInfos_(); + + /** @private {!Array.} */ + this.assetCards_ = []; + this.savedList_ = document.createElement('div'); + container.appendChild(this.savedList_); + this.remakeSavedList_(); + + // Add the "new" button, which shows the dialog. + const addButtonContainer = document.createElement('div'); + addButtonContainer.classList.add('add-button-container'); + container.appendChild(addButtonContainer); + // Style it as an MDL Floating Action Button (FAB). + const addButton = this.makeButton_('add', /* isFAB = */ true, () => { + this.showAssetDialog_(ShakaDemoAssetInfo.makeBlankAsset()); + }); + addButtonContainer.appendChild(addButton); + + document.addEventListener('shaka-main-selected-asset-changed', () => { + this.updateSelected_(); + }); + } + + /** + * @param {!ShakaDemoAssetInfo} assetInProgress + * @private + */ + showAssetDialog_(assetInProgress) { + // Remove buttons for any previous assets. + shaka.ui.Utils.removeAllChildren(this.dialog_); + + const inputDiv = document.createElement('div'); + this.dialog_.appendChild(inputDiv); + + const iconDiv = document.createElement('div'); + this.dialog_.appendChild(iconDiv); + + // An array of inputs which have validity checks which we care about. + const inputsToCheck = []; + + const commonDrmSystems = new Set([ + 'com.widevine.alpha', + 'com.microsoft.playready', + 'com.adobe.primetime', + 'org.w3.clearkey', + ]); + + // The license server and drm system fields need to know each others + // contents, and react to each others changes, to work. + // To simplify things, this method picks out the process of setting license + // server URLs; it can be called within both fields. + let licenseServerUrlInput; + let customDrmSystemInput; + const setLicenseServerURLs = () => { + const licenseServerURL = licenseServerUrlInput.value; + const customDRMSystem = customDrmSystemInput.value; + if (licenseServerURL) { + // Make a license server entry for every common DRM plugin. + assetInProgress.licenseServers.clear(); + for (const drmSystem of commonDrmSystems.values()) { + assetInProgress.licenseServers.set(drmSystem, licenseServerURL); + } + if (customDRMSystem) { + // Make a custom entry too. + assetInProgress.licenseServers.set(customDRMSystem, licenseServerURL); + } + } else { + assetInProgress.licenseServers.clear(); + } + }; + + const containerStyle = ShakaDemoInputContainer.Style.VERTICAL; + const container = new ShakaDemoInputContainer( + inputDiv, /* headerText = */ null, containerStyle, + /* docLink = */ null); + + /** + * A utility to simplify the creation of fields on the dialog. + * @param {string} name + * @param {function(!Element, !Element)} setup + * @param {function(!Element)} onChange + */ + const makeField = (name, setup, onChange) => { + container.addRow(null, null); + const input = new ShakaDemoTextInput(container, name, onChange); + input.extra().textContent = name; + setup(input.input(), input.container()); + }; + + // Make the manifest URL field. + const manifestSetup = (input, container) => { + input.value = assetInProgress.manifestUri; + inputsToCheck.push(input); + + // Make an error that shows up if you did not provide an URL. + const error = document.createElement('span'); + error.classList.add('mdl-textfield__error'); + error.textContent = 'Must have a manifest URL.'; + container.appendChild(error); + + // Add a regex that will detect empty strings. + input.required = true; + input.pattern = '^(?!([\r\n\t\f\v ]+)$).*$'; + }; + const manifestOnChange = (input) => { + assetInProgress.manifestUri = input.value; + }; + makeField('Manifest URL', manifestSetup, manifestOnChange); + + // Make the license server URL field. + const licenseSetup = (input, container) => { + licenseServerUrlInput = input; + const drmSystems = assetInProgress.licenseServers.keys(); + // Custom assets have only a single license server URL, no matter how + // many key systems they have. Thus, it's safe to say that the license + // server URL associated with the first key system is the asset's + // over-all license server URL. + const drmSystem = drmSystems.next(); + if (drmSystem && drmSystem.value) { + input.value = assetInProgress.licenseServers.get(drmSystem.value); + } + }; + const licenseOnChange = (input) => { + setLicenseServerURLs(); + }; + makeField('Custom License Server URL', licenseSetup, licenseOnChange); + + // Make the license certificate URL field. + const certSetup = (input, container) => { + if (assetInProgress.certificateUri) { + input.value = assetInProgress.certificateUri; + } + }; + const certOnChange = (input) => { + assetInProgress.certificateUri = input.value; + }; + makeField('Custom License Certificate URL', certSetup, certOnChange); + + // Make the drm system field. + const drmSetup = (input, container) => { + customDrmSystemInput = input; + const drmSystems = assetInProgress.licenseServers.keys(); + for (const drmSystem of drmSystems) { + if (!commonDrmSystems.has(drmSystem)) { + input.value = drmSystem; + break; + } + } + }; + const drmOnChange = (input) => { + setLicenseServerURLs(); + }; + makeField('Custom DRM System', drmSetup, drmOnChange); + + // Make the name field. + const nameSetup = (input, container) => { + input.value = assetInProgress.name; + inputsToCheck.push(input); + + // Make an error that shows up if you have an empty/duplicate name. + const error = document.createElement('span'); + error.classList.add('mdl-textfield__error'); + error.textContent = 'Must be a unique alphanumeric name.'; + container.appendChild(error); + + // Make a regex that will detect duplicates. + input.required = true; + input.pattern = '^(?!( *'; + for (const asset of this.assets_) { + if (asset == assetInProgress) { + // If editing an existing asset, it's okay if the name doesn't change. + continue; + } + input.pattern += '|' + asset.name; + } + input.pattern += ')$)[a-zA-Z0-9 ]*$'; + }; + const nameOnChange = (input) => { + assetInProgress.name = input.value; + }; + makeField('Name', nameSetup, nameOnChange); + + // Make the icon field. + const iconSetup = (input, container) => { + if (assetInProgress.iconUri) { + input.value = assetInProgress.iconUri; + const img = document.createElement('IMG'); + img.src = input.value; + iconDiv.appendChild(img); + } + }; + const iconOnChange = (input) => { + shaka.ui.Utils.removeAllChildren(iconDiv); + assetInProgress.iconUri = input.value; + if (input.value) { + const img = document.createElement('IMG'); + img.src = input.value; + iconDiv.appendChild(img); + } + }; + makeField('Icon URL', iconSetup, iconOnChange); + + // Create the buttons at the bottom of the dialog. + const buttonsDiv = document.createElement('tr'); + inputDiv.appendChild(buttonsDiv); + buttonsDiv.appendChild(this.makeButton_('Save', /* isFAB = */ false, () => { + for (const input of inputsToCheck) { + if (!input.validity.valid) { + return; + } + } + this.assets_.add(assetInProgress); + this.saveAssetInfos_(this.assets_); + this.remakeSavedList_(); + this.dialog_.close(); + })); + buttonsDiv.appendChild(this.makeButton_( + 'Cancel', /* isFAB = */ false, () => { + this.dialog_.close(); + })); + + // Update the componentHandler, to account for the new MDL elements. + componentHandler.upgradeDom(); + + // Show the dialog last, so that it knows where to place it. + this.dialog_.showModal(); + } + + /** + * @return {!Set.} + * @private + */ + loadAssetInfos_() { + const savedString = window.localStorage.getItem(ShakaDemoCustom.saveId_); + if (savedString) { + const assets = JSON.parse(savedString); + return new Set(assets.map((asset) => ShakaDemoAssetInfo.fromJSON(asset))); + } + return new Set(); + } + + /** + * @param {!Set.} assetInfos + * @private + */ + saveAssetInfos_(assetInfos) { + const saveId = ShakaDemoCustom.saveId_; + const assets = Array.from(assetInfos); + window.localStorage.setItem(saveId, JSON.stringify(assets)); + } + + /** + * @param {string} name + * @param {boolean} isFAB Should this button be styled as a Material Design + * Floating Action Button (FAB)? + * @param {function()} callback + * @return {!Element} + * @private + */ + makeButton_(name, isFAB, callback) { + const button = document.createElement('button'); + if (isFAB) { + button.classList.add('mdl-button--fab'); + button.classList.add('mdl-button--colored'); + const icon = document.createElement('i'); + icon.classList.add('material-icons'); + icon.textContent = name; + button.appendChild(icon); + } else { + button.textContent = name; + button.classList.add('mdl-button--raised'); + } + button.addEventListener('click', callback); + button.classList.add('mdl-button'); + button.classList.add('mdl-js-button'); + button.classList.add('mdl-js-ripple-effect'); + return button; + } + + /** + * @param {!ShakaDemoAssetInfo} asset + * @return {!AssetCard} + * @private + */ + createAssetCardFor_(asset) { + const savedList = this.savedList_; + const card = new AssetCard(savedList, asset, /* isFeatured = */ false); + card.addButton('Play', () => { + shakaDemoMain.loadAsset(asset); + this.updateSelected_(); + }); + // TODO: Add offline support. + // TODO: Be sure to un-store if the asset is deleted or edited. + card.addButton('Edit', () => { + this.showAssetDialog_(asset); + }); + card.addButton('Delete', () => { + this.assets_.delete(asset); + this.saveAssetInfos_(this.assets_); + this.remakeSavedList_(); + }); + return card; + } + + /** + * Updates which asset card is selected. + * @private + */ + updateSelected_() { + for (const card of this.assetCards_) { + card.selectByAsset(shakaDemoMain.selectedAsset); + } + } + + /** @private */ + remakeSavedList_() { + shaka.ui.Utils.removeAllChildren(this.savedList_); + + if (this.assets_.size == 0) { + // Add in a message telling you what to do. + const makeMessage = (textClass, text) => { + const textElement = document.createElement('h2'); + textElement.classList.add('mdl-typography--' + textClass); + // TODO: Localize these messages. + textElement.textContent = text; + this.savedList_.appendChild(textElement); + }; + makeMessage('title', + 'Try Shaka Player with your own content!'); + makeMessage('body-2', + 'Press the button below to add a custom asset.'); + makeMessage('body-1', + 'Custom assets will remain even after reloading the page.'); + } else { + // Make asset cards for the assets. + this.assetCards_ = Array.from(this.assets_).map((asset) => { + return this.createAssetCardFor_(asset); + }); + this.updateSelected_(); + } + } +} + + +/** + * The name of the field in window.localStorage that is used to store a user's + * custom assets. + * @const {string} + */ +ShakaDemoCustom.saveId_ = 'shakaPlayerDemoSavedAssets'; + + +document.addEventListener('shaka-main-loaded', ShakaDemoCustom.init); diff --git a/demo/demo.css b/demo/demo.css deleted file mode 100644 index 4fceca486..000000000 --- a/demo/demo.css +++ /dev/null @@ -1,390 +0,0 @@ -/** - * Copyright 2016 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -html { - /* this allows containers inside this to be constrained by percent height */ - height: 100%; -} - -body { - /* This is explicit because Chromecast has a different default (black). */ - background-color: white; - - margin: 0.5em; - padding: 0.5em 1em; - - font-family: Roboto, sans-serif; - font-weight: 300; - word-wrap: break-word; -} - -body.noinput { - /* noinput mode. */ - - /* Add some additional padding so that TV overscan doesn't cut off the version - * number or other information we might need. */ - padding: 2.5em 2em; - - /* Constrain the body to its size minus margin and padding, so that nested - * elements can constrain to the body. */ - width: 100%; - width: calc(100% - 4em); - height: 100%; - height: calc(100% - 5em); -} - -a { - color: #0070b0; - text-decoration: none; -} - -a:hover { - color: #0040b0; - text-decoration: underline; -} - -a.disabled_link { - /* Allow pointer events, so that we can show a tooltip on hover */ - cursor: not-allowed; - color: Gray; -} - -#loadButton, #unloadButton { - background-color: #d04030; - border: none; - border-radius: 2px; - color: white; - font-family: Roboto, sans-serif; - font-size: 0.9em; - font-weight: 600; - margin: 0; - padding: 0.5em 0 0.5em 0; - width: 7.5em; -} - -#loadButton:active, #unloadButton:active { - background-color: #e02020; -} - -#loadButton:hover, #unloadButton:hover { - background-color: #e02020; -} - -button[disabled] { - background-color: #888; - color: #ccc; -} - -details { - margin: 1em 0 1em 0; -} - -details div { - margin: 0 0 1.2em 0; -} - -h1 { - border-bottom: 1px solid #ccc; - font-family: 'Roboto Condensed', sans-serif; - font-size: 2.0em; - font-weight: 600; - margin: 0 0 0.5em 0; - padding: 0 0 0.2em 0; -} - -input[type=number], -input[type=text] { - font-family: Roboto, sans-serif; - font-size: 0.9em; - padding: 1px 2px 3px 6px; - height: 19px; -} - -label, .label { - color: #444; - display: inline-block; - font-family: 'Roboto Condensed'; - font-weight: 400; - width: 11em; -} - -input[type=checkbox] + label { - width: auto; -} - -p { - color: #444; - font-size: 0.9em; - font-weight: 300; - line-height: 1.6em; - margin: 0.5em 0; -} - -p#compiled_links { - border-bottom: 1px solid #eee; - margin: 0 0 1.5em 0; - padding: 0 0 0.5em 0; -} - -p.links { - border-bottom: 1px solid #eee; - border-top: 1px solid #eee; - margin: 1.5em 0 1.5em 0; - padding: 0.5em 0 0.5em 0; -} - -select { - font-family: sans-serif; - height: 2em; - -webkit-appearance: menulist-button; -} - -strong { - font-weight: 600; -} - -summary { - cursor: pointer; - display: block; - font-family: 'Roboto Condensed', sans-serif; - font-size: 1.1em; - outline: none; - margin: 0 0 1em 0; -} - -#assetList, #customAsset div { - margin: 0 0 1.2em 0; - /* Otherwise, this causes horizontal scrolling on narrow screens */ - max-width: 80%; -} - -#container { - margin: 0 auto 0 auto; - max-width: 40em; -} - -#container.noinput { - /* noinput mode: use all available space */ - max-width: 100%; - max-height: 100%; - width: 100%; - height: 100%; - margin: 0; - padding: 0; -} - -#container.noinput .input { - /* noinput mode: hide all input fields */ - display: none; -} - -#container.noinput h1 { - /* noinput mode: restrict h1 to 50px tall, center it */ - height: 50px; - width: 100%; - margin: 0 auto; - padding: 0; - text-align: center; -} - -#container.noinput #videoContainer { - /* noinput mode: use all available space, - but also constrain to the screen size */ - max-width: 100%; - max-height: 100%; - width: 100%; - height: 100%; - height: calc(100% - 50px); - margin: 0; - padding: 0; -} - -body.noinput #logSection #log { - /* noinput mode: if displayed, put the logs higher up on screen, over the top - of the video. */ - position: fixed; - top: 0; - left: 0; - margin: 10px; - width: auto; - height: calc(100% - 30px); -} - -#customAsset { - display: none; -} - -#customAsset input { - /* Otherwise, this becomes a two lines tall on narrow screens */ - height: 1.4em; -} - -#errorDisplay { - background-color: #d84a38; - margin: 0; - padding: 1em; - line-height: 2em; - text-align: center; - width: 100%; - display: none; -} - -#errorDisplayLink { - color: white; -} - -#errorDisplayCloseButton { - background-color: #d84a38; - color: white; - position: relative; - padding: 0 0; - top: 0em; - right: 0.5em; - float: right; - font-weight: bold; - text-decoration: none; - cursor: pointer; -} - -#progressDiv, #offlineNameDiv { - display: none; -} - -#loadButton { - margin: 0 1em 1em 0; -} - -#logSection { - display: none; -} - -#log { - background-color: #f4f4f4; - border: 1px solid #aaa; - color: #000; - font-family: monospace; - height: 200px; - overflow-x: hidden; - overflow-y: scroll; - padding: 5px; -} - -#log div { - border-bottom: 1px solid #ddd; - line-height: 1.4em; - margin: 0; - padding: 0 0.5em; - width: 100%; -} - -#log div span { - padding-right: 0.5em; - white-space: pre-wrap; -} - -#videoContainer { - background-color: transparent; -} - -#video { - /* height and margin needed in fullscreen mode */ - width: 100%; - height: 100%; - margin: auto; -} - -.errorLog { - background-color: #fee; - color: #f00; -} - -.warnLog { - background-color: #ffc; -} - -.infoLog { - background-color: #eff; -} - -.flex { - display: flex; -} - -.flex-grow { - flex-grow: 1; -} - -.for-screen-readers { - /* Hide the content from sighted users, but not screen readers */ - position: absolute; - left: -10000px; - top: auto; - width: 1px; - height: 1px; - overflow: hidden; -} - -@media screen and (max-width: 650px) { - h1 { - font-size: 1.5em; - } - - p { - font-size: 0.8em; - } -} - -@media screen and (max-width: 550px) { - button.appButton { - margin: 0 0.6em 0.8em 0; - padding: 9px 0 10px 0; - } - - button.appButton:active { - background-color: darkRed; - } - - h1 { - font-size: 1.4em; - } - - input[type=number], - input[type=text] { - font-size: 0.8em; - } - - label, .label { - font-weight: 500; - margin: 0 0 0.4em 0; - } - - select { - display: block; - margin: 0 1em 0.3em 0; - } - - summary { - font-weight: 600; - } -} - -@media screen and (max-width: 400px) { - button.appButton { - font-size: 0.8em; - margin: 0 0.4em 1em 0; - padding: 5px 0 7px 0; - } -} diff --git a/demo/demo.less b/demo/demo.less new file mode 100644 index 000000000..8a88e63a0 --- /dev/null +++ b/demo/demo.less @@ -0,0 +1,387 @@ +/** + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@accent-background-color: #ddf; +@error-color: #d84a38; +@drawer-width: 550px; +@footer-link-color: #9e9e9e; /* copied from MDL stylesheet */ +@footer-link-color-disabled: darken(@footer-link-color, 20%); + +/* This is a less mixin only, rather than a CSS class. MDL has an equivalent + * class with the same name, which can be used in the application directly. */ +.hidden() { + display: none; +} + +.borderless { + outline: none; +} + +#contents { + padding: 20px; + text-align: center; +} + +.close-button { + /* Move the button to the top-right. */ + position: absolute; + top: 10px; + right: 10px; + /* Give the button a round background, meant to look like the play button. */ + border-radius: 50%; + width: 32px; + color: #000; + background: rgba(255, 255, 255, 0.85); +} + +html, body { + /* Ensure everything has a consistent font. */ + font-family: "Roboto", "Helvetica", "Arial", sans-serif; +} + +/* Give the FAB a drop shadow, that expands a little bit when moused over. */ +.mdl-button--fab { + filter: drop-shadow(0 2px 5px #333333); + transition: 0.2s ease-in-out; +} +.mdl-button--fab:hover { + filter: drop-shadow(0 2px 8px #333333); +} + +/* Remove vertical padding on MDL text fields, but only while they are in + * the hamburger menu. */ +.hamburger-menu .mdl-textfield { + padding: 0; + // The default width of 300px is a bit too wide for us. + width: 200px; +} +.hamburger-menu .mdl-textfield__label { + top: 4px; +} +.hamburger-menu .mdl-textfield__label:after { + bottom: 0; +} +.hamburger-menu .mdl-layout-title { + /* The line-height style in mdl-layout-title looks weird on narrow displays, + * so remove it in the hamburger menu. */ + line-height: unset; +} +.hamburger-menu .input-container-label { + /* Give the labels for input rows a left margin. This keeps them from directly + * touching the left side of the screen, for improved readability. */ + margin-left: 1em; +} +.hamburger-menu .mdl-button--raised { + /* Left-align the text content of the section header buttons. */ + text-align: left; +} + +/* Styles for error display. */ +#error-display { + background-color: @error-color; + margin: 0; + padding: 1em; + line-height: 2em; + text-align: center; + width: 100%; +} + +#error-display-link { + color: white; +} + +.input-disabled { + pointer-events: none; +} + +#error-display-close-button { + background-color: @error-color; + color: white; + position: relative; + padding: 0 0; + top: 0em; + right: 2em; + float: right; + font-weight: bold; + text-decoration: none; + cursor: pointer; +} + +/* Styles for asset cards. */ +.asset-card { + display: inline-block; + margin: 1em; +} +.asset-card-unsupported { + display: inline-block; + margin: 1em; + background-color: #ddd; +} + +/* Asset icons. */ +.feature-icon { + width: 24px; + height: 24px; + background-size: contain; + background-repeat: no-repeat; + display: inline-block; + /* features */ + &[icon="high_definition"] { + background-image: data-uri('icons/custom_high_definition.svg'); + } + &[icon="ultra_high_definition"] { + background-image: data-uri('icons/custom_ultra_high_definition.svg'); + } + &[icon="subtitles"] { + background-image: data-uri('icons/baseline-subtitles-24px.svg'); + } + &[icon="closed_caption"] { + background-image: data-uri('icons/baseline-closed_caption-24px.svg'); + } + &[icon="live"] { + background-image: data-uri('icons/baseline-live_tv-24px.svg'); + } + &[icon="trick_mode"] { + background-image: data-uri('icons/baseline-fast_forward-24px.svg'); + } + &[icon="surround_sound"] { + background-image: data-uri('icons/baseline-surround_sound-24px.svg'); + } + &[icon="multiple_languages"] { + background-image: data-uri('icons/baseline-language-24px.svg'); + } + &[icon="audio_only"] { + background-image: data-uri('icons/baseline-audiotrack-24px.svg'); + } + /* key systems */ + &[icon="widevine"] { + background-image: data-uri('icons/custom_widevine.svg'); + } + &[icon="clear_key"] { + background-image: data-uri('icons/custom_clear_key.svg'); + } + &[icon="playready"] { + background-image: data-uri('icons/custom_playready.svg'); + } +} + +@media screen and (max-width: 400px) { + /* On screens less than 400px, the 330px-wide cards need to shrink. + * This makes them shrink linearly below that threshold. */ + .asset-card { + width: 82.5%; + } +} + +.asset-card div { + /* Override the default value of "stretch" for mdl cards. */ + justify-content: center; +} + +.asset-card img { + /* Icons are 300px by 210px (10:7 aspect ratio). */ + width: 300px; + /* Constrain to space if necessary. */ + max-width: 100%; + + display: block; + margin-left: auto; + margin-right: auto; +} + +.asset-card-unsupported img { + /* Set opacity to 50%, so the image is greyed out also. */ + opacity: 0.5; +} + +.asset-card.selected { + background-color: @accent-background-color; +} + +/* Override some MDL styles to get the desired look and feel. */ +.app-header { + background-color: white; + color: black; +} + +@media screen and (max-width: 780px) { + /* On smaller screens, the header should expand and the elements in it should + * wrap to remain accessible. */ + #nav-button-container { + height: auto; + flex-wrap: wrap; + } + /* The spacer should be hidden in this mode, so that the version number is no + * longer being forced to the right. */ + .app-header .mdl-layout-spacer { + .hidden(); + } +} + +.significant-right-padding { + padding-right: 8em; +} + +.mdl-dialog { + /* Override MDL dialog width, so that input elements don't overflow. */ + width: 320px; +} + +.mdl-dialog img { + width: 300px; + max-width: 100%; +} + +.app-header .mdl-layout__drawer-button { + color: black; +} + +.app-header .logo { + max-height: 80%; + + /* Match the padding of the buttons next to the logo. */ + padding: 0 16px; +} + +footer li { + list-style: square outside; +} + +footer a { + color: @footer-link-color; +} + +footer a[disabled] { + color: @footer-link-color-disabled; + cursor: not-allowed; +} + +/* Style the container that contains the "add custom assets" button. */ +.add-button-container { + text-align: right; + margin: 1em; +} + +.disabled-by-fail { + pointer-events: none; + user-select: none; +} + +/* Style the hamburger menu (mdl drawer). */ +.hamburger-menu { + /* To properly change the width of an MDL drawer, you also have to change the + * transform applied to hide it. */ + width: @drawer-width; + transform: translateX(-@drawer-width); + + /* If the main app page is scrollable, we don't want see "under" the drawer. + * Making the position fixed means it won't move, no matter what. + * Within the drawer itself, the drawer content can still be scrolled. */ + position: fixed; + + /* Constrain to the window if necessary, so that it doesn't overflow small + * mobile screens. */ + max-width: 100%; +} + +/* When the drawer is open, MDL sets overflow: hidden on the main content + * in order to hide the scroll bar. + * + * This also causes most of the elements on screen to jump to the right, + * though, which is very visually distracting. This overrides that style, to + * prevent that behavior. + * + * The class name mdl-layout__content is repeated twice to make the selector + * more specific than what MDL is using. */ +.mdl-layout__drawer.is-visible ~ .mdl-layout__content.mdl-layout__content { + overflow: auto; +} + +/* The title contains the close button, so use flex to center it. + * Also use right-padding to keep it off the right edge. */ +#hamburger-menu-title { + display: flex; + align-items: center; + padding-right: 16px; +} + +.mdl-layout__obfuscator.is-visible { + /* If the main app page is scrollable, we don't want see "under" the layout + * obfuscator (which grays out the app while the drawer is open). + * Making the position fixed means it won't move, no matter what. */ + position: fixed; +} + +/* Control the size of the video. */ +#video-bar { + /* The video bar fills the horizontal space, but its height depends on the + * contents. */ + width: 100%; + /* Add a little bit of padding on top, to make the video not look cropped. */ + padding-top: 1em; +} + +#video-container { + /* Fixed width, but height will expand based on video aspect ratio. + * Does not affect fullscreen size. */ + width: 640px; + margin: auto; + + /* Constrain to the window if necessary, so that it doesn't overflow small + * mobile screens. */ + max-width: 100%; +} + +#video { + /* Fill whatever space we're given, whether fullscreen or not. */ + width: 100%; + height: 100%; + margin: auto; +} + +/* Style the intermediate tooltip attach points, required for tooltips to be + * added to disabled buttons. */ +.tooltip-attach-point { + display: inline-block; +} + +/* Style the input containers. */ +.input-container-row { + display: inline-block; +} +.input-container-style-flex { + display: flex; + flex-wrap: wrap; +} +.input-container-style-vertical { + text-align: left; +} +.input-container-style-accordion { + text-align: left; + opacity: 0; + max-height: 0px; + transition: 0.3s ease-in-out; +} +.input-container-style-accordion.show { + opacity: 1; + /* If the max-height is too high (e.g. set to 100%), the "sliding out" + * animation is too fast to make out with the eye. + * So give it a fixed maximum instead. */ + max-height: 1000px; +} +.input-container-label { + padding-right: 1em; +} diff --git a/demo/common/demo_utils.js b/demo/demo_utils.js similarity index 56% rename from demo/common/demo_utils.js rename to demo/demo_utils.js index 720359e26..5c946fb73 100644 --- a/demo/common/demo_utils.js +++ b/demo/demo_utils.js @@ -19,106 +19,6 @@ /** @namespace */ let ShakaDemoUtils = {}; - -/** - * @param {shakaAssets.AssetInfo} asset - * @param {shaka.Player} player - */ -ShakaDemoUtils.setupAssetMetadata = function(asset, player) { - let config = /** @type {shaka.extern.PlayerConfiguration} */( - {drm: {}, manifest: {dash: {}}}); - - // Add config from this asset. - if (asset.licenseServers) { - config.drm.servers = asset.licenseServers; - } - if (asset.drmCallback) { - config.manifest.dash.customScheme = asset.drmCallback; - } - if (asset.clearKeys) { - config.drm.clearKeys = asset.clearKeys; - } - player.configure(config); - - // Configure network filters. - let networkingEngine = player.getNetworkingEngine(); - networkingEngine.clearAllRequestFilters(); - networkingEngine.clearAllResponseFilters(); - - if (asset.licenseRequestHeaders) { - let filter = ShakaDemoUtils.addLicenseRequestHeaders_.bind( - null, asset.licenseRequestHeaders); - networkingEngine.registerRequestFilter(filter); - } - - if (asset.requestFilter) { - networkingEngine.registerRequestFilter(asset.requestFilter); - } - if (asset.responseFilter) { - networkingEngine.registerResponseFilter(asset.responseFilter); - } - if (asset.extraConfig) { - player.configure( - /** @type {shaka.extern.PlayerConfiguration} */ (asset.extraConfig)); - } -}; - - -/** - * @param {!Object.} headers - * @param {shaka.net.NetworkingEngine.RequestType} requestType - * @param {shaka.extern.Request} request - * @private - */ -ShakaDemoUtils.addLicenseRequestHeaders_ = - function(headers, requestType, request) { - if (requestType != shaka.net.NetworkingEngine.RequestType.LICENSE) return; - - // Add these to the existing headers. Do not clobber them! - // For PlayReady, there will already be headers in the request. - for (let k in headers) { - request.headers[k] = headers[k]; - } -}; - - -/** - * Creates a number of asset buttons, with selection functionality. - * Clicking one of these elements will add the "selected" tag to it, and remove - * the "selected" tag from the previously selected element. - * @param {!Element} parentDiv The div to place the buttons in. - * @param {!Array.} assets The assets that should be - * given buttons. - * @param {?shakaAssets.AssetInfo} selectedAsset An asset that should start out - * selected. - * @param {function(!Element, shakaAssets.AssetInfo)} layout A function that is - * called to lay out the contents of a button. - * @param {function(shakaAssets.AssetInfo)} onclick A function that is called - * when a button is clicked. This is after giving the button the "selected" - * tag. - */ -ShakaDemoUtils.createAssetButtons = function( - parentDiv, assets, selectedAsset, layout, onclick) { - let assetButtons = []; - for (let asset of assets) { - let button = document.createElement('div'); - layout(button, asset); - button.onclick = () => { - onclick(asset); - for (let button of assetButtons) { - button.removeAttribute('selected'); - } - button.setAttribute('selected', ''); - }; - parentDiv.appendChild(button); - assetButtons.push(button); - - if (asset == selectedAsset) { - button.setAttribute('selected', ''); - } - } -}; - /** * Goes through the various values in shaka.extern.PlayerConfiguration, and * calls the given callback on them so that they can be stored to or read from @@ -151,6 +51,8 @@ ShakaDemoUtils.runThroughHashParams = (callback, config) => { // Override config values that are handled manually. overridden.push('abr.enabled'); overridden.push('streaming.jumpLargeGaps'); + overridden.push('drm.advanced'); + overridden.push('drm.servers'); // Determine which config values should be given full namespace names. // This is to remove ambiguity in situations where there are two objects in diff --git a/demo/front.js b/demo/front.js new file mode 100644 index 000000000..1768bc7e4 --- /dev/null +++ b/demo/front.js @@ -0,0 +1,137 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** @type {?ShakaDemoFront} */ +let shakaDemoFront; + + +/** + * Shaka Player demo, front page layout. + */ +class ShakaDemoFront { + /** + * Register the page configuration. + */ + static init() { + const container = shakaDemoMain.addNavButton('front'); + shakaDemoFront = new ShakaDemoFront(container); + } + + /** @param {!Element} container */ + constructor(container) { + /** @private {!Array.} */ + this.assetCards_ = []; + + /** @private {!Element} */ + this.messageDiv_ = document.createElement('div'); + + /** @private {!Element} */ + this.assetCardDiv_ = document.createElement('div'); + + container.appendChild(this.messageDiv_); + this.makeMessage_(); + + container.appendChild(this.assetCardDiv_); + this.remakeAssetCards_(); + + document.addEventListener('shaka-main-selected-asset-changed', () => { + this.updateSelected_(); + }); + document.addEventListener('shaka-main-offline-progress', () => { + this.updateOfflineProgress_(); + }); + document.addEventListener('shaka-main-offline-changed', () => { + this.remakeAssetCards_(); + }); + } + + /** @private */ + makeMessage_() { + // Add in a message telling you what to do. + const makeMessage = (textClass, text) => { + const textElement = document.createElement('h2'); + textElement.classList.add('mdl-typography--' + textClass); + // TODO: Localize these messages. + textElement.textContent = text; + this.messageDiv_.appendChild(textElement); + }; + makeMessage('body-2', + 'This is a demo of Google\'s Shaka Player, a JavaScript ' + + 'library for adaptive video streaming.'); + makeMessage('body-1', + 'Choose a video to playback; more assets are available via ' + + 'the search tab.'); + } + + /** @private */ + remakeAssetCards_() { + shaka.ui.Utils.removeAllChildren(this.assetCardDiv_); + + const assets = shakaAssets.testAssets.filter((asset) => { + return asset.isFeatured && !asset.disabled; + }); + this.assetCards_ = assets.map((asset) => { + return this.createAssetCardFor_(asset, this.assetCardDiv_); + }); + } + + /** + * @param {!ShakaDemoAssetInfo} asset + * @param {!Element} container + * @return {!AssetCard} + * @private + */ + createAssetCardFor_(asset, container) { + const card = new AssetCard(container, asset, /* isFeatured = */ true); + const unsupportedReason = shakaDemoMain.getAssetUnsupportedReason( + asset, /* needOffline= */ false); + if (unsupportedReason) { + card.markAsUnsupported(unsupportedReason); + } else { + card.addButton('Play', () => { + shakaDemoMain.loadAsset(asset); + this.updateSelected_(); + }); + card.addStoreButton(); + } + return card; + } + + /** + * Updates progress bars on asset cards. + * @private + */ + updateOfflineProgress_() { + for (const card of this.assetCards_) { + card.updateProgress(); + } + } + + /** + * Updates which asset card is selected. + * @private + */ + updateSelected_() { + for (const card of this.assetCards_) { + card.selectByAsset(shakaDemoMain.selectedAsset); + } + } +} + + +document.addEventListener('shaka-main-loaded', ShakaDemoFront.init); diff --git a/demo/icons/README.md b/demo/icons/README.md new file mode 100644 index 000000000..bd874147e --- /dev/null +++ b/demo/icons/README.md @@ -0,0 +1,4 @@ +All of the icon files prefixed with "baseline_" are assets from the MDL icons. +All of the icon files prefixed with "custom_" are new assets we made. + +All custom icons were made using the font "Fishmonger Cb Plain" diff --git a/demo/icons/baseline-audiotrack-24px.svg b/demo/icons/baseline-audiotrack-24px.svg new file mode 100644 index 000000000..ccd1f6dc9 --- /dev/null +++ b/demo/icons/baseline-audiotrack-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/baseline-closed_caption-24px.svg b/demo/icons/baseline-closed_caption-24px.svg new file mode 100644 index 000000000..f619b9bf5 --- /dev/null +++ b/demo/icons/baseline-closed_caption-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/baseline-fast_forward-24px.svg b/demo/icons/baseline-fast_forward-24px.svg new file mode 100644 index 000000000..fb10d8138 --- /dev/null +++ b/demo/icons/baseline-fast_forward-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/baseline-language-24px.svg b/demo/icons/baseline-language-24px.svg new file mode 100644 index 000000000..f5076217f --- /dev/null +++ b/demo/icons/baseline-language-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/baseline-live_tv-24px.svg b/demo/icons/baseline-live_tv-24px.svg new file mode 100644 index 000000000..19090b97d --- /dev/null +++ b/demo/icons/baseline-live_tv-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/baseline-subtitles-24px.svg b/demo/icons/baseline-subtitles-24px.svg new file mode 100644 index 000000000..cffd520e8 --- /dev/null +++ b/demo/icons/baseline-subtitles-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/baseline-surround_sound-24px.svg b/demo/icons/baseline-surround_sound-24px.svg new file mode 100644 index 000000000..d029cf9aa --- /dev/null +++ b/demo/icons/baseline-surround_sound-24px.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/custom_box_with_text.svg b/demo/icons/custom_box_with_text.svg new file mode 100644 index 000000000..b2ca849e9 --- /dev/null +++ b/demo/icons/custom_box_with_text.svg @@ -0,0 +1 @@ +HD \ No newline at end of file diff --git a/demo/icons/custom_clear_key.svg b/demo/icons/custom_clear_key.svg new file mode 100644 index 000000000..39a9d2cc6 --- /dev/null +++ b/demo/icons/custom_clear_key.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/custom_high_definition.svg b/demo/icons/custom_high_definition.svg new file mode 100644 index 000000000..08a007636 --- /dev/null +++ b/demo/icons/custom_high_definition.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/custom_playready.svg b/demo/icons/custom_playready.svg new file mode 100644 index 000000000..ce8c6bbdf --- /dev/null +++ b/demo/icons/custom_playready.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/custom_ultra_high_definition.svg b/demo/icons/custom_ultra_high_definition.svg new file mode 100644 index 000000000..7dffebe5c --- /dev/null +++ b/demo/icons/custom_ultra_high_definition.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/icons/custom_widevine.svg b/demo/icons/custom_widevine.svg new file mode 100644 index 000000000..7d42bbdb0 --- /dev/null +++ b/demo/icons/custom_widevine.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/index.html b/demo/index.html index 2dfb10d66..d4110c431 100644 --- a/demo/index.html +++ b/demo/index.html @@ -22,20 +22,25 @@ - - Shaka Player Demo - - - + + + + + + + + + + + -
-

Shaka Player

- -
- -

This is a demo of Google's Shaka Player, a JavaScript library for - adaptive video streaming.

- -

Choose an asset and tap Load. - (On Android, you may also need to press the play button on the - video.)

- - - -
- - +
+
+ +
+
+
+ Shaka Player Demo Config +
+
-
-
- - -
-
- - -
-
- - + +
+ +
+ +
+
+ - -
-
x
- -
- -
- -
- -
- Logs -
-
- -
- Configuration -
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
- -
- -
- Info -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- Active resolution: - -
- -
- Buffered: - -
-
- -
- Offline -
- - -
-
- Progress: - 0% -
-
- - -
-
+ + + +
+ +
diff --git a/demo/info_section.js b/demo/info_section.js deleted file mode 100644 index 640ee0858..000000000 --- a/demo/info_section.js +++ /dev/null @@ -1,356 +0,0 @@ -/** - * @license - * Copyright 2016 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Shaka Player demo, main section. - * - * @suppress {visibility} to work around compiler errors until we can - * refactor the demo into classes that talk via public method. TODO - */ - - -/** @suppress {duplicate} */ -var shakaDemo = shakaDemo || {}; // eslint-disable-line no-var - - -/** @private */ -shakaDemo.setupInfo_ = function() { - window.setInterval(shakaDemo.updateDebugInfo_, 125); - shakaDemo.player_.addEventListener( - 'trackschanged', shakaDemo.onTracksChanged_); - shakaDemo.player_.addEventListener( - 'adaptation', shakaDemo.onAdaptation_); - document.getElementById('variantTracks').addEventListener( - 'change', shakaDemo.onTrackSelected_); - document.getElementById('textTracks').addEventListener( - 'change', shakaDemo.onTrackSelected_); - document.getElementById('audioLanguages').addEventListener( - 'change', shakaDemo.onAudioLanguageSelected_); - document.getElementById('textLanguages').addEventListener( - 'change', shakaDemo.onTextLanguageSelected_); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onTracksChanged_ = function(event) { - // Update language options first and then populate new tracks with - // respect to the chosen languages. - shakaDemo.updateLanguages_(); - shakaDemo.updateVariantTracks_(); - shakaDemo.updateTextTracks_(); -}; - - -/** - * @private - */ -shakaDemo.updateVariantTracks_ = function() { - let trackList = document.getElementById('variantTracks'); - let langList = document.getElementById('audioLanguages'); - let languageAndRole = langList.selectedIndex >= 0 ? - langList.options[langList.selectedIndex].value : - ''; - - let tracks = shakaDemo.player_.getVariantTracks(); - - tracks.sort(function(t1, t2) { - // Sort by bandwidth. - return t1.bandwidth - t2.bandwidth; - }); - - shakaDemo.updateTrackOptions_(trackList, tracks, languageAndRole); -}; - - -/** - * @private - */ -shakaDemo.updateTextTracks_ = function() { - let trackList = document.getElementById('textTracks'); - - let langList = document.getElementById('textLanguages'); - let languageAndRole = langList.selectedIndex >= 0 ? - langList.options[langList.selectedIndex].value : - ''; - - let tracks = shakaDemo.player_.getTextTracks(); - - shakaDemo.updateTrackOptions_(trackList, tracks, languageAndRole); -}; - - -/** - * @param {Element} list - * @param {!Array.} tracks - * @param {string} languageAndRole - * @private - */ -shakaDemo.updateTrackOptions_ = function(list, tracks, languageAndRole) { - let formatters = { - variant: function(track) { - let trackInfo = ''; - if (track.language) trackInfo += 'language: ' + track.language + ', '; - if (track.label) trackInfo += 'label: ' + track.label + ', '; - if (track.roles.length) { - trackInfo += 'roles: [' + track.roles.join() + '], '; - } - if (track.width && track.height) { - trackInfo += track.width + 'x' + track.height + ', '; - } - if (track.channelsCount) { - trackInfo += 'channels: ' + track.channelsCount + ', '; - } - trackInfo += track.bandwidth + ' bits/s'; - return trackInfo; - }, - text: function(track) { - let trackInfo = 'language: ' + track.language + ', '; - if (track.label) trackInfo += 'label: ' + track.label + ', '; - if (track.roles.length) { - trackInfo += 'roles: [' + track.roles.join() + '], '; - } - trackInfo += 'kind: ' + track.kind; - return trackInfo; - }, - }; - // Remove old tracks. - while (list.firstChild) { - list.removeChild(list.firstChild); - } - - // Split language and role. - let res = languageAndRole.split(':'); - let language = res[0]; - let role = res[1] || ''; - - tracks = tracks.filter(function(track) { - let langMatch = track.language == language; - let roleMatch = role == '' || track.roles.includes(role); - return langMatch && roleMatch; - }); - - tracks.forEach(function(track) { - let option = document.createElement('option'); - option.textContent = formatters[track.type](track); - option.track = track; - option.value = track.id; - option.selected = track.active; - list.appendChild(option); - }); -}; - - -/** - * @private - */ -shakaDemo.updateLanguages_ = function() { - shakaDemo.updateTextLanguages_(); - shakaDemo.updateAudioLanguages_(); -}; - - -/** - * Updates options for text language selection. - * @private - */ -shakaDemo.updateTextLanguages_ = function() { - let player = shakaDemo.player_; - let list = document.getElementById('textLanguages'); - let languagesAndRoles = player.getTextLanguagesAndRoles(); - let tracks = player.getTextTracks(); - - shakaDemo.updateLanguageOptions_(list, languagesAndRoles, tracks); -}; - - -/** - * Updates options for audio language selection. - * @private - */ -shakaDemo.updateAudioLanguages_ = function() { - let player = shakaDemo.player_; - let list = document.getElementById('audioLanguages'); - let languagesAndRoles = player.getAudioLanguagesAndRoles(); - let tracks = player.getVariantTracks(); - - shakaDemo.updateLanguageOptions_(list, languagesAndRoles, tracks); -}; - - -/** - * @param {Element} list - * @param {!Array.<{language: string, role: string}>} languagesAndRoles - * @param {!Array.} tracks - * @private - */ -shakaDemo.updateLanguageOptions_ = - function(list, languagesAndRoles, tracks) { - // Remove old options - while (list.firstChild) { - list.removeChild(list.firstChild); - } - - // Using array.filter(f)[0] as an alternative to array.find(f) which is - // not supported in IE11. - let activeTracks = tracks.filter(function(track) { - return track.active == true; - }); - let selectedTrack = activeTracks[0]; - - // Populate list with new options. - languagesAndRoles.forEach(function(langAndRole) { - let language = langAndRole.language; - let role = langAndRole.role; - - let label = language; - if (role) { - label += ' (role: ' + role + ')'; - } - - let option = document.createElement('option'); - option.textContent = label; - option.value = language + ':' + role; - let isSelected = false; - - if (selectedTrack && selectedTrack.language == language) { - if (selectedTrack.roles.length) { - selectedTrack.roles.forEach(function(selectedRole) { - if (selectedRole == role) { - isSelected = true; - } - }); - } else { - isSelected = true; - } - } - - option.selected = isSelected; - list.appendChild(option); - }); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onAdaptation_ = function(event) { - let list = document.getElementById('variantTracks'); - - // Find the row for the active track and select it. - let tracks = shakaDemo.player_.getVariantTracks(); - - tracks.forEach(function(track) { - if (!track.active) return; - - for (let i = 0; i < list.options.length; ++i) { - let option = list.options[i]; - if (option.value == track.id) { - option.selected = true; - break; - } - } - }); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onTrackSelected_ = function(event) { - let list = event.target; - let option = list.options[list.selectedIndex]; - let track = option.track; - let player = shakaDemo.player_; - - if (list.id == 'variantTracks') { - // Disable abr manager before changing tracks. - let config = {abr: {enabled: false}}; - player.configure(config); - - player.selectVariantTrack(track, /* clearBuffer */ true); - } else { - player.selectTextTrack(track); - } - - // Adaptation might have been changed by calling selectTrack(). - // Update the adaptation checkbox. - let enableAdaptation = player.getConfiguration().abr.enabled; - document.getElementById('enableAdaptation').checked = enableAdaptation; -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onAudioLanguageSelected_ = function(event) { - let list = event.target; - let option = list.options[list.selectedIndex].value; - let player = shakaDemo.player_; - let res = option.split(':'); - let language = res[0]; - let role = res[1] || ''; - player.selectAudioLanguage(language, role); - shakaDemo.updateVariantTracks_(); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onTextLanguageSelected_ = function(event) { - let list = event.target; - let option = list.options[list.selectedIndex].value; - let player = shakaDemo.player_; - let res = option.split(':'); - let language = res[0]; - let role = res[1] || ''; - - player.selectTextLanguage(language, role); - shakaDemo.updateTextTracks_(); -}; - - -/** @private */ -shakaDemo.updateDebugInfo_ = function() { - let video = shakaDemo.video_; - - document.getElementById('videoResDebug').textContent = - video.videoWidth + ' x ' + video.videoHeight; - - let behind = 0; - let ahead = 0; - - let currentTime = video.currentTime; - let buffered = video.buffered; - for (let i = 0; i < buffered.length; ++i) { - if (buffered.start(i) <= currentTime && buffered.end(i) >= currentTime) { - ahead = buffered.end(i) - currentTime; - behind = currentTime - buffered.start(i); - break; - } - } - - document.getElementById('bufferedDebug').textContent = - '- ' + behind.toFixed(0) + 's / + ' + ahead.toFixed(0) + 's'; -}; diff --git a/demo/input.js b/demo/input.js new file mode 100644 index 000000000..11aff232a --- /dev/null +++ b/demo/input.js @@ -0,0 +1,238 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Creates and contains the MDL elements of a type of input. + */ +class ShakaDemoInput { + /** + * @param {!ShakaDemoInputContainer} parentContainer + * @param {string} inputType The element type for the input object. + * @param {string} containerType The element type for the container containing + * the input object. + * @param {string} extraType The element type for the "sibling element" to the + * input object. If null, it adds no such element. + * @param {function(!Element)} onChange + */ + constructor(parentContainer, inputType, containerType, extraType, onChange) { + /** @private {!Element} */ + this.container_ = document.createElement(containerType); + parentContainer.latestElementContainer.appendChild(this.container_); + + /** @private {!Element} */ + this.input_ = document.createElement(inputType); + this.input_.onchange = () => { onChange(this.input_); }; + this.input_.id = ShakaDemoInput.generateNewId_('input'); + this.container_.appendChild(this.input_); + + if (parentContainer.latestTooltip) { + // Since the row isn't focusable, add the tooltip information into the + // accessibility data of the input, so it can be accessed by users using + // screen readers. + const extraInfo = document.createElement('span'); + extraInfo.textContent = parentContainer.latestTooltip; + extraInfo.classList.add('hidden'); + extraInfo.id = ShakaDemoInput.generateNewId_('extra-info'); + this.container_.appendChild(extraInfo); + this.input_.setAttribute('aria-describedby', extraInfo.id); + } + + /** + * Most MDL inputs require some sort of "sibling element" that exists at + * the same level as the input itself. These other elements are used to + * create various visual effects, such as the ripple effect. + * @private {?Element} + */ + this.extra_ = null; + if (extraType) { + this.extra_ = document.createElement(extraType); + this.container_.appendChild(this.extra_); + } + } + + /** @return {!Element} */ + input() { + return this.input_; + } + + /** @return {!Element} */ + container() { + return this.container_; + } + + /** @return {?Element} */ + extra() { + return this.extra_; + } + + /** + * @param {string} prefix + * @return {string} + * @private + */ + static generateNewId_(prefix) { + const idNumber = ShakaDemoInput.lastId_; + ShakaDemoInput.lastId_ += 1; + return prefix + '-labeled-' + idNumber; + } +} + + +/** @private {number} */ +ShakaDemoInput.lastId_ = 0; + + +/** + * Creates and contains the MDL elements of a select input. + */ +class ShakaDemoSelectInput extends ShakaDemoInput { + /** + * @param {!ShakaDemoInputContainer} parentContainer + * @param {string} name + * @param {function(!Element)} onChange + * @param {!Object.} values + */ + constructor(parentContainer, name, onChange, values) { + super(parentContainer, 'select', 'div', 'label', onChange); + this.container_.classList.add('mdl-textfield'); + this.container_.classList.add('mdl-js-textfield'); + this.container_.classList.add('mdl-textfield--floating-label'); + this.input_.classList.add('mdl-textfield__input'); + this.extra_.classList.add('mdl-textfield__label'); + this.extra_.setAttribute('for', this.input_.id); + for (let value of Object.keys(values)) { + const option = document.createElement('option'); + option.textContent = values[value]; + option.value = value; + this.input_.appendChild(option); + } + } +} + + +/** + * Creates and contains the MDL elements of a bool input. + */ +class ShakaDemoBoolInput extends ShakaDemoInput { + /** + * @param {!ShakaDemoInputContainer} parentContainer + * @param {string} name + * @param {function(!Element)} onChange + */ + constructor(parentContainer, name, onChange) { + super(parentContainer, 'input', 'label', 'span', onChange); + this.input_.type = 'checkbox'; + this.container_.classList.add('mdl-switch'); + this.container_.classList.add('mdl-js-switch'); + this.container_.classList.add('mdl-js-ripple-effect'); + this.container_.setAttribute('for', this.input_.id); + this.input_.classList.add('mdl-switch__input'); + this.extra_.classList.add('mdl-switch__label'); + } +} + + +/** + * Creates and contains the MDL elements of a text input. + */ +class ShakaDemoTextInput extends ShakaDemoInput { + /** + * @param {!ShakaDemoInputContainer} parentContainer + * @param {string} name + * @param {function(!Element)} onChange + */ + constructor(parentContainer, name, onChange) { + super(parentContainer, 'input', 'div', 'label', onChange); + this.container_.classList.add('mdl-textfield'); + this.container_.classList.add('mdl-js-textfield'); + this.container_.classList.add('mdl-textfield--floating-label'); + this.input_.classList.add('mdl-textfield__input'); + this.extra_.classList.add('mdl-textfield__label'); + this.extra_.setAttribute('for', this.input_.id); + } +} + + +/** + * Creates and contains the MDL elements of a datalist input. + */ +class ShakaDemoDatalistInput extends ShakaDemoTextInput { + /** + * @param {!ShakaDemoInputContainer} parentContainer + * @param {string} name + * @param {function(!Element)} onChange + * @param {!Array.} values + */ + constructor(parentContainer, name, onChange, values) { + super(parentContainer, name, onChange); + // This element is not literally a datalist, as those are not supported on + // all platforms (and they also have no MDL style support). + // Instead, this is using the third-party "awesomplete" module, which acts + // as a text field with autocomplete selection. + const awesomplete = new Awesomplete(this.input_); + awesomplete.list = values.slice(); // Make a local copy of the values list. + awesomplete.minChars = 0; + this.input_.addEventListener('focus', () => { + // By default, awesomplete does not show suggestions on focusing on the + // input, only on typing something. + // This manually updates the suggestions, so that they will show up. + awesomplete.evaluate(); + }); + } +} + + +/** + * Creates and contains the MDL elements of a number input. + */ +class ShakaDemoNumberInput extends ShakaDemoTextInput { + /** + * @param {!ShakaDemoInputContainer} parentContainer + * @param {string} name + * @param {function(!Element)} onChange + * @param {boolean} canBeDecimal + * @param {boolean} canBeZero + * @param {boolean} canBeUnset + */ + constructor( + parentContainer, name, onChange, canBeDecimal, canBeZero, canBeUnset) { + super(parentContainer, name, onChange); + const error = document.createElement('span'); + error.classList.add('mdl-textfield__error'); + this.container_.appendChild(error); + + error.textContent = 'Must be a positive'; + this.input_.pattern = '(Infinity|'; + if (canBeZero) { + this.input_.pattern += '[0-9]*'; + } else { + this.input_.pattern += '[0-9]*[1-9][0-9]*'; + error.textContent += ', nonzero'; + } + if (canBeDecimal) { + // TODO: Handle commas as decimal delimeters, for appropriate regions? + this.input_.pattern += '(.[0-9]+)?'; + error.textContent += ' number.'; + } else { + error.textContent += ' integer.'; + } + this.input_.pattern += ')'; + if (canBeUnset) { + this.input_.pattern += '?'; + } + } +} diff --git a/demo/input_container.js b/demo/input_container.js new file mode 100644 index 000000000..0aa220a9c --- /dev/null +++ b/demo/input_container.js @@ -0,0 +1,198 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Creates elements for containing inputs. It represents a single "section" of + * input. + * It contains a number of "rows", each of which contains an optional label and + * an input. + * It also has an optional header, which can contain style-dependent + * functionality. + */ +class ShakaDemoInputContainer { + /** + * @param {!Element} parentDiv + * @param {?string} headerText The text to be displayed by the header. If + * null, there will be no header. + * @param {!ShakaDemoInputContainer.Style} style + * @param {?string} docLink + */ + constructor(parentDiv, headerText, style, docLink) { + /** @private {!ShakaDemoInputContainer.Style} */ + this.style_ = style; + + /** @private {?Element} */ + this.header_; + + /** @private {!Element} */ + this.table_ = document.createElement('div'); + + /** @private {?Element} */ + this.latestRow_; + + /** @type {?Element} */ + this.latestElementContainer; + + /** @type {?string} */ + this.latestTooltip; + + /** @private {number} */ + this.numRows_ = 0; + + if (headerText) { + this.createHeader_(parentDiv, headerText); + } + this.table_.classList.add(style); + if (style == ShakaDemoInputContainer.Style.ACCORDION) { + this.table_.classList.add('hidden'); + } + parentDiv.appendChild(this.table_); + if (docLink) { + this.addDocLink_(this.table_, docLink); + } + } + + /** + * @return {boolean} true if this is an open accordion menu, false otherwise + */ + getIsOpen() { + if (this.style_ == ShakaDemoInputContainer.Style.ACCORDION) { + return this.table_.classList.contains('show'); + } + return false; + } + + /** If this is an accordion menu, open it. */ + open() { + if (!this.style_ == ShakaDemoInputContainer.Style.ACCORDION) { + return; + } + this.table_.classList.remove('hidden'); + setTimeout(() => { + this.table_.classList.add('show'); + }, /* milliseconds= */ 20); + this.header_.classList.add('mdl-button--colored'); + } + + /** If this is an accordion menu, close it. */ + close() { + if (this.style_ != ShakaDemoInputContainer.Style.ACCORDION) { + return; + } + this.table_.classList.remove('show'); + this.table_.addEventListener('transitionend', (e) => { + this.table_.classList.add('hidden'); + }, {once: true}); + this.header_.classList.remove('mdl-button--colored'); + } + + /** + * @param {!Element} parentDiv + * @param {string} headerText + * @private + */ + createHeader_(parentDiv, headerText) { + if (this.style_ == ShakaDemoInputContainer.Style.ACCORDION) { + this.header_ = document.createElement('button'); + this.header_.classList.add('mdl-button--raised'); + this.header_.classList.add('mdl-button'); + this.header_.classList.add('mdl-js-button'); + this.header_.classList.add('mdl-js-ripple-effect'); + this.header_.addEventListener('click', () => { + // Show/hide the table. + if (this.getIsOpen()) { + this.close(); + } else { + this.open(); + } + }); + } else { + this.header_ = document.createElement('h3'); + } + this.header_.textContent = headerText; + parentDiv.appendChild(this.header_); + } + + /** + * Creates a link that links to a section within the Shaka Player docs. + * @param {!Element} parentDiv + * @param {string} docLink + * @private + */ + addDocLink_(parentDiv, docLink) { + const link = document.createElement('a'); + link.href = docLink; + link.target = '_blank'; + link.classList.add('mdl-button'); + link.classList.add('mdl-js-button'); + link.classList.add('mdl-js-ripple-effect'); + link.classList.add('mdl-button--colored'); + const icon = document.createElement('i'); + icon.classList.add('material-icons'); + icon.textContent = 'help'; + link.appendChild(icon); + parentDiv.appendChild(link); + } + + /** + * Makes a row, for storing an input. + * @param {?string} labelString + * @param {?string} tooltipString + * @param {string=} rowClass + */ + addRow(labelString, tooltipString, rowClass) { + this.latestRow_ = document.createElement('div'); + if (rowClass) { + this.latestRow_.classList.add(rowClass); + } + this.table_.appendChild(this.latestRow_); + + const elementId = 'input-container-row-' + this.numRows_; + this.numRows_ += 1; + + if (labelString) { + const label = document.createElement('label'); + label.setAttribute('for', elementId); + label.classList.add('input-container-label'); + const labelText = document.createElement('b'); + labelText.textContent = labelString; + label.appendChild(labelText); + this.latestRow_.appendChild(label); + } + + this.latestElementContainer = document.createElement('div'); + this.latestRow_.appendChild(this.latestElementContainer); + + this.latestElementContainer.classList.add('input-container-row'); + this.latestElementContainer.id = elementId; + + this.latestTooltip = tooltipString; + if (tooltipString) { + ShakaDemoTooltips.make(this.table_, this.latestRow_, tooltipString); + // Keep the row from being focused. + this.latestRow_.setAttribute('tabindex', -1); + this.latestRow_.classList.add('borderless'); + } + } +} + +/** @enum {string} */ +ShakaDemoInputContainer.Style = { + VERTICAL: 'input-container-style-vertical', + ACCORDION: 'input-container-style-accordion', + FLEX: 'input-container-style-flex', +}; diff --git a/demo/load.js b/demo/load.js index 4aafa97c1..d6d58d8cf 100644 --- a/demo/load.js +++ b/demo/load.js @@ -49,19 +49,23 @@ function shakaUncompiledModeSupported() { // NOTE: This is a quick-and-easy hack based on assumption that the old // demo page will be replaced in the near future. - function loadCss(buildType) { + function loadSpecificCss(href, linkRel) { var link = document.createElement('link'); link.type = 'text/css'; - if (buildType == 'uncompiled') { - link.rel = 'stylesheet/less'; - link.href = baseUrl + '../ui/controls.less'; - } else { - link.rel = 'stylesheet'; - link.href = baseUrl + '../dist/controls.css'; - } + link.href = baseUrl + href; + link.rel = linkRel; document.head.appendChild(link); } + function loadCss(buildType) { + if (buildType == 'uncompiled') { + loadSpecificCss('../ui/controls.less', 'stylesheet/less'); + loadSpecificCss('../demo/demo.less', 'stylesheet/less'); + } else { + loadSpecificCss('../dist/controls.css', 'stylesheet'); + loadSpecificCss('../dist/demo.css', 'stylesheet'); + } + } function importScript(src) { var script = document.createElement('script'); diff --git a/demo/log_section.js b/demo/log_section.js deleted file mode 100644 index 24004cda3..000000000 --- a/demo/log_section.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - * @license - * Copyright 2016 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Shaka Player demo, main section. - * - * @suppress {visibility} to work around compiler errors until we can - * refactor the demo into classes that talk via public method. TODO - */ - - -/** @suppress {duplicate} */ -var shakaDemo = shakaDemo || {}; // eslint-disable-line no-var - - -/** @private {!Object.} */ -shakaDemo.originalConsoleMethods_ = { - 'error': function() {}, - 'warn': function() {}, - 'info': function() {}, - 'log': function() {}, - 'debug': function() {}, -}; - - -/** @private {!Object.} */ -shakaDemo.patchedConsoleMethods_ = { - 'error': function() {}, - 'warn': function() {}, - 'info': function() {}, - 'log': function() {}, - 'debug': function() {}, -}; - - -/** @private */ -shakaDemo.setupLogging_ = function() { - let logToScreen = document.getElementById('logToScreen'); - let log = document.getElementById('log'); - - if (!shaka['log']) { - // This may be the compiled library, which has no logging by default. - logToScreen.parentElement.style.display = 'none'; - return; - } - - if (!Object.keys || !window.console || !console.log || !console.log.bind) { - // This may be a very old browser that we can't support anyway. - return; - } - - // Store the original and to-screen versions of logging methods. - Object.keys(shakaDemo.originalConsoleMethods_).forEach(function(k) { - let original = console[k].bind(console); - let className = k + 'Log'; - shakaDemo.originalConsoleMethods_[k] = original; - shakaDemo.patchedConsoleMethods_[k] = function() { - // Pass the call on to the original: - original.apply(console, arguments); - shakaDemo.formatLog_(log, className, arguments); - }; - }); - - logToScreen.addEventListener('change', shakaDemo.onLogChange_); - // Set the initial state. - shakaDemo.onLogChange_(); -}; - - -/** @private */ -shakaDemo.onLogChange_ = function() { - if (!shaka['log']) return; - - let logToScreen = document.getElementById('logToScreen'); - let logSection = document.getElementById('logSection'); - if (logToScreen.checked) { - logSection.style.display = 'block'; - logSection.open = true; // Open the details to show the logs. - for (let k in shakaDemo.patchedConsoleMethods_) { - console[k] = shakaDemo.patchedConsoleMethods_[k]; - } - } else { - logSection.style.display = 'none'; - for (let k in shakaDemo.originalConsoleMethods_) { - console[k] = shakaDemo.originalConsoleMethods_[k]; - } - } - // Re-initialize Shaka library logging to the freshly-patched console methods. - shaka['log']['setLevel'](shaka['log']['currentLevel']); - // Change the hash, to mirror this. - shakaDemo.hashShouldChange_(); -}; - - -/** - * @param {Element} log - * @param {string} className - * @param {Arguments} logArguments - * @private - */ -shakaDemo.formatLog_ = function(log, className, logArguments) { - // Format the log and append it to the HTML: - let div = document.createElement('div'); - div.className = className; - for (let i = 0; i < logArguments.length; ++i) { - let span = document.createElement('span'); - let arg = logArguments[i]; - let text; - if (arg === null) { - text = 'null'; - } else if (arg === undefined) { - text = 'undefined'; - } else { - text = arg.toString(); - } - if (Array.isArray(arg) || text == '[object Object]') { - text = JSON.stringify(arg); - } - span.textContent = text; - div.appendChild(span); - } - log.appendChild(div); - log.scrollTop = log.scrollHeight; -}; diff --git a/demo/main.js b/demo/main.js index b37746f18..aa02782cb 100644 --- a/demo/main.js +++ b/demo/main.js @@ -16,62 +16,971 @@ */ /** - * @fileoverview Shaka Player demo, main section. - * - * @suppress {visibility} to work around compiler errors until we can - * refactor the demo into classes that talk via public method. TODO + * 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). */ +class ShakaDemoMain { + constructor() { + /** @private {HTMLMediaElement} */ + this.video_; + + /** @private {shaka.Player} */ + this.player_; + + /** @type {?ShakaDemoAssetInfo} */ + this.selectedAsset; + + /** @private {?shaka.extern.PlayerConfiguration} */ + this.defaultConfig_; + + /** @private {boolean} */ + this.fullyLoaded_ = false; + + /** @private {?shaka.ui.Controls} */ + this.controls_; + + /** @private {boolean} */ + this.nativeControlsEnabled_ = false; + + /** @private {?shaka.offline.Storage} */ + this.storage_; + + /** @private {shaka.extern.SupportType} */ + this.support_; + + /** @private {string} */ + this.uiLocale_ = ''; + } + + /** + * This function contains the steps of initialization that should be followed + * whether or not the demo successfully set up. + * @private + */ + initCommon_() { + // Set up event listeners. + document.getElementById('error-display-close-button').addEventListener( + 'click', (event) => this.closeError_()); + + // Set up version string. + this.setUpVersionString_(); + } + + /** + * 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. + */ + initFailed() { + this.initCommon_(); + + // Process a synthetic error about lack of browser support. + const severity = shaka.util.Error.Severity.CRITICAL; + const message = 'Your browser is not supported!'; + const href = 'https://github.com/google/shaka-player#' + + 'platform-and-browser-support-matrix'; + this.handleError_(severity, message, href); + + // 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' clas. + elementsToDisable.push(document.getElementsByClassName( + 'mdl-layout__drawer-button')); + for (const element of elementsToDisable) { + element.tabIndex = -1; + element.classList.add('disabled-by-fail'); + } + } + + /** + * Initialize the application. + */ + async init() { + this.initCommon_(); + + this.support_ = await shaka.Player.probeSupport(); + + this.video_ = + /** @type {!HTMLVideoElement} */(document.getElementById('video')); + this.video_.poster = ShakaDemoMain.mainPoster_; + + if (navigator.serviceWorker) { + console.debug('Registering service worker.'); + try { + const registration = + await navigator.serviceWorker.register('service_worker.js'); + console.debug('Service worker registered!', registration.scope); + } catch (error) { + console.error('Service worker registration failed!', error); + } + } + + this.setupPlayer_(); + this.readHash_(); + window.addEventListener('hashchange', () => this.hashChanged_()); + + await this.setupStorage_(); + + // The main page is loaded. Dispatch an event, so the various + // configurations will load themselves. + this.dispatchEventWithName_('shaka-main-loaded'); + + // Update the componentHandler, to account for any new MDL elements added. + componentHandler.upgradeDom(); + + this.fullyLoaded_ = true; + this.remakeHash(); + } + + /** @private */ + setupPlayer_() { + const videoContainer = /** @type {!HTMLElement} */ ( + document.getElementById('video-container')); + const video = /** @type {!HTMLVideoElement} */ (this.video_); + + // Register custom controls to the UI. + shaka.ui.Controls.registerElement('close', new CloseButton.Factory()); + + // Set up UI. + const uiControlPanelElements = [ + 'time_and_duration', + 'spacer', + 'mute', + 'volume', + 'fullscreen', + 'overflow_menu', + 'close', + ]; + const uiConfig = { + castReceiverAppId: '7B25EC44', + controlPanelElements: uiControlPanelElements, + }; + this.player_ = new shaka.Player(video); + const ui = new shaka.ui.Overlay(this.player_, videoContainer, video); + ui.configure(uiConfig); + + // 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(); + 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 + + this.player_.addEventListener( + 'error', (event) => this.onErrorEvent_(event)); + + // Listen to events on controls. + this.controls_ = ui.getControls(); + this.controls_.addEventListener('error', shakaDemoMain.onErrorEvent_); + this.controls_.addEventListener('caststatuschanged', (event) => { + this.onCastStatusChange_(event['newStatus']); + }); + + // Disable controls until something is loaded + this.controls_.setEnabledShakaControls(false); + this.controls_.setEnabledNativeControls(false); + + const drawerCloseButton = document.getElementById('drawer-close-button'); + drawerCloseButton.addEventListener('click', () => { + const layout = document.getElementById('main-layout'); + layout.MaterialLayout.toggleDrawer(); + }); + } + + /** + * @return {!Promise} + * @private + */ + async setupStorage_() { + goog.asserts.assert(this.player_, 'Player must already be initialized.'); + this.storage_ = new shaka.offline.Storage(this.player_); + + const 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]' : ''); + }; + const getAssetWithIdentifier = (identifier) => { + for (const asset of shakaAssets.testAssets) { + if (getIdentifierFromAsset(asset) == identifier) { + return asset; + } + } + return null; + }; + + const progressCallback = (content, progress) => { + const identifier = content.appMetadata['identifier']; + const asset = getAssetWithIdentifier(identifier); + if (asset) { + asset.storedProgress = progress; + this.dispatchEventWithName_('shaka-main-offline-progress'); + } + }; + const trackSelectionCallback = (tracks) => { + // Select the highest-bandwidth variant. + const bestTrack = tracks + .filter((track) => track.type == 'variant') + .sort((a, b) => a.bandwidth - b.bandwidth) + .pop(); + return [bestTrack]; + }; + this.storage_.configure({offline: { + progressCallback: progressCallback, + trackSelectionCallback: trackSelectionCallback, + }}); + + // TODO: Add support for storing DRM-protected assets. + + // Setup asset callbacks for storage. + for (const asset of shakaAssets.testAssets) { + if (this.getAssetUnsupportedReason(asset, /* needOffline= */ true)) { + // Don't bother setting up the callbacks. + continue; + } + + asset.storeCallback = async () => { + await this.drmConfiguration_(asset); + const metadata = { + 'identifier': getIdentifierFromAsset(asset), + 'downloaded': new Date(), + }; + asset.storedProgress = 0; + this.dispatchEventWithName_('shaka-main-offline-progress'); + const stored = await this.storage_.store(asset.manifestUri, metadata); + asset.storedContent = stored; + this.dispatchEventWithName_('shaka-main-offline-changed'); + // Update the componentHandler, to account for any new MDL elements + // added. + componentHandler.upgradeDom(); + }; + asset.unstoreCallback = async () => { + if (asset == this.selectedAsset) { + this.unload(); + } + if (asset.storedContent && asset.storedContent.offlineUri) { + asset.storedProgress = 0; + this.dispatchEventWithName_('shaka-main-offline-progress'); + await this.storage_.remove(asset.storedContent.offlineUri); + asset.storedContent = null; + this.dispatchEventWithName_('shaka-main-offline-changed'); + // Update the componentHandler, to account for any new MDL elements + // added. + componentHandler.upgradeDom(); + } + }; + } + + // Load stored asset infos. + const list = await this.storage_.list(); + for (const storedContent of list) { + console.log(storedContent); + const identifier = storedContent.appMetadata['identifier']; + const asset = getAssetWithIdentifier(identifier); + if (asset) { + asset.storedContent = storedContent; + } + } + } + + /** @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 {?string} unsupportedReason Null if asset is supported. + */ + getAssetUnsupportedReason(asset, needOffline) { + // Is the asset disabled? + if (asset.disabled) { + return 'This asset is disabled.'; + } + + if (needOffline && !asset.features.includes(shakaAssets.Feature.OFFLINE)) { + return 'This asset cannot be downloaded.'; + } + + if (!asset.drm.includes(shakaAssets.KeySystem.CLEAR)) { + const hasSupportedDRM = asset.drm.some((drm) => { + return this.support_.drm[drm]; + }); + if (!hasSupportedDRM) { + return 'Your browser does not support the required key systems.'; + } + if (needOffline) { + const hasSupportedOfflineDRM = asset.drm.some((drm) => { + return this.support_.drm[drm] && + this.support_.drm[drm].persistentState; + }); + if (!hasSupportedOfflineDRM) { + return 'Your browser does not support offline licenses for the ' + + 'required key systems.'; + } + } + } + + // Does the browser support the asset's manifest type? + if (asset.features.includes(shakaAssets.Feature.DASH) && + !this.support_.manifest['mpd']) { + return 'Your browser does not support MPEG-DASH manifests.'; + } + if (asset.features.includes(shakaAssets.Feature.HLS) && + !this.support_.manifest['m3u8']) { + return 'Your browser does not support HLS manifests.'; + } + + // Does the asset contain a playable mime type? + let 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 'Your browser does not support the required video format.'; + } + + return null; + } + + /** + * 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_; + } + + /** @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.controls_.getLocalization().changeLocale(preferredLocales); + } + + /** @return {string} */ + getUILocale() { + return this.uiLocale_; + } + + /** @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(); + ShakaDemoUtils.runThroughHashParams(readParam, config); + } + 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. + 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'); + 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(function(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; + }); + } + } + + // Disable custom controls. + this.nativeControlsEnabled_ = 'nativecontrols' in params; + + // Check if uncompiled mode is supported. + if (!ShakaDemoUtils.browserSupportsUncompiledMode()) { + const uncompiledLink = document.getElementById('uncompiled_link'); + 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); + } + } + } + + /** @return {!Object.} params */ + 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.} */ + const combined = fields.concat(fragments); + const params = {}; + for (let i = 0; i < combined.length; ++i) { + const kv = combined[i].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]]; + } + } + } + + /** + * 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.player_.getConfiguration(); + return this.getValueFromGivenConfig_(valueName, config); + } + + /** + * @param {string} valueName + */ + resetConfiguration(valueName) { + this.configure(valueName, undefined); + } + + /** + * @param {string|!Object} config + * @param {*=} value + */ + configure(config, value) { + this.player_.configure(config, value); + } + + /** @return {!shaka.extern.PlayerConfiguration} */ + getConfiguration() { + return this.player_.getConfiguration(); + } + + /** + * @param {string} uri + * @return {!Promise.} + * @private + */ + async requestCertificate_(uri) { + const netEngine = this.player_.getNetworkingEngine(); + 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.hideNode_(videoBar); + this.video_.poster = ShakaDemoMain.mainPoster_; + + this.player_.unload(); + + // The currently-selected asset changed, so update asset cards. + this.dispatchEventWithName_('shaka-main-selected-asset-changed'); + } + + /** + * @param {ShakaDemoAssetInfo} asset + * @return {!Promise} + * @private + */ + async drmConfiguration_(asset) { + asset.applyFilters(this.player_.getNetworkingEngine()); + const assetConfig = asset.getConfiguration(); + for (let section in assetConfig) { + this.configure(section, assetConfig[section]); + } + + const config = 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); + const certArray = new Uint8Array(certificate); + for (const drmSystem of asset.licenseServers.keys()) { + 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; + } + } + } + + this.configure('drm.advanced', config.drm.advanced); + shakaDemoMain.remakeHash(); + } + + /** + * @param {ShakaDemoAssetInfo} asset + */ + async loadAsset(asset) { + this.selectedAsset = asset; + const videoBar = document.getElementById('video-bar'); + this.showNode_(videoBar); + this.closeError_(); + this.video_.poster = ShakaDemoMain.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'}); + + // 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}); + + const manifestUri = (asset.storedContent ? + asset.storedContent.offlineUri : + null) || asset.manifestUri; + this.player_.load(manifestUri).then(() => { + // Now that something is loaded, enable controls. + if (this.nativeControlsEnabled_) { + this.controls_.setEnabledShakaControls(false); + this.controls_.setEnabledNativeControls(true); + } else { + this.controls_.setEnabledShakaControls(true); + this.controls_.setEnabledNativeControls(false); + } + if (this.player_.isAudioOnly()) { + this.video_.poster = ShakaDemoMain.audioOnlyPoster_; + } + }).catch((error) => { + this.onError_(/** @type {!shaka.util.Error} */ (error)); + }); + } + + /** 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(); + ShakaDemoUtils.runThroughHashParams(setParam, config); + } + if (!this.getCurrentConfigValue('abr.enabled')) { + params.push('noadaptation'); + } + if (this.getCurrentConfigValue('streaming.jumpLargeGaps')) { + params.push('jumpLargeGaps'); + } + params.push('uilang=' + this.getUILocale()); + + const navButtons = document.getElementById('nav-button-container'); + for (let button of navButtons.childNodes) { + if (button.nodeType == Node.ELEMENT_NODE && + button.classList.contains('mdl-button--accent')) { + params.push('panel=' + button.textContent); + 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.nativeControlsEnabled_) { + params.push('nativecontrols'); + } + + // 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 {Node} node + * @private + */ + hideNode_(node) { + node.classList.add('hidden'); + } + + /** + * @param {Node} node + * @private + */ + showNode_(node) { + node.classList.remove('hidden'); + } + + /** + * 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 {!HTMLDivElement} The container for the tab. + */ + 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.textContent); + if (!selected && !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.hideNode_(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 (let child of navButtons.childNodes) { + if (child.nodeType == Node.ELEMENT_NODE) { + child.classList.remove('mdl-button--accent'); + } + } + for (let child of contents.childNodes) { + if (child.nodeType == Node.ELEMENT_NODE) { + this.hideNode_(child); + } + } + button.classList.add('mdl-button--accent'); + this.showNode_(container); + this.remakeHash(); + + // Scroll so that the top of the tab is in view. + container.scrollIntoView({behavior: 'smooth', block: 'start'}); + }; + button.addEventListener('click', switchPage); + if (selected) { + switchPage(); + } + + return /** @type {!HTMLDivElement} */ (container); + } + + /** + * Dispatches a custom event to document. + * @param {string} name + * @private + */ + dispatchEventWithName_(name) { + const event = document.createEvent('CustomEvent'); + event.initCustomEvent(name, + /* canBubble = */ false, + /* cancelable = */ false, + /* detail = */ null); + document.dispatchEvent(event); + } + + /** @private */ + setUpVersionString_() { + const version = shaka.Player.version; + let split = version.split('-'); + let 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 versionStringDiv = document.getElementById('version-string'); + versionStringDiv.textContent = split.join('-'); + if (inParen.length > 0) { + versionStringDiv.textContent += ' (' + inParen.join(', ') + ')'; + } + } + + /** + * Closes the error bar. + * @private + */ + closeError_() { + document.getElementById('error-display').classList.add('hidden'); + const link = document.getElementById('error-display-link'); + link.href = ''; + link.textContent = ''; + link.severity = null; + } + + /** + * @param {!Event} event + * @private + */ + onErrorEvent_(event) { + 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; + } + + this.handleError_(severity, message, href); + } + + /** + * @param {!shaka.util.Error.Severity} severity + * @param {string} message + * @param {string} href + * @private + */ + handleError_(severity, message, href) { + const link = document.getElementById('error-display-link'); + + // Always show the new error if: + // 1. there is no error showing currently + // 2. the new error is more severe than the old one + if (link.severity == null || severity > link.severity) { + link.href = href; + // IE8 and other very old browsers don't have textContent. + if (link.textContent === undefined) { + link.innerText = message; + } else { + link.textContent = message; + } + link.severity = severity; + if (link.href) { + link.classList.remove('input-disabled'); + } else { + link.classList.add('input-disabled'); + } + document.getElementById('error-display').classList.remove('hidden'); + } + } + + /** + * @param {boolean} connected + * @private + */ + onCastStatusChange_(connected) { + // TODO: Handle. + } +} -/** @suppress {duplicate} */ -var shakaDemo = shakaDemo || {}; // eslint-disable-line no-var - - -/** @private {shaka.cast.CastProxy} */ -shakaDemo.castProxy_ = null; - - -/** @private {HTMLMediaElement} */ -shakaDemo.video_ = null; - - -/** @private {shaka.Player} */ -shakaDemo.player_ = null; - - -/** @private {HTMLMediaElement} */ -shakaDemo.localVideo_ = null; - - -/** @private {shaka.Player} */ -shakaDemo.localPlayer_ = null; - - -/** @private {shaka.extern.SupportType} */ -shakaDemo.support_; - - -/** @private {shaka.ui.Controls} */ -shakaDemo.controls_ = null; - - -/** @private {boolean} */ -shakaDemo.hashCanChange_ = false; - - -/** @private {boolean} */ -shakaDemo.suppressHashChangeEvent_ = false; - - -/** @private {(number|undefined)} */ -shakaDemo.startTime_ = undefined; +let shakaDemoMain = new ShakaDemoMain(); /** * @private * @const {string} */ -shakaDemo.mainPoster_ = +ShakaDemoMain.mainPoster_ = 'https://shaka-player-demo.appspot.com/assets/poster.jpg'; @@ -79,707 +988,10 @@ shakaDemo.mainPoster_ = * @private * @const {string} */ -shakaDemo.audioOnlyPoster_ = +ShakaDemoMain.audioOnlyPoster_ = 'https://shaka-player-demo.appspot.com/assets/audioOnly.gif'; -/** - * Initialize the application. - */ -shakaDemo.init = function() { - document.getElementById('errorDisplayCloseButton').addEventListener( - 'click', shakaDemo.closeError); - - // Display the version number. - document.getElementById('version').textContent = shaka.Player.version; - - // Fill in the language preferences based on browser config, if available. - const languages = navigator.languages || ['en-us']; - document.getElementById('preferredAudioLanguage').value = languages[0]; - document.getElementById('preferredTextLanguage').value = languages[0]; - document.getElementById('preferredUILanguage').value = languages[0]; - // TODO(#1591): Support multiple language preferences - - document.getElementById('preferredAudioChannelCount').value = '2'; - - let params = shakaDemo.getParams_(); - - shakaDemo.setupLogging_(); - - shakaDemo.preBrowserCheckParams_(params); - - // Display uncaught exceptions. - window.addEventListener('error', function(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 (event.message.includes('cvox.ApiImplementation')) return; - - shakaDemo.onError_(/** @type {!shaka.util.Error} */ (event.error)); - }); - - if (!shaka.Player.isBrowserSupported()) { - let errorDisplayLink = document.getElementById('errorDisplayLink'); - let error = 'Your browser is not supported!'; - - // IE8 and other very old browsers don't have textContent. - if (errorDisplayLink.textContent === undefined) { - errorDisplayLink.innerText = error; - } else { - errorDisplayLink.textContent = error; - } - - // Disable the load/unload buttons. - let loadButton = document.getElementById('loadButton'); - loadButton.disabled = true; - document.getElementById('unloadButton').disabled = true; - - // Hide the error message's close button. - let errorDisplayCloseButton = - document.getElementById('errorDisplayCloseButton'); - errorDisplayCloseButton.style.display = 'none'; - - // Make sure the error is seen. - errorDisplayLink.style.fontSize = '250%'; - - // TODO: Link to docs about browser support. For now, disable link. - errorDisplayLink.href = '#'; - // Disable for newer browsers: - errorDisplayLink.style.pointerEvents = 'none'; - // Disable for older browsers: - errorDisplayLink.style.textDecoration = 'none'; - errorDisplayLink.style.cursor = 'default'; - errorDisplayLink.onclick = function() { return false; }; - - let errorDisplay = document.getElementById('errorDisplay'); - errorDisplay.style.display = 'block'; - } else { - if (navigator.serviceWorker) { - console.debug('Registering service worker.'); - navigator.serviceWorker.register('service_worker.js') - .then(function(registration) { - console.debug('Service worker registered!', registration.scope); - }).catch(function(error) { - console.error('Service worker registration failed!', error); - }); - } - - /** @param {Event} event */ - let offlineStatusChanged = function(event) { - let version = document.getElementById('version'); - let text = version.textContent; - text = text.replace(' (offline)', ''); - if (!navigator.onLine) { - text += ' (offline)'; - } - version.textContent = text; - shakaDemo.computeDisabledAssets(); - }; - window.addEventListener('online', offlineStatusChanged); - window.addEventListener('offline', offlineStatusChanged); - offlineStatusChanged(null); - - shaka.Player.probeSupport().then(function(support) { - shakaDemo.support_ = support; - - let localVideo = - /** @type {!HTMLVideoElement} */(document.getElementById('video')); - - let videoContainer = localVideo.parentElement; - let ui = localVideo['ui']; - - let localPlayer = ui.getPlayer(); - shakaDemo.castProxy_ = ui.getControls().getCastProxy(); - - shakaDemo.video_ = shakaDemo.castProxy_.getVideo(); - shakaDemo.player_ = shakaDemo.castProxy_.getPlayer(); - shakaDemo.player_.addEventListener('error', shakaDemo.onErrorEvent_); - shakaDemo.localVideo_ = localVideo; - shakaDemo.localPlayer_ = localPlayer; - - // Set the default poster. - shakaDemo.localVideo_.poster = shakaDemo.mainPoster_; - - let asyncSetup = shakaDemo.setupAssets_(); - shakaDemo.setupOffline_(); - shakaDemo.setupInfo_(); - shakaDemo.setupConfiguration_(); - - goog.asserts.assert(videoContainer, 'Must have a video container.'); - - shakaDemo.controls_ = ui.getControls(); - const localization = shakaDemo.controls_.getLocalization(); - - localization.addEventListener( - shaka.ui.Localization.UNKNOWN_LOCALES, (event) => { - for (let locale of event['locales']) { - shakaDemo.loadLocale_(locale); - } - }); - - const uiLang = document.getElementById('preferredUILanguage').value; - localization.changeLocale([uiLang]); - // TODO(#1591): Support multiple language preferences - - shakaDemo.controls_.addEventListener('error', shakaDemo.onErrorEvent_); - - shakaDemo.controls_.addEventListener('caststatuschanged', (event) => { - shakaDemo.onCastStatusChange_(event['newStatus']); - }); - - // Disable controls until something is loaded - shakaDemo.controls_.setEnabledShakaControls(false); - - asyncSetup.catch(function(error) { - // shakaDemo.setupOfflineAssets_ errored while trying to - // load the offline assets. Notify the user of this. - shakaDemo.onError_(/** @type {!shaka.util.Error} */ (error)); - }).then(function() { - shakaDemo.postBrowserCheckParams_(params); - window.addEventListener('hashchange', shakaDemo.updateFromHash_); - }); - }).catch(function(error) { - // Some part of the setup of the demo app threw an error. - // Notify the user of this. - shakaDemo.onError_(/** @type {!shaka.util.Error} */ (error)); - }); - } -}; - - -/** - * @return {!Object.} params - * @private - */ -shakaDemo.getParams_ = function() { - // 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.} */ - let combined = fields.concat(fragments); - let params = {}; - for (let i = 0; i < combined.length; ++i) { - let kv = combined[i].split('='); - params[kv[0]] = kv.slice(1).join('='); - } - return params; -}; - - -/** - * @param {!Object.} params - * @private - */ -shakaDemo.preBrowserCheckParams_ = function(params) { - if ('videoRobustness' in params) { - document.getElementById('drmSettingsVideoRobustness').value = - params['videoRobustness']; - } - if ('audioRobustness' in params) { - document.getElementById('drmSettingsAudioRobustness').value = - params['audioRobustness']; - } - if ('lang' in params) { - document.getElementById('preferredAudioLanguage').value = params['lang']; - document.getElementById('preferredTextLanguage').value = params['lang']; - document.getElementById('preferredUILanguage').value = params['lang']; - } - if ('audiolang' in params) { - document.getElementById('preferredAudioLanguage').value = - params['audiolang']; - } - if ('textlang' in params) { - document.getElementById('preferredTextLanguage').value = params['textlang']; - } - if ('uilang' in params) { - document.getElementById('preferredUILanguage').value = params['uilang']; - } - if ('channels' in params) { - document.getElementById('preferredAudioChannelCount').value = - params['channels']; - } - if ('asset' in params) { - document.getElementById('manifestInput').value = params['asset']; - } - if ('license' in params) { - document.getElementById('licenseServerInput').value = params['license']; - } - if ('certificate' in params) { - document.getElementById('certificateInput').value = params['certificate']; - } - if ('availabilityWindowOverride' in params) { - document.getElementById('availabilityWindowOverride').value = - params['availabilityWindowOverride']; - } - if ('logtoscreen' in params) { - document.getElementById('logToScreen').checked = true; - // Call onLogChange_ manually, because setting checked - // programatically doesn't fire a 'change' event. - shakaDemo.onLogChange_(); - } - if ('noinput' in params) { - // Both the content container and body need different styles in this mode. - document.getElementById('container').className = 'noinput'; - document.body.className = 'noinput'; - } - if ('play' in params) { - document.getElementById('enableLoadOnRefresh').checked = true; - } - if ('startTime' in params) { - // Used manually for debugging start time issues in live streams. - shakaDemo.startTime_ = parseInt(params['startTime'], 10); - } - // shaka.log is not set if logging isn't enabled. - // I.E. if using the compiled version of shaka. - if (shaka.log) { - // The log level selector is only visible if logging is available. - document.getElementById('logLevelListDiv').hidden = false; - - // Set log level. - let toSelectValue; - if ('vv' in params) { - toSelectValue = 'vv'; - shaka.log.setLevel(shaka.log.Level.V2); - } else if ('v' in params) { - toSelectValue = 'v'; - shaka.log.setLevel(shaka.log.Level.V1); - } else if ('debug' in params) { - toSelectValue = 'debug'; - shaka.log.setLevel(shaka.log.Level.DEBUG); - } - if (toSelectValue) { - // Set the log level selector to the proper value. - let logLevelList = document.getElementById('logLevelList'); - for (let index = 0; index < logLevelList.length; index++) { - if (logLevelList[index].value == toSelectValue) { - logLevelList.selectedIndex = index; - break; - } - } - } - } -}; - - -/** - * Decide if a license server from the demo app URI matches the configuration - * of a demo asset. - * - * @param {!shakaAssets.AssetInfo} assetInfo - * @param {?string} licenseUri - * @return {boolean} - * @private - */ -shakaDemo.licenseServerMatch_ = function(assetInfo, licenseUri) { - // If no license server was specified, assume that this is a match. - // This provides backward compatibility and shorter URIs. - if (!licenseUri) { - return true; - } - - // If a server is specified in the URI, but not in the asset, it's not a - // match. It's not clear when this would ever be meaningful, so the decision - // not to match is arbitrary. - if (licenseUri && !assetInfo.licenseServers) { - return false; - } - - // Otherwise, it's a match only if the license server in the URI matches what - // is in the asset config. - for (let k in assetInfo.licenseServers) { - if (licenseUri == assetInfo.licenseServers[k]) { - return true; - } - } - - return false; -}; - - -/** - * @param {!Object.} params - * @private - */ -shakaDemo.postBrowserCheckParams_ = function(params) { - // If a custom asset was given in the URL, select it now. - if ('asset' in params) { - let assetList = document.getElementById('assetList'); - let assetUri = params['asset']; - let licenseUri = params['license']; - let isDefault = false; - // Check all options except the last, which is 'custom asset'. - for (let index = 0; index < assetList.options.length - 1; index++) { - if (assetList[index].asset && - assetList[index].asset.manifestUri == assetUri && - shakaDemo.licenseServerMatch_(assetList[index].asset, licenseUri)) { - assetList.selectedIndex = index; - isDefault = true; - break; - } - } - if (isDefault) { - // Clear the custom fields. - document.getElementById('manifestInput').value = ''; - document.getElementById('licenseServerInput').value = ''; - } else { - // It was a custom asset, so put it into the custom field. - assetList.selectedIndex = assetList.options.length - 1; - let customAsset = document.getElementById('customAsset'); - customAsset.style.display = 'block'; - } - - // Call updateButtons_ manually, because changing assetList - // programatically doesn't fire a 'change' event. - shakaDemo.updateButtons_(/* canHide */ true); - } - - let smallGapLimit = document.getElementById('smallGapLimit'); - smallGapLimit.placeholder = 0.5; // The default smallGapLimit. - if ('smallGapLimit' in params) { - smallGapLimit.value = params['smallGapLimit']; - // Call onGapInput_ manually, because setting the value - // programatically doesn't fire 'input' event. - let fakeEvent = /** @type {!Event} */({target: smallGapLimit}); - shakaDemo.onGapInput_(fakeEvent); - } - - let jumpLargeGaps = document.getElementById('jumpLargeGaps'); - if ('jumpLargeGaps' in params) { - jumpLargeGaps.checked = true; - // Call onJumpLargeGapsChange_ manually, because setting checked - // programatically doesn't fire a 'change' event. - let fakeEvent = /** @type {!Event} */({target: jumpLargeGaps}); - shakaDemo.onJumpLargeGapsChange_(fakeEvent); - } else { - jumpLargeGaps.checked = - shakaDemo.player_.getConfiguration().streaming.jumpLargeGaps; - } - - if ('noadaptation' in params) { - let enableAdaptation = document.getElementById('enableAdaptation'); - enableAdaptation.checked = false; - // Call onAdaptationChange_ manually, because setting checked - // programatically doesn't fire a 'change' event. - let fakeEvent = /** @type {!Event} */({target: enableAdaptation}); - shakaDemo.onAdaptationChange_(fakeEvent); - } - - if ('nativecontrols' in params) { - let showNative = document.getElementById('showNative'); - showNative.checked = true; - // Call onNativeChange_ manually, because setting checked - // programatically doesn't fire a 'change' event. - let fakeEvent = /** @type {!Event} */({target: showNative}); - shakaDemo.onNativeChange_(fakeEvent); - } - - // Allow the hash to be changed, and give it an initial change. - shakaDemo.hashCanChange_ = true; - shakaDemo.hashShouldChange_(); - - if ('noinput' in params || 'play' in params) { - shakaDemo.load(); - } -}; - - -/** @private */ -shakaDemo.updateFromHash_ = function() { - // Hash changes made by us should be ignored. We only want to respond to hash - // changes made by the user in the URL bar. - if (shakaDemo.suppressHashChangeEvent_) { - shakaDemo.suppressHashChangeEvent_ = false; - return; - } - - let params = shakaDemo.getParams_(); - shakaDemo.preBrowserCheckParams_(params); - shakaDemo.postBrowserCheckParams_(params); -}; - - -/** @private */ -shakaDemo.hashShouldChange_ = function() { - if (!shakaDemo.hashCanChange_) { - return; - } - - let params = []; - let oldParams = shakaDemo.getParams_(); - - // Save the current asset. - let assetUri; - let licenseServerUri; - if (shakaDemo.player_) { - assetUri = shakaDemo.player_.getAssetUri(); - let drmInfo = shakaDemo.player_.drmInfo(); - if (drmInfo) { - licenseServerUri = drmInfo.licenseServerUri; - } - } - let assetList = document.getElementById('assetList'); - if (assetUri) { - // Store the currently playing asset URI. - params.push('asset=' + assetUri); - - // Is the asset a default asset? - let isDefault = false; - // Check all options except the last, which is 'custom asset'. - for (let index = 0; index < assetList.options.length - 1; index++) { - if (assetList[index].asset.manifestUri == assetUri) { - isDefault = true; - break; - } - } - - // If it's a custom asset we should store whatever the license - // server URI is. - if (!isDefault && licenseServerUri) { - params.push('license=' + licenseServerUri); - } - } else { - if (assetList.selectedIndex == assetList.length - 1) { - // It's a custom asset. - let manifestInputValue = document.getElementById('manifestInput').value; - if (manifestInputValue) { - params.push('asset=' + manifestInputValue); - } - - let licenseInputValue = - document.getElementById('licenseServerInput').value; - if (licenseInputValue) { - params.push('license=' + licenseInputValue); - } - } else { - // It's a default asset. - params.push('asset=' + - assetList[assetList.selectedIndex].asset.manifestUri); - } - } - - // The certificate URI can't be had from DrmInfo, so always use the UI state. - let certificateInputValue = - document.getElementById('certificateInput').value; - if (certificateInputValue) { - params.push('certificate=' + certificateInputValue); - } - - // Save config panel state. - if (document.getElementById('smallGapLimit').value.length) { - params.push('smallGapLimit=' + - document.getElementById('smallGapLimit').value); - } - if (document.getElementById('jumpLargeGaps').checked) { - params.push('jumpLargeGaps'); - } - const audioLang = document.getElementById('preferredAudioLanguage').value; - const textLang = document.getElementById('preferredTextLanguage').value; - const uiLang = document.getElementById('preferredUILanguage').value; - if (textLang == audioLang && audioLang == uiLang) { - params.push('lang=' + audioLang); - } else { - params.push('audiolang=' + audioLang); - params.push('textlang=' + textLang); - params.push('uilang=' + uiLang); - } - let channels = document.getElementById('preferredAudioChannelCount').value; - if (channels != '2') { - params.push('channels=' + channels); - } - if (document.getElementById('logToScreen').checked) { - params.push('logtoscreen'); - } - if (!document.getElementById('enableAdaptation').checked) { - params.push('noadaptation'); - } - if (document.getElementById('showNative').checked) { - params.push('nativecontrols'); - } - let availabilityWindowOverride = - document.getElementById('availabilityWindowOverride').value; - if (availabilityWindowOverride) { - params.push('availabilityWindowOverride=' + availabilityWindowOverride); - } - if (shaka.log) { - let logLevelList = document.getElementById('logLevelList'); - let logLevel = logLevelList[logLevelList.selectedIndex].value; - if (logLevel != 'info') { - params.push(logLevel); - } - } - if (document.getElementById('enableLoadOnRefresh').checked) { - params.push('play'); - } - - // These parameters must be added manually, so preserve them. - if ('noinput' in oldParams) { - params.push('noinput'); - } - if (shakaDemo.startTime_ != undefined) { - params.push('startTime=' + shakaDemo.startTime_); - } - - // Store values for drm configuration. - let videoRobustness = - document.getElementById('drmSettingsVideoRobustness').value; - if (videoRobustness) { - params.push('videoRobustness=' + videoRobustness); - } - - let audioRobustness = - document.getElementById('drmSettingsAudioRobustness').value; - if (audioRobustness) { - params.push('audioRobustness=' + audioRobustness); - } - - // These parameters must be added manually, so preserve them. - // These are only used by the loader in load.js to decide which version of - // the library to load. - let buildType = 'uncompiled'; - let strippedHash = '#' + params.join(';'); - if ('build' in oldParams) { - params.push('build=' + oldParams['build']); - buildType = oldParams['build']; - } else if ('compiled' in oldParams) { - params.push('build=compiled'); - buildType = 'compiled'; - } - - // Make the build links smart enough to preserve the app state while changing - // the build type. - (['compiled', 'debug_compiled', 'uncompiled']).forEach(function(type) { - let elem = document.getElementById(type + '_link'); - elem.href = '#'; - if (buildType == type) { - elem.classList.add('disabled_link'); - elem.removeAttribute('href'); - elem.title = 'currently selected'; - } else { - elem.classList.remove('disabled_link'); - elem.onclick = function() { - location.hash = strippedHash + ';build=' + type; - location.reload(); - return false; - }; - } - }); - - // Check if uncompiled mode is supported. This function is provided by the - // bootstrapping system in load.js. - if (!window['shakaUncompiledModeSupported']()) { - let uncompiledLink = document.getElementById('uncompiled_link'); - uncompiledLink.classList.add('disabled_link'); - uncompiledLink.removeAttribute('href'); - uncompiledLink.title = 'requires a newer browser'; - uncompiledLink.onclick = null; - } - - let newHash = '#' + params.join(';'); - if (newHash != location.hash) { - // We want to suppress hashchange events triggered here. We only want to - // respond to hashchange events initiated by the user in the URL bar. - shakaDemo.suppressHashChangeEvent_ = true; - location.hash = newHash; - } - - // If search is already blank, setting it triggers a navigation and reloads - // the page. Only blank out the search if we have just upgraded from search - // parameters to hash parameters. - if (location.search) { - location.search = ''; - } -}; - - -/** - * @param {string} locale - * @private - */ -shakaDemo.loadLocale_ = async function(locale) { - const url = '../ui/locales/' + locale + '.json'; - const response = await fetch(url); - if (!response.ok) { - console.warn('Unable to load locale', locale); - return; - } - - const obj = await response.json(); - const map = new Map(); - for (let key in obj) { - map.set(key, obj[key]); - } - - const localization = shakaDemo.controls_.getLocalization(); - localization.insert(locale, map); -}; - - -/** - * @param {!Event} event - * @private - */ -shakaDemo.onErrorEvent_ = function(event) { - let error = event.detail; - shakaDemo.onError_(error); -}; - - -/** - * @param {!shaka.util.Error} error - * @private - */ -shakaDemo.onError_ = function(error) { - console.error('Player error', error); - let link = document.getElementById('errorDisplayLink'); - - // Don't let less serious or equally serious errors replace what is already - // shown. The first error is usually the most important one, and the others - // may distract us in bug reports. - - // If this is an unexpected non-shaka.util.Error, severity is null. - if (error.severity == null) { - // Treat these as the most severe, since they should not happen. - error.severity = /** @type {shaka.util.Error.Severity} */(99); - } - - // Always show the new error if: - // 1. there is no error showing currently - // 2. the new error is more severe than the old one - if (link.severity == null || - error.severity > link.severity) { - let message = error.message || ('Error code ' + error.code); - if (error.code) { - link.href = '../docs/api/shaka.util.Error.html#value:' + error.code; - } else { - link.href = ''; - } - link.textContent = message; - // By converting severity == null to 99, non-shaka errors will not be - // replaced by any subsequent error. - link.severity = error.severity || 99; - // Make the link clickable only if we have an error code. - link.style.pointerEvents = error.code ? 'auto' : 'none'; - document.getElementById('errorDisplay').style.display = 'block'; - } -}; - - -/** - * Closes the error bar. - */ -shakaDemo.closeError = function() { - document.getElementById('errorDisplay').style.display = 'none'; - let link = document.getElementById('errorDisplayLink'); - link.href = ''; - link.textContent = ''; - link.severity = null; -}; - - -document.addEventListener('shaka-ui-loaded', shakaDemo.init); +document.addEventListener('shaka-ui-loaded', () => shakaDemoMain.init()); +document.addEventListener('shaka-ui-load-failed', + () => shakaDemoMain.initFailed()); diff --git a/demo/offline_section.js b/demo/offline_section.js deleted file mode 100644 index 321b4b995..000000000 --- a/demo/offline_section.js +++ /dev/null @@ -1,293 +0,0 @@ -/** - * @license - * Copyright 2016 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/** - * @fileoverview Shaka Player demo, main section. - * - * @suppress {visibility} to work around compiler errors until we can - * refactor the demo into classes that talk via public method. TODO - */ - - -/** @suppress {duplicate} */ -var shakaDemo = shakaDemo || {}; // eslint-disable-line no-var - - -/** @private {?HTMLOptGroupElement} */ -shakaDemo.offlineOptGroup_ = null; - - -/** @private {boolean} */ -shakaDemo.offlineOperationInProgress_ = false; - - -/** - * @param {boolean} canHide True to hide the progress value if there isn't an - * operation going. - * @private - */ -shakaDemo.updateButtons_ = function(canHide) { - let assetList = document.getElementById('assetList'); - let inProgress = shakaDemo.offlineOperationInProgress_; - - document.getElementById('progressDiv').style.display = - canHide && !inProgress ? 'none' : 'block'; - - let option = assetList.options[assetList.selectedIndex]; - let storedContent = option.storedContent; - let supportsPersistentStateForAsset = true; - let supportsPersistentState = true; - let supportsOfflineStorage = true; - // Persistent state support only matters if the asset has DRM. - if (option.asset && option.asset.drm && option.asset.drm.length) { - supportsPersistentStateForAsset = option.asset.drm.some(function(drm) { - return shakaDemo.support_.drm[drm] && - shakaDemo.support_.drm[drm].persistentState; - }); - supportsPersistentState = - Object.keys(shakaDemo.support_.drm).some((drm) => { - return shakaDemo.support_.drm[drm] && - shakaDemo.support_.drm[drm].persistentState; - }); - } - - // Note that offline assets are synthetic and do not have a "features" field. - if (option.asset && option.asset.features) { - if (!option.asset.features.includes(shakaAssets.Feature.OFFLINE)) { - // For whatever reason, this asset can't handle offline storage. - supportsOfflineStorage = false; - } - } - - // Only show when the custom asset option is selected. - document.getElementById('offlineNameDiv').style.display = - option.asset ? 'none' : 'block'; - - let button = document.getElementById('storeDeleteButton'); - button.disabled = false; - button.textContent = storedContent ? 'Delete' : 'Store'; - let helpText = document.getElementById('storeDeleteHelpText'); - if (inProgress) { - button.disabled = true; - helpText.textContent = 'Operation is in progress...'; - } else if (!supportsPersistentState) { - button.disabled = true; - helpText.textContent = 'This browser does not support persistent licenses.'; - } else if (!supportsPersistentStateForAsset) { - button.disabled = true; - helpText.textContent = 'This browser does not support persistent ' + - 'licenses for any DRM system in this asset.'; - } else if (option.isStored) { - button.disabled = true; - helpText.textContent = 'The asset is stored offline. ' + - 'Checkout the "Offline" section in the "Asset" list'; - } else if (!supportsOfflineStorage) { - button.disabled = true; - helpText.textContent = 'The asset does not support offline storage.'; - } else { - helpText.textContent = ''; - } -}; - - -/** @private */ -shakaDemo.setupOffline_ = function() { - document.getElementById('storeDeleteButton') - .addEventListener('click', shakaDemo.storeDeleteAsset_); - document.getElementById('assetList') - .addEventListener('change', shakaDemo.updateButtons_.bind(null, true)); - shakaDemo.updateButtons_(true); -}; - - -/** - * @return {!Promise} - * @private - */ -shakaDemo.setupOfflineAssets_ = function() { - const Storage = shaka.offline.Storage; - if (!Storage.support()) { - let section = document.getElementById('offlineSection'); - section.style.display = 'none'; - return Promise.resolve(); - } - - /** @type {!HTMLOptGroupElement} */ - let group; - let assetList = document.getElementById('assetList'); - if (!shakaDemo.offlineOptGroup_) { - group = - /** @type {!HTMLOptGroupElement} */ ( - document.createElement('optgroup')); - shakaDemo.offlineOptGroup_ = group; - group.label = 'Offline'; - assetList.appendChild(group); - } else { - group = shakaDemo.offlineOptGroup_; - } - - let db = new Storage(/** @type {!shaka.Player} */ (shakaDemo.localPlayer_)); - return db.list().then(function(storedContents) { - storedContents.forEach(function(storedContent) { - for (let i = 0; i < assetList.options.length; i++) { - let option = assetList.options[i]; - if (option.asset && - option.asset.manifestUri == storedContent.originalManifestUri) { - option.isStored = true; - break; - } - } - let asset = {manifestUri: storedContent.offlineUri}; - - let option = document.createElement('option'); - option.textContent = - storedContent.appMetadata ? storedContent.appMetadata.name : ''; - option.asset = asset; - option.storedContent = storedContent; - group.appendChild(option); - }); - - shakaDemo.updateButtons_(true); - return db.destroy(); - }); -}; - - -/** @private */ -shakaDemo.storeDeleteAsset_ = function() { - shakaDemo.closeError(); - shakaDemo.offlineOperationInProgress_ = true; - shakaDemo.updateButtons_(false); - - let assetList = document.getElementById('assetList'); - let option = assetList.options[assetList.selectedIndex]; - - // This will use the configuration from the player, so we need to set all - // our configurations on the player and not here. - let storage = new shaka.offline.Storage( - /** @type {!shaka.Player} */ (shakaDemo.localPlayer_)); - - - // Clear the progress display so that we will start at zero. - let percent = 0; - let progress = document.getElementById('progress'); - progress.textContent = (percent * 100).toFixed(2); - - let p; - if (option.storedContent) { - let offlineUri = option.storedContent.offlineUri; - let originalManifestUri = option.storedContent.originalManifestUri; - - // If this is a stored demo asset, we'll need to configure the player with - // license server authentication so we can delete the offline license. - for (let i = 0; i < shakaAssets.testAssets.length; i++) { - let originalAsset = shakaAssets.testAssets[i]; - if (originalManifestUri == originalAsset.manifestUri) { - shakaDemo.preparePlayer_(originalAsset); - break; - } - } - - p = storage.remove(offlineUri).then(function() { - for (let i = 0; i < assetList.options.length; i++) { - let option = assetList.options[i]; - if (option.asset && option.asset.manifestUri == originalManifestUri) { - option.isStored = false; - } - } - return shakaDemo.refreshAssetList_(); - }); - } else { - let configureCertificate = Promise.resolve(); - - let asset = shakaDemo.preparePlayer_(option.asset); - - if (asset.certificateUri) { - configureCertificate = shakaDemo.requestCertificate_(asset.certificateUri) - .then(shakaDemo.configureCertificate_); - } - - p = configureCertificate.then(function() { - let nameField = document.getElementById('offlineName').value; - let assetName = asset.name ? '[OFFLINE] ' + asset.name : null; - let metadata = {name: assetName || nameField || asset.manifestUri}; - return storage.store(asset.manifestUri, metadata).then(function() { - if (option.asset) { - option.isStored = true; - } - return shakaDemo.refreshAssetList_().then(function() { - // Auto-select offline copy of asset after storing. - let group = shakaDemo.offlineOptGroup_; - for (let i = 0; i < group.childNodes.length; i++) { - let option = group.childNodes[i]; - if (option.textContent == assetName) { - assetList.selectedIndex = option.index; - } - } - }); - }); - }); - } - - p.catch(function(reason) { - let error = /** @type {!shaka.util.Error} */(reason); - shakaDemo.onError_(error); - }).then(function() { - shakaDemo.offlineOperationInProgress_ = false; - shakaDemo.updateButtons_(true /* canHide */); - return storage.destroy(); - }); -}; - - -/** - * @return {!Promise} - * @private - */ -shakaDemo.refreshAssetList_ = function() { - // Remove all child elements. - let group = shakaDemo.offlineOptGroup_; - while (group.firstChild) { - group.removeChild(group.firstChild); - } - - return shakaDemo.setupOfflineAssets_(); -}; - - -/** - * @param {boolean} connected - * @private - */ -shakaDemo.onCastStatusChange_ = function(connected) { - if (!shakaDemo.offlineOptGroup_) { - // No offline support. - return; - } - - // When we are casting, offline assets become unavailable. - shakaDemo.offlineOptGroup_.disabled = connected; - - if (connected) { - let assetList = document.getElementById('assetList'); - let option = assetList.options[assetList.selectedIndex]; - if (option.storedContent) { - // This is an offline asset. Select something else. - assetList.selectedIndex = 0; - } - } -}; diff --git a/demo/search.js b/demo/search.js new file mode 100644 index 000000000..f11236d4e --- /dev/null +++ b/demo/search.js @@ -0,0 +1,337 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +/** @type {?ShakaDemoSearch} */ +let shakaDemoSearch; + + +/** + * Shaka Player demo, feature discovery page layout. + */ +class ShakaDemoSearch { + /** + * Register the page configuration. + */ + static init() { + const container = shakaDemoMain.addNavButton('search'); + shakaDemoSearch = new ShakaDemoSearch(container); + } + + /** @param {!Element} container */ + constructor(container) { + /** @private {!Array.} */ + this.desiredFeatures_ = []; + + /** @private {?shakaAssets.Source} */ + this.desiredSource_; + + /** @private {?shakaAssets.KeySystem} */ + this.desiredDRM_; + + this.makeSearchDiv_(container); + + /** @private {!Array.} */ + this.assetCards_ = []; + + this.resultsDiv_ = document.createElement('div'); + container.appendChild(this.resultsDiv_); + + this.remakeResultsDiv_(); + + document.addEventListener('shaka-main-selected-asset-changed', () => { + this.updateSelected_(); + }); + document.addEventListener('shaka-main-offline-progress', () => { + this.updateOfflineProgress_(); + }); + document.addEventListener('shaka-main-offline-changed', () => { + this.remakeResultsDiv_(); + }); + } + + /** + * @param {!ShakaDemoAssetInfo} asset + * @return {!AssetCard} + * @private + */ + createAssetCardFor_(asset) { + const resultsDiv = this.resultsDiv_; + const card = new AssetCard(resultsDiv, asset, /* isFeatured = */ false); + const unsupportedReason = shakaDemoMain.getAssetUnsupportedReason( + asset, /* needOffline= */ false); + if (unsupportedReason) { + card.markAsUnsupported(unsupportedReason); + } else { + card.addButton('Play', () => { + shakaDemoMain.loadAsset(asset); + this.updateSelected_(); + }); + card.addStoreButton(); + } + return card; + } + + /** + * Updates progress bars on asset cards. + * @private + */ + updateOfflineProgress_() { + for (const card of this.assetCards_) { + card.updateProgress(); + } + } + + /** + * Updates which asset card is selected. + * @private + */ + updateSelected_() { + for (const card of this.assetCards_) { + card.selectByAsset(shakaDemoMain.selectedAsset); + } + } + + /** @private */ + remakeResultsDiv_() { + shaka.ui.Utils.removeAllChildren(this.resultsDiv_); + + const assets = this.searchResults_(); + this.assetCards_ = assets.map((asset) => this.createAssetCardFor_(asset)); + this.updateSelected_(); + } + + /** + * @param {!ShakaDemoSearch.SearchTerm} term + * @param {ShakaDemoSearch.TermType} type + * @param {!Array.} others + * @private + */ + addDesiredTerm_(term, type, others) { + switch (type) { + case ShakaDemoSearch.TermType.DRM: + this.desiredDRM_ = /** @type {shakaAssets.KeySystem} */ (term); + break; + case ShakaDemoSearch.TermType.SOURCE: + this.desiredSource_ = /** @type {shakaAssets.Source} */ (term); + break; + case ShakaDemoSearch.TermType.FEATURE: + // Only this term should be in the desired features. + for (let term of others) { + const index = this.desiredFeatures_.indexOf( + /** @type {shakaAssets.Feature} */ (term)); + if (index != -1) { + this.desiredFeatures_.splice(index, 1); + } + } + this.desiredFeatures_.push(/** @type {shakaAssets.Feature} */ (term)); + break; + } + } + + /** + * @param {!ShakaDemoSearch.SearchTerm} term + * @param {ShakaDemoSearch.TermType} type + * @private + */ + removeDesiredTerm_(term, type) { + let index; + switch (type) { + case ShakaDemoSearch.TermType.DRM: + this.desiredDRM_ = null; + break; + case ShakaDemoSearch.TermType.SOURCE: + this.desiredSource_ = null; + break; + case ShakaDemoSearch.TermType.FEATURE: + index = this.desiredFeatures_.indexOf( + /** @type {shakaAssets.Feature} */ (term)); + if (index != -1) { + this.desiredFeatures_.splice(index, 1); + } + break; + } + } + + /** + * Creates an input for a single search term. + * @param {!ShakaDemoInputContainer} searchContainer + * @param {!ShakaDemoSearch.SearchTerm} choice + * The term this represents. + * @param {ShakaDemoSearch.TermType} type + * The type of term that this term is. + * @private + */ + makeBooleanInput_(searchContainer, choice, type) { + // Give the container a significant amount of right padding, to make + // it clearer which toggle corresponds to which label. + searchContainer.addRow(choice, null, 'significant-right-padding'); + const onChange = (input) => { + if (input.checked) { + this.addDesiredTerm_(choice, type, [choice]); + } else { + this.removeDesiredTerm_(choice, type); + } + this.remakeResultsDiv_(); + // Update the componentHandler, to account for any new MDL elements + // added. Notably, tooltips. + componentHandler.upgradeDom(); + }; + // eslint-disable-next-line no-new + new ShakaDemoBoolInput(searchContainer, choice, onChange); + } + + /** + * Creates an input for a group of related but mutually-exclusive search + * terms. + * @param {!ShakaDemoInputContainer} searchContainer + * @param {string} name + * @param {!Array.} choices + * An array of the terms in this term group. + * @param {ShakaDemoSearch.TermType} type + * The type of term that this term group contains. All of the + * terms in the "choices" array must be of this type. + * @private + */ + makeSelectInput_(searchContainer, name, choices, type) { + searchContainer.addRow(null, null); + const nullOption = 'Unspecified'; + const valuesObject = {}; + for (let term of choices) { + if (type == 'DRM') { + // The internal names of the keysystems aren't very readable, so use a + // common name instead. + // However, as we are basing this off of the key name, we have to remove + // any underscores, so that the user isn't presented with a button + // labeled CLEAR_KEY. + for (const key in shakaAssets.KeySystem) { + if (shakaAssets.KeySystem[key] == term) { + // TODO: It'd be better to have some table of "translations", + // instead of making a readable name here with string operations. + valuesObject[term] = key.split('_').map((name) => { + // Return everything but first character to lower-case. + return name[0] + name.substr(1).toLowerCase(); + }).join(' '); + break; + } + } + } else { + valuesObject[term] = term; + } + } + valuesObject[nullOption] = nullOption; + let lastValue = nullOption; + const onChange = (input) => { + if (input.value != nullOption) { + this.addDesiredTerm_(input.value, type, choices); + } else { + this.removeDesiredTerm_(lastValue, type); + } + lastValue = input.value; + this.remakeResultsDiv_(); + // Update the componentHandler, to account for any new MDL elements added. + // Notably, tooltips. + componentHandler.upgradeDom(); + }; + const input = new ShakaDemoSelectInput( + searchContainer, name, onChange, valuesObject); + input.extra().textContent = name; + input.input().value = nullOption; + } + + /** + * @param {!Element} container + * @private + */ + makeSearchDiv_(container) { + const Feature = shakaAssets.Feature; + const FEATURE = ShakaDemoSearch.TermType.FEATURE; + const DRM = ShakaDemoSearch.TermType.DRM; + const SOURCE = ShakaDemoSearch.TermType.SOURCE; + + // Core term inputs. + const coreContainer = new ShakaDemoInputContainer( + container, /* headerText = */ null, ShakaDemoInputContainer.Style.FLEX, + /* docLink = */ null); + this.makeSelectInput_(coreContainer, 'Manifest', + [Feature.DASH, Feature.HLS], FEATURE); + this.makeSelectInput_(coreContainer, 'Container', + [Feature.MP4, Feature.MP2TS, Feature.WEBM], FEATURE); + this.makeSelectInput_(coreContainer, 'DRM', + Object.values(shakaAssets.KeySystem), DRM); + this.makeSelectInput_(coreContainer, 'Source', + Object.values(shakaAssets.Source).filter((term) => term != 'Custom'), + SOURCE); + + // Special terms. + const containerStyle = ShakaDemoInputContainer.Style.FLEX; + const specialContainer = new ShakaDemoInputContainer( + container, /* headerText = */ null, containerStyle, + /* docLink = */ null); + this.makeBooleanInput_(specialContainer, Feature.LIVE, FEATURE); + this.makeBooleanInput_(specialContainer, Feature.HIGH_DEFINITION, FEATURE); + this.makeBooleanInput_(specialContainer, Feature.XLINK, FEATURE); + this.makeBooleanInput_(specialContainer, Feature.SUBTITLES, FEATURE); + this.makeBooleanInput_(specialContainer, Feature.TRICK_MODE, FEATURE); + this.makeBooleanInput_(specialContainer, Feature.SURROUND, FEATURE); + this.makeBooleanInput_(specialContainer, Feature.OFFLINE, FEATURE); + this.makeBooleanInput_(specialContainer, Feature.STORED, FEATURE); + } + + /** + * @return {!Array.} + * @private + */ + searchResults_() { + return shakaAssets.testAssets.filter((asset) => { + if (asset.disabled) { + return false; + } + if (this.desiredDRM_ && !asset.drm.includes(this.desiredDRM_)) { + return false; + } + if (this.desiredSource_ && asset.source != this.desiredSource_) { + return false; + } + for (let feature of this.desiredFeatures_) { + if (feature == shakaAssets.Feature.STORED) { + if (!asset.isStored()) { + return false; + } + } else if (!asset.features.includes(feature)) { + return false; + } + } + return true; + }); + } +} + + +/** @typedef {shakaAssets.Feature|shakaAssets.Source} */ +ShakaDemoSearch.SearchTerm; + + +/** @enum {string} */ +ShakaDemoSearch.TermType = { + FEATURE: 'Feature', + DRM: 'DRM', + SOURCE: 'Source', +}; + + +document.addEventListener('shaka-main-loaded', ShakaDemoSearch.init); diff --git a/demo/service_worker.js b/demo/service_worker.js index 2499385c5..7439fb22b 100644 --- a/demo/service_worker.js +++ b/demo/service_worker.js @@ -22,12 +22,12 @@ /** * The name of the cache for this version of the application. - * This should be updated when old, unneded application resources could be + * This should be updated when old, unneeded application resources could be * cleaned up by a newer version of the application. * * @const {string} */ -const CACHE_NAME = 'shaka-player-v2'; +const CACHE_NAME = 'shaka-player-v2.5+'; /** @@ -62,8 +62,7 @@ const CRITICAL_RESOURCES = [ '.', // This resolves to the page. 'index.html', // Another way to access the page. 'app_manifest.json', - - 'demo.css', + 'shaka_logo_trans.png', // These CSS files will reference Web Fonts which will be cached on sight // thanks to CACHEABLE_URL_PREFIXES below. This means we don't have to @@ -77,6 +76,11 @@ const CRITICAL_RESOURCES = [ '../dist/shaka-player.ui.js', '../dist/demo.compiled.js', '../dist/controls.css', + '../dist/demo.css', + + // These files are required for the demo to include MDL. + 'https://code.getmdl.io/1.3.0/material.indigo-blue.min.css', + 'https://code.getmdl.io/1.3.0/material.min.js', ]; @@ -117,6 +121,8 @@ const CACHEABLE_URL_PREFIXES = [ // Google Web Fonts should be cached when first seen, without being explicitly // listed, and should be preferred from cache for speed. 'https://fonts.gstatic.com/', + // Same goes for asset icons. + 'https://storage.googleapis.com/shaka-asset-icons/', ]; diff --git a/demo/shaka_logo_trans.png b/demo/shaka_logo_trans.png new file mode 100644 index 000000000..9094f53b8 Binary files /dev/null and b/demo/shaka_logo_trans.png differ diff --git a/demo/tooltip.js b/demo/tooltip.js new file mode 100644 index 000000000..ccd53b748 --- /dev/null +++ b/demo/tooltip.js @@ -0,0 +1,49 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Creates and contains the MDL elements of a tooltip. + */ +class ShakaDemoTooltips { + /** + * @param {!Element} parentDiv + * @param {!Element} labeledElement + * @param {string} message + */ + static make(parentDiv, labeledElement, message) { + labeledElement.id = ShakaDemoTooltips.generateNewId_(); + const tooltip = document.createElement('div'); + tooltip.classList.add('mdl-tooltip'); + tooltip.classList.add('mdl-tooltip--large'); + tooltip.setAttribute('for', labeledElement.id); + tooltip.textContent = message; + parentDiv.appendChild(tooltip); + } + + /** + * @return {string} + * @private + */ + static generateNewId_() { + const idNumber = ShakaDemoTooltips.lastId_; + ShakaDemoTooltips.lastId_ += 1; + return 'tooltip-labeled-' + idNumber; + } +} + +/** @private {number} */ +ShakaDemoTooltips.lastId_ = 0; diff --git a/docs/design/architecture.md b/docs/design/architecture.md index a69270539..7827d8549 100644 --- a/docs/design/architecture.md +++ b/docs/design/architecture.md @@ -9,3 +9,5 @@ ![Shaka offline diagram](offline.gv.png) ![PresentationTimeline diagram](timeline.svg) + +![Demo page architecture](newdemo.gv.png) diff --git a/docs/design/newdemo.gv b/docs/design/newdemo.gv new file mode 100644 index 000000000..943771dfe --- /dev/null +++ b/docs/design/newdemo.gv @@ -0,0 +1,83 @@ +# Generate png with: dot -Tpng -O newdemo.gv + +digraph new_demo { + label = "Shaka Player New Demo Page Architecture Diagram" + + node [ style = filled ] + + "ShakaDemoMain" [ shape = oval ] + + "Shaka Player" [ shape = Mdiamond ] + + subgraph cluster_new_demo_panels { + label = "Panels" + style = filled + color = aliceblue + shape = rectangle + + "ShakaDemoConfig" [ shape = oval ] + "ShakaDemoCustom" [ shape = oval ] + "ShakaDemoSearch" [ shape = oval ] + "ShakaDemoFront" [ shape = oval ] + } + + subgraph cluster_new_demo_common { + label = "Common" + style = filled + color = lavenderblush + shape = rectangle + + "ShakaDemoUtils" [ shape = Msquare ] + "ShakaAssets" [ shape = Msquare ] + "ShakaDemoAssetInfo" [ shape = Msquare ] + } + + subgraph cluster_new_demo_utilities { + label = "Misc Utilities" + style = filled + color = palegoldenrod + shape = rectangle + + "ShakaDemoInput" [ shape = Msquare ] + "ShakaDemoInputContainer" [ shape = Msquare ] + "AssetCard" [ shape = mSquare ] + } + + # Dependencies on Shaka Player + ShakaDemoMain -> "Shaka Player" + + # Dependencies on ShakaDemoMain + ShakaDemoConfig -> ShakaDemoMain + ShakaDemoCustom -> ShakaDemoMain + ShakaDemoSearch -> ShakaDemoMain + ShakaDemoFront -> ShakaDemoMain + + # Dependencies on ShakaDemoUtils + ShakaDemoMain -> ShakaDemoUtils [ style = dotted ] + + # Dependencies on ShakaAssets + ShakaDemoMain -> ShakaAssets [ style = dotted ] + ShakaDemoSearch -> ShakaAssets [ style = dotted ] + ShakaDemoFront -> ShakaAssets [ style = dotted ] + + # Dependencies on ShakaDemoAssetInfo + ShakaAssets -> ShakaDemoAssetInfo [ style = dotted ] + AssetCard -> ShakaDemoAssetInfo [ style = dotted ] + ShakaDemoCustom -> ShakaDemoAssetInfo [ style = dotted ] + + # Dependencies on ShakaDemoInput + ShakaDemoCustom -> ShakaDemoInput [ style = dotted ] + ShakaDemoConfig -> ShakaDemoInput [ style = dotted ] + ShakaDemoSearch -> ShakaDemoInput [ style = dotted ] + ShakaDemoInputContainer -> ShakaDemoInput + + # Dependencies on ShakaDemoInputContainer + ShakaDemoCustom -> ShakaDemoInputContainer [ style = dotted ] + ShakaDemoConfig -> ShakaDemoInputContainer [ style = dotted ] + ShakaDemoSearch -> ShakaDemoInputContainer [ style = dotted ] + + # Dependencies on AssetCard + ShakaDemoSearch -> AssetCard [ style = dotted ] + ShakaDemoCustom -> AssetCard [ style = dotted ] + ShakaDemoFront -> AssetCard [ style = dotted ] +} diff --git a/docs/design/newdemo.gv.png b/docs/design/newdemo.gv.png new file mode 100644 index 000000000..10b01658b Binary files /dev/null and b/docs/design/newdemo.gv.png differ diff --git a/docs/tutorials/architecture.md b/docs/tutorials/architecture.md index 0083766aa..f389817cc 100644 --- a/docs/tutorials/architecture.md +++ b/docs/tutorials/architecture.md @@ -25,3 +25,8 @@ alt="PresentationTimeline diagram" style="max-width: 100%"> +Demo page architecture + diff --git a/docs/tutorials/ui.md b/docs/tutorials/ui.md index 3dcf24a11..f7670972a 100644 --- a/docs/tutorials/ui.md +++ b/docs/tutorials/ui.md @@ -79,8 +79,15 @@ function onUIErrorEvent(errorEvent) { // Handle UI error } +function initFailed() { + // Handle the failure to load +} + // Listen to the custom shaka-ui-loaded event, to wait until the UI is loaded. document.addEventListener('shaka-ui-loaded', init); +// Listen to the custom shaka-ui-load-failed event, in case Shaka Player fails +// to load (e.g. due to lack of browser support). +document.addEventListener('shaka-ui-load-failed, initFailed); ``` diff --git a/externs/awesomplete.js b/externs/awesomplete.js new file mode 100644 index 000000000..b95a48ad2 --- /dev/null +++ b/externs/awesomplete.js @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Externs for Awesomplete methods. + * @externs + */ + + +/** + * @param {!Element} input + * @constructor + */ +const Awesomplete = function(input) {}; + +/** @type {!Array.} */ +Awesomplete.prototype.list; + +/** @type {number} */ +Awesomplete.prototype.minChars; + +Awesomplete.prototype.evaluate = function() {}; diff --git a/externs/dialog_polyfill.js b/externs/dialog_polyfill.js new file mode 100644 index 000000000..e77a8db1c --- /dev/null +++ b/externs/dialog_polyfill.js @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Externs for dialog-polyfill.js methods. + * @externs + */ + + +/** @const */ +const dialogPolyfill = {}; + + +/** + * @param {!Element} dialog + * @const + */ +dialogPolyfill.registerDialog = function(dialog) {}; + diff --git a/externs/mdl.js b/externs/mdl.js new file mode 100644 index 000000000..69c93d5a3 --- /dev/null +++ b/externs/mdl.js @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2016 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * @fileoverview Externs for MDL methods. + * @externs + */ + + +/** @const */ +const componentHandler = {}; + +/** @const */ +componentHandler.upgradeDom = function() {}; + + +/** @constructor */ +const MaterialLayout = function() {}; + +MaterialLayout.prototype.toggleDrawer = function() {}; + +/** @const {?MaterialLayout} */ +Element.prototype.MaterialLayout; diff --git a/karma.conf.js b/karma.conf.js index 336729f20..98758d78c 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -99,6 +99,7 @@ module.exports = function(config) { 'test/test/util/*.js', // list of test assets next + 'demo/common/asset.js', 'demo/common/assets.js', // if --test-custom-asset *is not* present, we will add unit tests. diff --git a/lib/cast/cast_utils.js b/lib/cast/cast_utils.js index 3440b6793..251768235 100644 --- a/lib/cast/cast_utils.js +++ b/lib/cast/cast_utils.js @@ -128,6 +128,8 @@ shaka.cast.CastUtils.PlayerGetterMethods = { // TODO(vaage): Remove |getManifestUri| references in v2.6. // NOTE: The 'getManifestUri' property is not proxied, as CastProxy has a // handler for it. + // NOTE: The 'getManifestParserFactory' property is not proxied, as it would + // not serialize. 'getPlaybackRate': 2, 'getTextLanguages': 2, 'getTextLanguagesAndRoles': 2, diff --git a/lib/player.js b/lib/player.js index efcdf0a4e..f6dd3884a 100644 --- a/lib/player.js +++ b/lib/player.js @@ -3571,6 +3571,18 @@ shaka.Player.prototype.getManifest = function() { }; +/** + * Get the type of manifest parser that the player is using. If the player has + * not loaded any content, this will return |null|. + * + * @return {?shaka.extern.ManifestParser.Factory} + * @export + */ +shaka.Player.prototype.getManifestParserFactory = function() { + return this.parser_ ? this.parser_.constructor : null; +}; + + /** * @param {shaka.extern.Period} period * @param {shaka.extern.Variant} variant diff --git a/package.json b/package.json index 67c1bba38..c6719114e 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ ], "devDependencies": { "array-includes": "~3.0.3", + "awesomplete": "~1.1.1", "babel-core": "^6.26.0", "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.6.1", "cajon": "^0.4.4", + "dialog-polyfill": "^0.4.10", "es6-promise-polyfill": "^1.2.0", "es6-shim": "~0.35.3", "eslint": "^4.14.0", diff --git a/test/cast/cast_utils_unit.js b/test/cast/cast_utils_unit.js index 3cd487f77..b7de28490 100644 --- a/test/cast/cast_utils_unit.js +++ b/test/cast/cast_utils_unit.js @@ -31,6 +31,7 @@ describe('CastUtils', function() { 'getManifest', // Too large to proxy // TODO(vaage): Remove |getManifestUri| references in v2.6. 'getManifestUri', // Handled specially by CastProxy + 'getManifestParserFactory', // Would not serialize. // Test helper methods (not @export'd) 'createDrmEngine', diff --git a/test/player_external.js b/test/player_external.js index db279fe6c..9304b2955 100644 --- a/test/player_external.js +++ b/test/player_external.js @@ -80,6 +80,7 @@ describe('Player', () => { }); describe('plays', () => { + /** @param {!ShakaDemoAssetInfo} asset */ function createAssetTest(asset) { if (asset.disabled) return; @@ -211,14 +212,20 @@ describe('Player', () => { /** @type {Object} */ const licenseServers = getClientArg('testCustomLicenseServer'); const keySystems = Object.keys(licenseServers || {}); - const asset = { - source: 'command line', - name: 'custom', - manifestUri: testCustomAsset, - focus: true, - licenseServers: licenseServers, - drm: keySystems, - }; + const asset = new ShakaDemoAssetInfo( + /* name= */ 'custom', + /* iconUri= */ '', + /* manifestUri= */ testCustomAsset, + /* source= */ shakaAssets.Source.CUSTOM); + if (keySystems.length) { + for (let keySystem of keySystems) { + asset.addKeySystem(/** @type {!shakaAssets.KeySystem} */ (keySystem)); + const licenseServer = licenseServers[keySystem]; + if (licenseServer) { + asset.addLicenseServer(keySystem, licenseServer); + } + } + } createAssetTest(asset); } else { // No custom assets? Create a test for each asset in the demo asset list. diff --git a/test/test/externs/jasmine.js b/test/test/externs/jasmine.js index a554c3827..4a2e40293 100644 --- a/test/test/externs/jasmine.js +++ b/test/test/externs/jasmine.js @@ -80,8 +80,11 @@ var pending = function(message) {}; jasmine.Matchers.prototype.not; -/** @param {*} value */ -jasmine.Matchers.prototype.toBe = function(value) {}; +/** + * @param {*} value + * @param {string=} message + */ +jasmine.Matchers.prototype.toBe = function(value, message) {}; /** diff --git a/ui/language_utils.js b/ui/language_utils.js index 09178bfcb..334736daa 100644 --- a/ui/language_utils.js +++ b/ui/language_utils.js @@ -49,9 +49,7 @@ shaka.ui.LanguageUtils = class { langMenu, 'shaka-back-to-overflow-button'); // 2. Remove everything - while (langMenu.firstChild) { - langMenu.removeChild(langMenu.firstChild); - } + shaka.ui.Utils.removeAllChildren(langMenu); // 3. Add the backTo Menu button back langMenu.appendChild(backButton); diff --git a/ui/less/containers.less b/ui/less/containers.less index d6f5e2b9d..5736be2d9 100644 --- a/ui/less/containers.less +++ b/ui/less/containers.less @@ -56,8 +56,8 @@ * flips. CSS is just great like that. :-( */ .shaka-video { /* At the moment, nothing special is required here. - * Note that this should NOT be an overlay-child, as its size should dictate - * the size of the container, not vice-versa. */ + * Note that this should NOT be an overlay-child, as its size could dictate + * the size of the container for some applications. */ } /* A container for all controls, including the giant play button, seek bar, etc. diff --git a/ui/resolution_selection.js b/ui/resolution_selection.js index 040a69322..4e0aac64b 100644 --- a/ui/resolution_selection.js +++ b/ui/resolution_selection.js @@ -174,9 +174,7 @@ shaka.ui.ResolutionSelection = class extends shaka.ui.Element { this.resolutionMenu_, 'shaka-back-to-overflow-button'); // 2. Remove everything - while (this.resolutionMenu_.firstChild) { - this.resolutionMenu_.removeChild(this.resolutionMenu_.firstChild); - } + shaka.ui.Utils.removeAllChildren(this.resolutionMenu_); // 3. Add the backTo Menu button back this.resolutionMenu_.appendChild(backButton); diff --git a/ui/ui.js b/ui/ui.js index 960d80d46..ef85f56ad 100644 --- a/ui/ui.js +++ b/ui/ui.js @@ -192,10 +192,9 @@ shaka.ui.Overlay.scanPageForShakaElements_ = function() { 'Please see https://tinyurl.com/y7s4j9tr for the list of ' + 'supported browsers.'); - // Although this has failed, fire the "loaded" event. This will let apps - // get on with the business of startup, check isBrowserSupported() - // themselves, and show an appropriate error message at the app level. - shaka.ui.Overlay.dispatchLoadedEvent_(); + // After scanning the page for elements, fire a special "loaded" event for + // when the load fails. This will allow the page to react to the failure. + shaka.ui.Overlay.dispatchLoadedEvent_('shaka-ui-load-failed'); return; } @@ -290,16 +289,19 @@ shaka.ui.Overlay.scanPageForShakaElements_ = function() { // After scanning the page for elements, fire the "loaded" event. This will // let apps know they can use the UI library programmatically now, even if // they didn't have any Shaka-related elements declared in their HTML. - shaka.ui.Overlay.dispatchLoadedEvent_(); + shaka.ui.Overlay.dispatchLoadedEvent_('shaka-ui-loaded'); }; -/** @private */ -shaka.ui.Overlay.dispatchLoadedEvent_ = function() { +/** + * @param {string} eventName + * @private + */ +shaka.ui.Overlay.dispatchLoadedEvent_ = function(eventName) { // "Event" is not constructable on IE, so we use this CustomEvent pattern. const uiLoadedEvent = /** @type {!CustomEvent} */( document.createEvent('CustomEvent')); - uiLoadedEvent.initCustomEvent('shaka-ui-loaded', false, false, null); + uiLoadedEvent.initCustomEvent(eventName, false, false, null); document.dispatchEvent(uiLoadedEvent); }; diff --git a/ui/ui_utils.js b/ui/ui_utils.js index 4984cee80..d3dd85cbc 100644 --- a/ui/ui_utils.js +++ b/ui/ui_utils.js @@ -110,3 +110,13 @@ shaka.ui.Utils.setDisplay = function(element, display) { } }; +/** + * Remove all of the child nodes of an elements. + * @param {!Element} element + * @export + */ +shaka.ui.Utils.removeAllChildren = function(element) { + while (element.firstChild) { + element.removeChild(element.firstChild); + } +};