From efc2ed3df13b011dfbbd4f9065ae15f8fdb33b55 Mon Sep 17 00:00:00 2001 From: Theodore Abshire Date: Tue, 23 Apr 2019 15:12:44 -0700 Subject: [PATCH] Added new demo page. This is a complete replacement for the old demo page, made to be more modern-looking and easier to maintain. It contains new features such as remembering the URIs you provide for custom assets, and searching through the default assets by feature. This demo page is not quite ready for release yet, but it's getting close. Change-Id: Iad01d1fc02c3cd238d73b9b9e02dbb4301cb6f2a --- .gitignore | 1 + build/all.py | 22 +- build/check.py | 1 + build/conformance.textproto | 5 +- build/gendeps.py | 1 + demo/app_manifest.json | 2 +- demo/asset_card.js | 253 +++ demo/asset_section.js | 416 ---- demo/cast_receiver/index.html | 2 +- demo/cast_receiver/receiver_app.js | 6 +- demo/close_button.js | 67 + demo/common/asset.js | 383 ++++ demo/common/assets.js | 2150 +++++++------------ demo/config.js | 584 +++++ demo/configuration_section.js | 222 -- demo/custom.js | 396 ++++ demo/demo.css | 390 ---- demo/demo.less | 387 ++++ demo/{common => }/demo_utils.js | 102 +- demo/front.js | 137 ++ demo/icons/README.md | 4 + demo/icons/baseline-audiotrack-24px.svg | 1 + demo/icons/baseline-closed_caption-24px.svg | 1 + demo/icons/baseline-fast_forward-24px.svg | 1 + demo/icons/baseline-language-24px.svg | 1 + demo/icons/baseline-live_tv-24px.svg | 1 + demo/icons/baseline-subtitles-24px.svg | 1 + demo/icons/baseline-surround_sound-24px.svg | 1 + demo/icons/custom_box_with_text.svg | 1 + demo/icons/custom_clear_key.svg | 1 + demo/icons/custom_high_definition.svg | 1 + demo/icons/custom_playready.svg | 1 + demo/icons/custom_ultra_high_definition.svg | 1 + demo/icons/custom_widevine.svg | 1 + demo/index.html | 269 +-- demo/info_section.js | 356 --- demo/input.js | 238 ++ demo/input_container.js | 198 ++ demo/load.js | 18 +- demo/log_section.js | 138 -- demo/main.js | 1532 +++++++------ demo/offline_section.js | 293 --- demo/search.js | 337 +++ demo/service_worker.js | 14 +- demo/shaka_logo_trans.png | Bin 0 -> 2417 bytes demo/tooltip.js | 49 + docs/design/architecture.md | 2 + docs/design/newdemo.gv | 83 + docs/design/newdemo.gv.png | Bin 0 -> 120614 bytes docs/tutorials/architecture.md | 5 + docs/tutorials/ui.md | 7 + externs/awesomplete.js | 36 + externs/dialog_polyfill.js | 33 + externs/mdl.js | 37 + karma.conf.js | 1 + lib/cast/cast_utils.js | 2 + lib/player.js | 12 + package.json | 2 + test/cast/cast_utils_unit.js | 1 + test/player_external.js | 23 +- test/test/externs/jasmine.js | 7 +- ui/language_utils.js | 4 +- ui/less/containers.less | 4 +- ui/resolution_selection.js | 4 +- ui/ui.js | 18 +- ui/ui_utils.js | 10 + 66 files changed, 5085 insertions(+), 4192 deletions(-) create mode 100644 demo/asset_card.js delete mode 100644 demo/asset_section.js create mode 100644 demo/close_button.js create mode 100644 demo/common/asset.js create mode 100644 demo/config.js delete mode 100644 demo/configuration_section.js create mode 100644 demo/custom.js delete mode 100644 demo/demo.css create mode 100644 demo/demo.less rename demo/{common => }/demo_utils.js (56%) create mode 100644 demo/front.js create mode 100644 demo/icons/README.md create mode 100644 demo/icons/baseline-audiotrack-24px.svg create mode 100644 demo/icons/baseline-closed_caption-24px.svg create mode 100644 demo/icons/baseline-fast_forward-24px.svg create mode 100644 demo/icons/baseline-language-24px.svg create mode 100644 demo/icons/baseline-live_tv-24px.svg create mode 100644 demo/icons/baseline-subtitles-24px.svg create mode 100644 demo/icons/baseline-surround_sound-24px.svg create mode 100644 demo/icons/custom_box_with_text.svg create mode 100644 demo/icons/custom_clear_key.svg create mode 100644 demo/icons/custom_high_definition.svg create mode 100644 demo/icons/custom_playready.svg create mode 100644 demo/icons/custom_ultra_high_definition.svg create mode 100644 demo/icons/custom_widevine.svg delete mode 100644 demo/info_section.js create mode 100644 demo/input.js create mode 100644 demo/input_container.js delete mode 100644 demo/log_section.js delete mode 100644 demo/offline_section.js create mode 100644 demo/search.js create mode 100644 demo/shaka_logo_trans.png create mode 100644 demo/tooltip.js create mode 100644 docs/design/newdemo.gv create mode 100644 docs/design/newdemo.gv.png create mode 100644 externs/awesomplete.js create mode 100644 externs/dialog_polyfill.js create mode 100644 externs/mdl.js diff --git a/.gitignore b/.gitignore index 4e91cce7be..06fa1c1ea8 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 2fca8eee45..857adb2174 100755 --- a/build/all.py +++ b/build/all.py @@ -30,6 +30,17 @@ 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 8af3c1f318..15189c1c7d 100755 --- a/build/check.py +++ b/build/check.py @@ -118,6 +118,7 @@ def get(*path_components): 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 4d2f0a43a2..8937c1d8a6 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 07cb08ebcc..f1b9697ae6 100755 --- a/build/gendeps.py +++ b/build/gendeps.py @@ -31,6 +31,7 @@ '--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 320085d21d..f2d1e9c7ae 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 0000000000..4158339012 --- /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 3eed041b40..0000000000 --- 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 90aeb01070..f7bece32ca 100644 --- a/demo/cast_receiver/index.html +++ b/demo/cast_receiver/index.html @@ -47,8 +47,8 @@ // 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 e9132e9bfd..3c8d594ec4 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 0000000000..5cdeaa2c6c --- /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 0000000000..47d7de17ad --- /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 44f3abd73c..19088cbedc 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', - - SUBTITLES: 'subtitles', - CAPTIONS: 'captions', - SEGMENTED_TEXT: 'segmented text', + // Set if the asset has a special trick mode track, for rewinding effects. + TRICK_MODE: 'Special trick mode track', + XLINK: 'XLink', + + // 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', - - LIVE: 'live', + // 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', + + // 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 0000000000..306daa7734 --- /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 86f832f971..0000000000 --- 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 0000000000..04c8212d9e --- /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 4fceca486d..0000000000 --- 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 0000000000..8a88e63a01 --- /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 720359e26c..5c946fb738 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 0000000000..1768bc7e43 --- /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 0000000000..bd874147e4 --- /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 0000000000..ccd1f6dc95 --- /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 0000000000..f619b9bf56 --- /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 0000000000..fb10d81381 --- /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 0000000000..f5076217f2 --- /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 0000000000..19090b97d2 --- /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 0000000000..cffd520e8a --- /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 0000000000..d029cf9aa1 --- /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 0000000000..b2ca849e9e --- /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 0000000000..39a9d2cc69 --- /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 0000000000..08a0076368 --- /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 0000000000..ce8c6bbdfb --- /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 0000000000..7dffebe5c6 --- /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 0000000000..7d42bbdb0f --- /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 2dfb10d667..d4110c4319 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 640ee08581..0000000000 --- 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 0000000000..11aff232a8 --- /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 0000000000..0aa220a9ca --- /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 4aafa97c19..d6d58d8cf2 100644 --- a/demo/load.js +++ b/demo/load.js @@ -49,18 +49,22 @@ 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'; + link.href = baseUrl + href; + link.rel = linkRel; + + document.head.appendChild(link); + } + function loadCss(buildType) { if (buildType == 'uncompiled') { - link.rel = 'stylesheet/less'; - link.href = baseUrl + '../ui/controls.less'; + loadSpecificCss('../ui/controls.less', 'stylesheet/less'); + loadSpecificCss('../demo/demo.less', 'stylesheet/less'); } else { - link.rel = 'stylesheet'; - link.href = baseUrl + '../dist/controls.css'; + loadSpecificCss('../dist/controls.css', 'stylesheet'); + loadSpecificCss('../dist/demo.css', 'stylesheet'); } - - document.head.appendChild(link); } function importScript(src) { diff --git a/demo/log_section.js b/demo/log_section.js deleted file mode 100644 index 24004cda31..0000000000 --- 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 b37746f18b..aa02782cb9 100644 --- a/demo/main.js +++ b/demo/main.js @@ -16,770 +16,982 @@ */ /** - * @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_; -/** @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_; + /** @type {?ShakaDemoAssetInfo} */ + this.selectedAsset; + /** @private {?shaka.extern.PlayerConfiguration} */ + this.defaultConfig_; -/** @private {shaka.ui.Controls} */ -shakaDemo.controls_ = null; + /** @private {boolean} */ + this.fullyLoaded_ = false; + /** @private {?shaka.ui.Controls} */ + this.controls_; -/** @private {boolean} */ -shakaDemo.hashCanChange_ = false; - - -/** @private {boolean} */ -shakaDemo.suppressHashChangeEvent_ = false; - - -/** @private {(number|undefined)} */ -shakaDemo.startTime_ = undefined; - - -/** - * @private - * @const {string} - */ -shakaDemo.mainPoster_ = - 'https://shaka-player-demo.appspot.com/assets/poster.jpg'; - + /** @private {boolean} */ + this.nativeControlsEnabled_ = false; -/** - * @private - * @const {string} - */ -shakaDemo.audioOnlyPoster_ = - 'https://shaka-player-demo.appspot.com/assets/audioOnly.gif'; + /** @private {?shaka.offline.Storage} */ + this.storage_; + /** @private {shaka.extern.SupportType} */ + this.support_; -/** - * Initialize the application. - */ -shakaDemo.init = function() { - document.getElementById('errorDisplayCloseButton').addEventListener( - 'click', shakaDemo.closeError); + /** @private {string} */ + this.uiLocale_ = ''; + } - // Display the version number. - document.getElementById('version').textContent = shaka.Player.version; + /** + * 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_()); - // 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 + // Set up version string. + this.setUpVersionString_(); + } - document.getElementById('preferredAudioChannelCount').value = '2'; + /** + * 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_(); - let params = shakaDemo.getParams_(); + // 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); - shakaDemo.setupLogging_(); + // Update the componentHandler, to account for any new MDL elements added. + componentHandler.upgradeDom(); - shakaDemo.preBrowserCheckParams_(params); + // 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'); + } + } - // 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; + /** + * Initialize the application. + */ + async init() { + this.initCommon_(); - shakaDemo.onError_(/** @type {!shaka.util.Error} */ (event.error)); - }); + this.support_ = await shaka.Player.probeSupport(); - if (!shaka.Player.isBrowserSupported()) { - let errorDisplayLink = document.getElementById('errorDisplayLink'); - let error = 'Your browser is not supported!'; + this.video_ = + /** @type {!HTMLVideoElement} */(document.getElementById('video')); + this.video_.poster = ShakaDemoMain.mainPoster_; - // 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); - }); + 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); + } } - /** @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(); + 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, }; - 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']); - }); + 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 - shakaDemo.controls_.setEnabledShakaControls(false); + // Disable controls until something is loaded + this.controls_.setEnabledShakaControls(false); + this.controls_.setEnabledNativeControls(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)); + 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; + }; -/** - * @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; -}; - + 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; + } -/** - * @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; + 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; } } } -}; - -/** - * 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; + /** @private */ + hashChanged_() { + this.readHash_(); + this.dispatchEventWithName_('shaka-main-config-change'); } - // 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; - } + /** + * 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.'; + } - // 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; + if (needOffline && !asset.features.includes(shakaAssets.Feature.OFFLINE)) { + return 'This asset cannot be downloaded.'; } - } - return false; -}; + 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.'; + } -/** - * @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; - } + // Does the asset contain a playable mime type? + let mimeTypes = []; + if (asset.features.includes(shakaAssets.Feature.WEBM)) { + mimeTypes.push('video/webm'); } - 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'; + 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.'; } - // Call updateButtons_ manually, because changing assetList - // programatically doesn't fire a 'change' event. - shakaDemo.updateButtons_(/* canHide */ true); + return null; } - 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); + /** + * Enable or disable the native controls. + * Goes into effect during the next load. + * + * @param {boolean} enabled + */ + setNativeControlsEnabled(enabled) { + this.nativeControlsEnabled_ = enabled; + this.remakeHash(); } - 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; + /** + * Get if the native controls are enabled. + * + * @return {boolean} enabled + */ + getNativeControlsEnabled() { + return this.nativeControlsEnabled_; } - 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); - } + /** @param {string} locale */ + setUILocale(locale) { + this.uiLocale_ = locale; - 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); - } + // Fall back to browser languages after the demo page setting. + const preferredLocales = [locale].concat(navigator.languages); - // Allow the hash to be changed, and give it an initial change. - shakaDemo.hashCanChange_ = true; - shakaDemo.hashShouldChange_(); + this.controls_.getLocalization().changeLocale(preferredLocales); + } - if ('noinput' in params || 'play' in params) { - shakaDemo.load(); + /** @return {string} */ + getUILocale() { + return this.uiLocale_; } -}; + /** @private */ + readHash_() { + const params = this.getParams(); -/** @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; - } + if (this.player_) { + const readParam = (hashName, configName) => { + if (hashName in params) { + const existing = this.getCurrentConfigValue(configName); - let params = shakaDemo.getParams_(); - shakaDemo.preBrowserCheckParams_(params); - shakaDemo.postBrowserCheckParams_(params); -}; + // 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); + } -/** @private */ -shakaDemo.hashShouldChange_ = function() { - if (!shakaDemo.hashCanChange_) { - return; - } + // 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; + }); + } + } - let params = []; - let oldParams = shakaDemo.getParams_(); + // Disable custom controls. + this.nativeControlsEnabled_ = 'nativecontrols' in params; - // 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; + // 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'; } - } - 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 (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); } } + } - // If it's a custom asset we should store whatever the license - // server URI is. - if (!isDefault && licenseServerUri) { - params.push('license=' + licenseServerUri); + /** @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('='); } - } else { - if (assetList.selectedIndex == assetList.length - 1) { - // It's a custom asset. - let manifestInputValue = document.getElementById('manifestInput').value; - if (manifestInputValue) { - params.push('asset=' + manifestInputValue); + 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]); + } - let licenseInputValue = - document.getElementById('licenseServerInput').value; - if (licenseInputValue) { - params.push('license=' + licenseInputValue); + 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 { - // It's a default asset. - params.push('asset=' + - assetList[assetList.selectedIndex].asset.manifestUri); + // Remove any server certificates. + for (const drmSystem of asset.licenseServers.keys()) { + if (config.drm.advanced[drmSystem]) { + delete config.drm.advanced[drmSystem].serverCertificate; + } + } } - } - // 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); + 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)); + }); } - // 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); + /** Remakes the location's hash. */ + remakeHash() { + if (!this.fullyLoaded_) { + // Don't remake the hash until the demo page is fully loaded. + return; } - } - 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_); - } + 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()); - // Store values for drm configuration. - let videoRobustness = - document.getElementById('drmSettingsVideoRobustness').value; - if (videoRobustness) { - params.push('videoRobustness=' + videoRobustness); - } + 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; + } + } - let audioRobustness = - document.getElementById('drmSettingsAudioRobustness').value; - if (audioRobustness) { - params.push('audioRobustness=' + audioRobustness); - } + for (const type of ['compiled', 'debug_compiled', 'uncompiled']) { + const elem = document.getElementById(type.split('_').join('-') + '-link'); + if (elem.hasAttribute('disabled')) { + params.push('build=' + type); + } + } - // 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'; - } + if (this.nativeControlsEnabled_) { + params.push('nativecontrols'); + } - // 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; - }; + // 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; + } } - }); - // 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; - } + 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; + } - 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; - } + // 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(); - // 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 = ''; - } -}; + // 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); + } + } -/** - * @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; + // 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(', ') + ')'; + } } - const obj = await response.json(); - const map = new Map(); - for (let key in obj) { - map.set(key, obj[key]); + /** + * 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'); + } } - const localization = shakaDemo.controls_.getLocalization(); - localization.insert(locale, map); -}; + /** + * @param {boolean} connected + * @private + */ + onCastStatusChange_(connected) { + // TODO: Handle. + } +} -/** - * @param {!Event} event - * @private - */ -shakaDemo.onErrorEvent_ = function(event) { - let error = event.detail; - shakaDemo.onError_(error); -}; +let shakaDemoMain = new ShakaDemoMain(); /** - * @param {!shaka.util.Error} error * @private + * @const {string} */ -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'; - } -}; +ShakaDemoMain.mainPoster_ = + 'https://shaka-player-demo.appspot.com/assets/poster.jpg'; /** - * Closes the error bar. + * @private + * @const {string} */ -shakaDemo.closeError = function() { - document.getElementById('errorDisplay').style.display = 'none'; - let link = document.getElementById('errorDisplayLink'); - link.href = ''; - link.textContent = ''; - link.severity = null; -}; +ShakaDemoMain.audioOnlyPoster_ = + 'https://shaka-player-demo.appspot.com/assets/audioOnly.gif'; -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 321b4b995b..0000000000 --- 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 0000000000..f11236d4ed --- /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 2499385c57..7439fb22b5 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 0000000000000000000000000000000000000000..9094f53b8dc1efed2a5ae9a03d1dc5b63265a29a GIT binary patch literal 2417 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 literal 0 HcmV?d00001 diff --git a/demo/tooltip.js b/demo/tooltip.js new file mode 100644 index 0000000000..ccd53b748e --- /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 a692705397..7827d8549d 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 0000000000..943771dfe5 --- /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 0000000000000000000000000000000000000000..10b01658bb28fc6fad67a6b27d88e9009842729a GIT binary patch literal 120614 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�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 AIsgCw literal 0 HcmV?d00001 diff --git a/docs/tutorials/architecture.md b/docs/tutorials/architecture.md index 0083766aaf..f389817cc0 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 3dcf24a114..f7670972a1 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 0000000000..b95a48ad20 --- /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 0000000000..e77a8db1ce --- /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 0000000000..69c93d5a37 --- /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 336729f20d..98758d78c3 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 3440b67931..251768235a 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 efcdf0a4ed..f6dd3884a7 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 67c1bba389..c6719114ea 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 3cd487f771..b7de28490c 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 db279fe6cb..9304b29559 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 a554c38270..4a2e402937 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 09178bfcb6..334736daaa 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 d6f5e2b9d4..5736be2d9f 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 040a69322f..4e0aac64be 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 960d80d46c..ef85f56ad4 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 4984cee805..d3dd85cbcc 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); + } +};