diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/controls.js | 72 | ||||
-rw-r--r-- | src/js/defaults.js | 22 | ||||
-rw-r--r-- | src/js/html5.js | 146 | ||||
-rw-r--r-- | src/js/listeners.js | 13 | ||||
-rw-r--r-- | src/js/media.js | 25 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 83 | ||||
-rw-r--r-- | src/js/plyr.js | 16 | ||||
-rw-r--r-- | src/js/source.js | 6 | ||||
-rw-r--r-- | src/js/ui.js | 4 | ||||
-rw-r--r-- | src/js/utils.js | 15 |
10 files changed, 299 insertions, 103 deletions
diff --git a/src/js/controls.js b/src/js/controls.js index 5d6d98a1..828f0dcd 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -7,6 +7,7 @@ import utils from './utils'; import ui from './ui'; import i18n from './i18n'; import captions from './captions'; +import html5 from './html5'; // Sniff out the browser const browser = utils.getBrowser(); @@ -435,8 +436,8 @@ const controls = { utils.toggleHidden(pane, !toggle); }, - // Set the YouTube quality menu - // TODO: Support for HTML5 + // Set the quality menu + // TODO: Vimeo support setQualityMenu(options) { // Menu required if (!utils.is.element(this.elements.settings.panes.quality)) { @@ -449,12 +450,10 @@ const controls = { // Set options if passed and filter based on config if (utils.is.array(options)) { this.options.quality = options.filter(quality => this.config.quality.options.includes(quality)); - } else { - this.options.quality = this.config.quality.options; } // Toggle the pane and tab - const toggle = !utils.is.empty(this.options.quality) && this.isYouTube; + const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1; controls.toggleTab.call(this, type, toggle); // If we're hiding, nothing more to do @@ -470,22 +469,26 @@ const controls = { let label = ''; switch (quality) { - case 'hd2160': + case 2160: label = '4K'; break; - case 'hd1440': + case 1440: label = 'WQHD'; break; - case 'hd1080': + case 1080: label = 'HD'; break; - case 'hd720': + case 720: label = 'HD'; break; + case 576: + label = 'SD'; + break; + default: break; } @@ -497,9 +500,14 @@ const controls = { return controls.createBadge.call(this, label); }; - this.options.quality.forEach(quality => - controls.createMenuItem.call(this, quality, list, type, controls.getLabel.call(this, 'quality', quality), getBadge(quality)), - ); + // Sort options by the config and then render options + this.options.quality.sort((a, b) => { + const sorting = this.config.quality.options; + return sorting.indexOf(a) > sorting.indexOf(b) ? 1 : -1; + }).forEach(quality => { + const label = controls.getLabel.call(this, 'quality', quality); + controls.createMenuItem.call(this, quality, list, type, label, getBadge(quality)); + }); controls.updateSetting.call(this, type, list); }, @@ -512,28 +520,10 @@ const controls = { return value === 1 ? 'Normal' : `${value}×`; case 'quality': - switch (value) { - case 'hd2160': - return '2160P'; - case 'hd1440': - return '1440P'; - case 'hd1080': - return '1080P'; - case 'hd720': - return '720P'; - case 'large': - return '480P'; - case 'medium': - return '360P'; - case 'small': - return '240P'; - case 'tiny': - return 'Tiny'; - case 'default': - return 'Auto'; - default: - return value; + if (utils.is.number(value)) { + return `${value}p`; } + return utils.toTitleCase(value); case 'captions': return controls.getLanguage.call(this); @@ -544,7 +534,7 @@ const controls = { }, // Update the selected setting - updateSetting(setting, container) { + updateSetting(setting, container, input) { const pane = this.elements.settings.panes[setting]; let value = null; let list = container; @@ -555,7 +545,7 @@ const controls = { break; default: - value = this[setting]; + value = !utils.is.empty(input) ? input : this[setting]; // Get default if (utils.is.empty(value)) { @@ -563,7 +553,7 @@ const controls = { } // Unsupported value - if (!this.options[setting].includes(value)) { + if (!utils.is.empty(this.options[setting]) && !this.options[setting].includes(value)) { this.debug.warn(`Unsupported value of '${value}' for ${setting}`); return; } @@ -767,10 +757,10 @@ const controls = { // Check if we need to hide/show the settings menu checkMenu() { - const speedHidden = this.elements.settings.tabs.speed.getAttribute('hidden') !== null; - const languageHidden = this.elements.settings.tabs.captions.getAttribute('hidden') !== null; + const { tabs } = this.elements.settings; + const visible = !utils.is.empty(tabs) && Object.values(tabs).some(tab => !tab.hidden); - utils.toggleHidden(this.elements.settings.menu, speedHidden && languageHidden); + utils.toggleHidden(this.elements.settings.menu, !visible); }, // Show/hide menu @@ -1179,6 +1169,10 @@ const controls = { controls.setSpeedMenu.call(this); + if (this.isHTML5) { + controls.setQualityMenu.call(this, html5.getQualityOptions.call(this)); + } + return container; }, diff --git a/src/js/defaults.js b/src/js/defaults.js index 98df8b85..3e053f2f 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -63,17 +63,19 @@ const defaults = { // Quality default quality: { - default: 'default', + default: 720, options: [ - 'hd2160', - 'hd1440', - 'hd1080', - 'hd720', - 'large', - 'medium', - 'small', - 'tiny', - 'default', + 4320, + 2880, + 2160, + 1440, + 1080, + 720, + 576, + 480, + 360, + 240, + 'default', // YouTube's "auto" ], }, diff --git a/src/js/html5.js b/src/js/html5.js new file mode 100644 index 00000000..3818a441 --- /dev/null +++ b/src/js/html5.js @@ -0,0 +1,146 @@ +// ========================================================================== +// Plyr HTML5 helpers +// ========================================================================== + +import support from './support'; +import utils from './utils'; + +const html5 = { + getSources() { + if (!this.isHTML5) { + return null; + } + + return this.media.querySelectorAll('source'); + }, + + // Get quality levels + getQualityOptions() { + if (!this.isHTML5) { + return null; + } + + // Get sources + const sources = html5.getSources.call(this); + + if (utils.is.empty(sources)) { + return null; + } + + // Get <source> with size attribute + const sizes = Array.from(sources).filter(source => !utils.is.empty(source.getAttribute('size'))); + + // If none, bail + if (utils.is.empty(sizes)) { + return null; + } + + // Reduce to unique list + return utils.dedupe(sizes.map(source => Number(source.getAttribute('size')))); + }, + + extend() { + if (!this.isHTML5) { + return; + } + + const player = this; + + // Quality + Object.defineProperty(player.media, 'quality', { + get() { + // Get sources + const sources = html5.getSources.call(player); + + if (utils.is.empty(sources)) { + return null; + } + + const matches = Array.from(sources).filter(source => source.getAttribute('src') === player.source); + + if (utils.is.empty(matches)) { + return null; + } + + return Number(matches[0].getAttribute('size')); + }, + set(input) { + // Get sources + const sources = html5.getSources.call(player); + + if (utils.is.empty(sources)) { + return; + } + + // Get matches for requested size + const matches = Array.from(sources).filter(source => Number(source.getAttribute('size')) === input); + + // No matches for requested size + if (utils.is.empty(matches)) { + return; + } + + // Get supported sources + const supported = matches.filter(source => support.mime.call(player, source.getAttribute('type'))); + + // No supported sources + if (utils.is.empty(supported)) { + return; + } + + // Trigger change event + utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { + quality: input, + }); + + // Get current state + const { currentTime, playing } = player; + + // Set new source + player.media.src = supported[0].getAttribute('src'); + + // Load new source + player.media.load(); + + // Resume playing + if (playing) { + player.play(); + } + + // Restore time + player.currentTime = currentTime; + + // Trigger change event + utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { + quality: input, + }); + }, + }); + }, + + // Cancel current network requests + // See https://github.com/sampotts/plyr/issues/174 + cancelRequests() { + if (!this.isHTML5) { + return; + } + + // Remove child sources + utils.removeElement(html5.getSources()); + + // Set blank video src attribute + // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error + // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection + this.media.setAttribute('src', this.config.blankVideo); + + // Load the new empty source + // This will cancel existing requests + // See https://github.com/sampotts/plyr/issues/174 + this.media.load(); + + // Debugging + this.debug.log('Cancelled network requests'); + }, +}; + +export default html5; diff --git a/src/js/listeners.js b/src/js/listeners.js index 7c39ece7..be7a53ef 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -355,13 +355,16 @@ class Listeners { this.player.storage.set({ speed: this.player.speed }); }); + // Quality request + utils.on(this.player.media, 'qualityrequested', event => { + // Save to storage + this.player.storage.set({ quality: event.detail.quality }); + }); + // Quality change - utils.on(this.player.media, 'qualitychange', () => { + utils.on(this.player.media, 'qualitychange', event => { // Update UI - controls.updateSetting.call(this.player, 'quality'); - - // Save to storage - this.player.storage.set({ quality: this.player.quality }); + controls.updateSetting.call(this.player, 'quality', null, event.detail.quality); }); // Caption language change diff --git a/src/js/media.js b/src/js/media.js index 3a97a9d9..bba2c62b 100644 --- a/src/js/media.js +++ b/src/js/media.js @@ -6,6 +6,7 @@ import support from './support'; import utils from './utils'; import youtube from './plugins/youtube'; import vimeo from './plugins/vimeo'; +import html5 from './html5'; import ui from './ui'; // Sniff out the browser @@ -75,31 +76,9 @@ const media = { } } else if (this.isHTML5) { ui.setTitle.call(this); - } - }, - // Cancel current network requests - // See https://github.com/sampotts/plyr/issues/174 - cancelRequests() { - if (!this.isHTML5) { - return; + html5.extend.call(this); } - - // Remove child sources - utils.removeElement(this.media.querySelectorAll('source')); - - // Set blank video src attribute - // This is to prevent a MEDIA_ERR_SRC_NOT_SUPPORTED error - // Info: http://stackoverflow.com/questions/32231579/how-to-properly-dispose-of-an-html5-video-and-close-socket-or-connection - this.media.setAttribute('src', this.config.blankVideo); - - // Load the new empty source - // This will cancel existing requests - // See https://github.com/sampotts/plyr/issues/174 - this.media.load(); - - // Debugging - this.debug.log('Cancelled network requests'); }, }; diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 93f8cd33..27865d6f 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -6,6 +6,64 @@ import utils from './../utils'; import controls from './../controls'; import ui from './../ui'; +// Standardise YouTube quality unit +function mapQualityUnit(input) { + switch (input) { + case 'hd2160': + return 2160; + + case 2160: + return 'hd2160'; + + case 'hd1440': + return 1440; + + case 1440: + return 'hd1440'; + + case 'hd1080': + return 1080; + + case 1080: + return 'hd1080'; + + case 'hd720': + return 720; + + case 720: + return 'hd720'; + + case 'large': + return 480; + + case 480: + return 'large'; + + case 'medium': + return 360; + + case 360: + return 'medium'; + + case 'small': + return 240; + + case 240: + return 'small'; + + default: + return 'default'; + } +} + +function mapQualityUnits(levels) { + if (utils.is.empty(levels)) { + return levels; + } + + return utils.dedupe(levels.map(level => mapQualityUnit(level))); +} + const youtube = { setup() { // Add embed class for responsive @@ -168,14 +226,10 @@ const youtube = { utils.dispatchEvent.call(player, player.media, 'error'); }, - onPlaybackQualityChange(event) { - // Get the instance - const instance = event.target; - - // Get current quality - player.media.quality = instance.getPlaybackQuality(); - - utils.dispatchEvent.call(player, player.media, 'qualitychange'); + onPlaybackQualityChange() { + utils.dispatchEvent.call(player, player.media, 'qualitychange', false, { + quality: player.media.quality, + }); }, onPlaybackRateChange(event) { // Get the instance @@ -240,15 +294,18 @@ const youtube = { // Quality Object.defineProperty(player.media, 'quality', { get() { - return instance.getPlaybackQuality(); + return mapQualityUnit(instance.getPlaybackQuality()); }, set(input) { + const quality = input; + + // Set via API + instance.setPlaybackQuality(mapQualityUnit(quality)); + // Trigger request event utils.dispatchEvent.call(player, player.media, 'qualityrequested', false, { - quality: input, + quality, }); - - instance.setPlaybackQuality(input); }, }); @@ -401,7 +458,7 @@ const youtube = { } // Get quality - controls.setQualityMenu.call(player, instance.getAvailableQualityLevels()); + controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); break; diff --git a/src/js/plyr.js b/src/js/plyr.js index 5c3aeea2..75d75ef9 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -670,24 +670,28 @@ class Plyr { /** * Set playback quality - * Currently YouTube only - * @param {string} input - Quality level + * Currently HTML5 & YouTube only + * @param {number} input - Quality level */ set quality(input) { let quality = null; - if (utils.is.string(input)) { - quality = input; + if (!utils.is.empty(input)) { + quality = Number(input); } - if (!utils.is.string(quality)) { + if (!utils.is.number(quality) || quality === 0) { quality = this.storage.get('quality'); } - if (!utils.is.string(quality)) { + if (!utils.is.number(quality)) { quality = this.config.quality.selected; } + if (!utils.is.number(quality)) { + quality = this.config.quality.default; + } + if (!this.options.quality.includes(quality)) { this.debug.warn(`Unsupported quality option (${quality})`); return; diff --git a/src/js/source.js b/src/js/source.js index d252ba6b..3e713102 100644 --- a/src/js/source.js +++ b/src/js/source.js @@ -4,6 +4,7 @@ import { providers } from './types'; import utils from './utils'; +import html5 from './html5'; import media from './media'; import ui from './ui'; import support from './support'; @@ -31,13 +32,14 @@ const source = { } // Cancel current network requests - media.cancelRequests.call(this); + html5.cancelRequests.call(this); // Destroy instance and re-setup this.destroy.call( this, () => { - // TODO: Reset menus here + // Reset quality options + this.options.quality = []; // Remove elements utils.removeElement(this.media); diff --git a/src/js/ui.js b/src/js/ui.js index 97281b28..1d671577 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -71,8 +71,8 @@ const ui = { // Reset loop state this.loop = null; - // Reset quality options - this.options.quality = []; + // Reset quality setting + this.quality = null; // Reset volume display ui.updateVolume.call(this); diff --git a/src/js/utils.js b/src/js/utils.js index e9687a14..dd1466df 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -586,15 +586,15 @@ const utils = { }, // Trigger event - dispatchEvent(element, type, bubbles, detail) { + dispatchEvent(element, type = '', bubbles = false, detail = {}) { // Bail if no element - if (!utils.is.element(element) || !utils.is.string(type)) { + if (!utils.is.element(element) || utils.is.empty(type)) { return; } // Create and dispatch the event const event = new CustomEvent(type, { - bubbles: utils.is.boolean(bubbles) ? bubbles : false, + bubbles, detail: Object.assign({}, detail, { plyr: utils.is.plyr(this) ? this : null, }), @@ -737,6 +737,15 @@ const utils = { return utils.extend(target, ...sources); }, + // Remove duplicates in an array + dedupe(array) { + if (!utils.is.array(array)) { + return array; + } + + return array.filter((item, index) => array.indexOf(item) === index); + }, + // Get the provider for a given URL getProviderByUrl(url) { // YouTube |