diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/captions.js | 51 | ||||
-rw-r--r-- | src/js/controls.js | 100 | ||||
-rw-r--r-- | src/js/defaults.js | 5 | ||||
-rw-r--r-- | src/js/fullscreen.js | 2 | ||||
-rw-r--r-- | src/js/listeners.js | 7 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 29 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 8 | ||||
-rw-r--r-- | src/js/plyr.js | 46 | ||||
-rw-r--r-- | src/js/plyr.polyfilled.js | 2 | ||||
-rw-r--r-- | src/js/source.js | 4 | ||||
-rw-r--r-- | src/js/support.js | 10 | ||||
-rw-r--r-- | src/sass/components/embed.scss | 25 |
12 files changed, 204 insertions, 85 deletions
diff --git a/src/js/captions.js b/src/js/captions.js index c8bc5833..c6618fda 100644 --- a/src/js/captions.js +++ b/src/js/captions.js @@ -6,6 +6,7 @@ import support from './support'; import utils from './utils'; import controls from './controls'; +import i18n from './i18n'; const captions = { // Setup captions @@ -46,6 +47,7 @@ const captions = { return; } + // Inject the container if (!utils.is.element(this.elements.captions)) { this.elements.captions = utils.createElement('div', utils.getAttributesFromSelector(this.config.selectors.captions)); @@ -148,7 +150,49 @@ const captions = { // Get the current track for the current language getCurrentTrack() { - return captions.getTracks.call(this).find(track => track.language.toLowerCase() === this.language); + const tracks = captions.getTracks.call(this); + + if (!tracks.length) { + return null; + } + + // Get track based on current language + let track = tracks.find(track => track.language.toLowerCase() === this.language); + + // Get the <track> with default attribute + if (!track) { + track = utils.getElement.call(this, 'track[default]'); + } + + // Get the first track + if (!track) { + [track] = tracks; + } + + return track; + }, + + // Get UI label for track + getLabel(track) { + let currentTrack = track; + + if (!utils.is.track(currentTrack) && support.textTracks && this.captions.active) { + currentTrack = captions.getCurrentTrack.call(this); + } + + if (utils.is.track(currentTrack)) { + if (!utils.is.empty(currentTrack.label)) { + return currentTrack.label; + } + + if (!utils.is.empty(currentTrack.language)) { + return track.language.toUpperCase(); + } + + return i18n.get('enabled', this.config); + } + + return i18n.get('disabled', this.config); }, // Display active caption if it contains text @@ -206,11 +250,6 @@ const captions = { // Display captions container and button (for initialization) show() { - // If there's no caption toggle, bail - if (!utils.is.element(this.elements.buttons.captions)) { - return; - } - // Try to load the value from storage let active = this.storage.get('captions'); diff --git a/src/js/controls.js b/src/js/controls.js index 160c3665..615da39e 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -15,10 +15,7 @@ const browser = utils.getBrowser(); const controls = { // Webkit polyfill for lower fill range updateRangeFill(target) { - // WebKit only - if (!browser.isWebkit) { - return; - } + // Get range from event if event passed const range = utils.is.event(target) ? target.target : target; @@ -28,6 +25,14 @@ const controls = { return; } + // Set aria value for https://github.com/sampotts/plyr/issues/905 + range.setAttribute('aria-valuenow', range.value); + + // WebKit only + if (!browser.isWebkit) { + return; + } + // Set CSS custom property range.style.setProperty('--value', `${range.value / range.max * 100}%`); }, @@ -52,6 +57,7 @@ const controls = { icon, utils.extend(attributes, { role: 'presentation', + focusable: 'false', }), ); @@ -238,6 +244,7 @@ const controls = { 'label', { for: attributes.id, + id: `${attributes.id}-label`, class: this.config.classNames.hidden, }, i18n.get(type, this.config), @@ -255,6 +262,12 @@ const controls = { step: 0.01, value: 0, autocomplete: 'off', + // A11y fixes for https://github.com/sampotts/plyr/issues/905 + role: 'slider', + 'aria-labelledby': `${attributes.id}-label`, + 'aria-valuemin': 0, + 'aria-valuemax': 100, + 'aria-valuenow': 0, }, attributes, ), @@ -281,6 +294,8 @@ const controls = { min: 0, max: 100, value: 0, + role: 'presentation', + 'aria-hidden': true, }, attributes, ), @@ -456,6 +471,9 @@ const controls = { const toggle = !utils.is.empty(this.options.quality) && this.options.quality.length > 1; controls.toggleTab.call(this, type, toggle); + // Check if we need to toggle the parent + controls.checkMenu.call(this); + // If we're hiding, nothing more to do if (!toggle) { return; @@ -495,13 +513,15 @@ const controls = { }; // 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)); - }); + 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); }, @@ -517,10 +537,11 @@ const controls = { if (utils.is.number(value)) { return `${value}p`; } + return utils.toTitleCase(value); case 'captions': - return controls.getLanguage.call(this); + return captions.getLabel.call(this); default: return null; @@ -535,7 +556,16 @@ const controls = { switch (setting) { case 'captions': - value = this.captions.active ? this.captions.language : i18n.get('disabled', this.config); + if (this.captions.active) { + if (this.options.captions.length > 2 || !this.options.captions.some(lang => lang === 'enabled')) { + value = this.captions.language; + } else { + value = 'enabled'; + } + } else { + value = ''; + } + break; default: @@ -566,17 +596,19 @@ const controls = { list = pane && pane.querySelector('ul'); } - // Update the label - if (!utils.is.empty(value)) { - const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`); - label.innerHTML = controls.getLabel.call(this, setting, value); + // If there's no list it means it's not been rendered... + if (!utils.is.element(list)) { + return; } - // Find the radio option + // Update the label + const label = this.elements.settings.tabs[setting].querySelector(`.${this.config.classNames.menu.value}`); + label.innerHTML = controls.getLabel.call(this, setting, value); + + // Find the radio option and check it const target = list && list.querySelector(`input[value="${value}"]`); if (utils.is.element(target)) { - // Check it target.checked = true; } }, @@ -627,21 +659,7 @@ const controls = { // Get current selected caption language // TODO: rework this to user the getter in the API? - getLanguage() { - if (!this.supported.ui) { - return null; - } - if (support.textTracks && captions.getTracks.call(this).length && this.captions.active) { - const currentTrack = captions.getCurrentTrack.call(this); - - if (utils.is.track(currentTrack)) { - return currentTrack.label; - } - } - - return i18n.get('disabled', this.config); - }, // Set a list of available captions languages setCaptionsMenu() { @@ -656,6 +674,9 @@ const controls = { // Empty the menu utils.emptyElement(list); + // Check if we need to toggle the parent + controls.checkMenu.call(this); + // If there's no captions, bail if (!toggle) { return; @@ -663,8 +684,8 @@ const controls = { // Re-map the tracks into just the data we need const tracks = captions.getTracks.call(this).map(track => ({ - language: track.language, - label: !utils.is.empty(track.label) ? track.label : track.language.toUpperCase(), + language: !utils.is.empty(track.language) ? track.language : 'enabled', + label: captions.getLabel.call(this, track), })); // Add the "Disabled" option to turn off captions @@ -680,12 +701,15 @@ const controls = { track.language, list, 'language', - track.label || track.language, - controls.createBadge.call(this, track.language.toUpperCase()), + track.label, + track.language !== 'enabled' ? controls.createBadge.call(this, track.language.toUpperCase()) : null, track.language.toLowerCase() === this.captions.language.toLowerCase(), ); }); + // Store reference + this.options.captions = tracks.map(track => track.language); + controls.updateSetting.call(this, type, list); }, @@ -1211,7 +1235,7 @@ const controls = { seektime: this.config.seekTime, speed: this.speed, quality: this.quality, - captions: controls.getLanguage.call(this), + captions: captions.getLabel.call(this), // TODO: Looping // loop: 'None', }); diff --git a/src/js/defaults.js b/src/js/defaults.js index 8e6b9bd5..e96587f7 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -56,7 +56,7 @@ const defaults = { // Sprite (for icons) loadSprite: true, iconPrefix: 'plyr', - iconUrl: 'https://cdn.plyr.io/3.1.0/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.2.1/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', @@ -115,7 +115,7 @@ const defaults = { // Captions settings captions: { active: false, - language: window.navigator.language ? window.navigator.language.split('-')[0] : 'en', + language: (navigator.language || navigator.userLanguage).split('-')[0], }, // Fullscreen settings @@ -185,6 +185,7 @@ const defaults = { all: 'All', reset: 'Reset', disabled: 'Disabled', + enabled: 'Enabled', advertisement: 'Ad', }, diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js index 857a2edc..8795f460 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -90,7 +90,7 @@ class Fullscreen { static get prefix() { // No prefix if (utils.is.function(document.exitFullscreen)) { - return false; + return ''; } // Check for fullscreen support by vendor prefix diff --git a/src/js/listeners.js b/src/js/listeners.js index be7a53ef..5887c3ab 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -2,7 +2,6 @@ // Plyr Event Listeners // ========================================================================== -import support from './support'; import utils from './utils'; import controls from './controls'; import ui from './ui'; @@ -293,6 +292,10 @@ class Listeners { // If autoplay, then load advertisement if required // TODO: Show some sort of loading state while the ad manager loads else there's a delay before ad shows utils.on(this.player.media, 'playing', () => { + if (!this.player.ads) { + return; + } + // If ads are enabled, wait for them first if (this.player.ads.enabled && !this.player.ads.initialized) { // Wait for manager response @@ -331,7 +334,7 @@ class Listeners { // Disable right click if (this.player.supported.ui && this.player.config.disableContextMenu) { utils.on( - this.player.media, + this.player.elements.wrapper, 'contextmenu', event => { event.preventDefault(); diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 3aac5b2d..24003d3f 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -35,10 +35,14 @@ const vimeo = { setAspectRatio(input) { const ratio = utils.is.string(input) ? input.split(':') : this.config.ratio.split(':'); const padding = 100 / ratio[0] * ratio[1]; - const height = 240; - const offset = (height - padding) / (height / 50); this.elements.wrapper.style.paddingBottom = `${padding}%`; - this.media.style.transform = `translateY(-${offset}%)`; + + if (this.supported.ui) { + const height = 240; + const offset = (height - padding) / (height / 50); + + this.media.style.transform = `translateY(-${offset}%)`; + } }, // API Ready @@ -55,6 +59,7 @@ const vimeo = { speed: true, transparent: 0, gesture: 'media', + playsinline: !this.config.fullscreen.iosNative, }; const params = utils.buildUrlParams(options); @@ -88,6 +93,11 @@ const vimeo = { player.media.paused = true; player.media.currentTime = 0; + // Disable native text track rendering + if (player.supported.ui) { + player.embed.disableTextTrack(); + } + // Create a faux HTML5 API using the Vimeo API player.media.play = () => { player.embed.play().then(() => { @@ -124,7 +134,9 @@ const vimeo = { utils.dispatchEvent.call(player, player.media, 'seeking'); // Seek after events - player.embed.setCurrentTime(time); + player.embed.setCurrentTime(time).catch(() => { + // Do nothing + }); // Restore pause state if (paused) { @@ -310,6 +322,15 @@ const vimeo = { if (parseInt(data.percent, 10) === 1) { utils.dispatchEvent.call(player, player.media, 'canplaythrough'); } + + // Get duration as if we do it before load, it gives an incorrect value + // https://github.com/sampotts/plyr/issues/891 + player.embed.getDuration().then(value => { + if (value !== player.media.duration) { + player.media.duration = value; + utils.dispatchEvent.call(player, player.media, 'durationchange'); + } + }); }); player.embed.on('seeked', () => { diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index d1ab537b..12bc2b11 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -270,6 +270,9 @@ const youtube = { return Number(instance.getCurrentTime()); }, set(time) { + // Vimeo will automatically play on seek + const { paused } = player.media; + // Set seeking flag player.media.seeking = true; @@ -278,6 +281,11 @@ const youtube = { // Seek after events sent instance.seekTo(time); + + // Restore pause state + if (paused) { + player.pause(); + } }, }); diff --git a/src/js/plyr.js b/src/js/plyr.js index 04a68d02..2e64aa1a 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.1.0 +// plyr.js v3.2.1 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== @@ -97,6 +97,7 @@ class Plyr { this.options = { speed: [], quality: [], + captions: [], }; // Debugging @@ -184,12 +185,17 @@ class Plyr { if (truthy.includes(params.autoplay)) { this.config.autoplay = true; } - if (truthy.includes(params.playsinline)) { - this.config.inline = true; - } if (truthy.includes(params.loop)) { this.config.loop.active = true; } + + // TODO: replace fullscreen.iosNative with this playsinline config option + // YouTube requires the playsinline in the URL + if (this.isYouTube) { + this.config.playsinline = truthy.includes(params.playsinline); + } else { + this.config.playsinline = true; + } } } else { // <div> with attributes @@ -223,7 +229,7 @@ class Plyr { this.config.autoplay = true; } if (this.media.hasAttribute('playsinline')) { - this.config.inline = true; + this.config.playsinline = true; } if (this.media.hasAttribute('muted')) { this.config.muted = true; @@ -240,7 +246,7 @@ class Plyr { } // Check for support again but with type - this.supported = support.check(this.type, this.provider, this.config.inline); + this.supported = support.check(this.type, this.provider, this.config.playsinline); // If no support for even API, bail if (!this.supported.api) { @@ -368,7 +374,7 @@ class Plyr { * Get playing state */ get playing() { - return Boolean(!this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true)); + return Boolean(this.ready && !this.paused && !this.ended && (this.isHTML5 ? this.media.readyState > 2 : true)); } /** @@ -446,7 +452,7 @@ class Plyr { } // Set - this.media.currentTime = parseFloat(targetTime.toFixed(4)); + this.media.currentTime = targetTime; // Logging this.debug.log(`Seeking to ${this.currentTime} seconds`); @@ -492,7 +498,7 @@ class Plyr { */ get duration() { // Faux duration set via config - const fauxDuration = parseInt(this.config.duration, 10); + const fauxDuration = parseFloat(this.config.duration); // True duration const realDuration = this.media ? Number(this.media.duration) : 0; @@ -839,8 +845,8 @@ class Plyr { * @param {boolean} input - Whether to enable captions */ toggleCaptions(input) { - // If there's no full support, or there's no caption toggle - if (!this.supported.ui || !utils.is.element(this.elements.buttons.captions)) { + // If there's no full support + if (!this.supported.ui) { return; } @@ -875,17 +881,29 @@ class Plyr { return; } - // Toggle captions based on input - this.toggleCaptions(!utils.is.empty(input)); - // If empty string is passed, assume disable captions if (utils.is.empty(input)) { + this.toggleCaptions(false); return; } // Normalize const language = input.toLowerCase(); + // Check for support + if (!this.options.captions.includes(language)) { + this.debug.warn(`Unsupported language option: ${language}`); + return; + } + + // Ensure captions are enabled + this.toggleCaptions(true); + + // Enabled only + if (language === 'enabled') { + return; + } + // If nothing to change, bail if (this.language === language) { return; diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 50e0c589..10da2f83 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.1.0 +// plyr.js v3.2.1 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/source.js b/src/js/source.js index 3e713102..4e3f9186 100644 --- a/src/js/source.js +++ b/src/js/source.js @@ -55,7 +55,7 @@ const source = { this.provider = !utils.is.empty(input.sources[0].provider) ? input.sources[0].provider : providers.html5; // Check for support - this.supported = support.check(this.type, this.provider, this.config.inline); + this.supported = support.check(this.type, this.provider, this.config.playsinline); // Create new markup switch (`${this.provider}:${this.type}`) { @@ -103,7 +103,7 @@ const source = { if (this.config.muted) { this.media.setAttribute('muted', ''); } - if (this.config.inline) { + if (this.config.playsinline) { this.media.setAttribute('playsinline', ''); } } diff --git a/src/js/support.js b/src/js/support.js index a9a302f3..5528e898 100644 --- a/src/js/support.js +++ b/src/js/support.js @@ -12,16 +12,16 @@ const support = { // Check for support // Basic functionality vs full UI - check(type, provider, inline) { + check(type, provider, playsinline) { let api = false; let ui = false; const browser = utils.getBrowser(); - const playsInline = browser.isIPhone && inline && support.inline; + const canPlayInline = browser.isIPhone && playsinline && support.playsinline; switch (`${provider}:${type}`) { case 'html5:video': api = support.video; - ui = api && support.rangeInput && (!browser.isIPhone || playsInline); + ui = api && support.rangeInput && (!browser.isIPhone || canPlayInline); break; case 'html5:audio': @@ -32,7 +32,7 @@ const support = { case 'youtube:video': case 'vimeo:video': api = true; - ui = support.rangeInput && (!browser.isIPhone || playsInline); + ui = support.rangeInput && (!browser.isIPhone || canPlayInline); break; default: @@ -59,7 +59,7 @@ const support = { // Inline playback support // https://webkit.org/blog/6784/new-video-policies-for-ios/ - inline: 'playsInline' in document.createElement('video'), + playsinline: 'playsInline' in document.createElement('video'), // Check for mime type support against a player instance // Credits: http://diveintohtml5.info/everything.html diff --git a/src/sass/components/embed.scss b/src/sass/components/embed.scss index c15ee522..d72836de 100644 --- a/src/sass/components/embed.scss +++ b/src/sass/components/embed.scss @@ -3,14 +3,12 @@ // YouTube, Vimeo, etc // -------------------------------------------------------------- -.plyr__video-embed { - // Default to 16:9 ratio but this is set by JavaScript based on config - $padding: ((100 / 16) * 9); - $height: 240; - $offset: to-percentage(($height - $padding) / ($height / 50)); +// Default to 16:9 ratio but this is set by JavaScript based on config +$embed-padding: ((100 / 16) * 9); +.plyr__video-embed { height: 0; - padding-bottom: to-percentage($padding); + padding-bottom: to-percentage($embed-padding); position: relative; iframe { @@ -22,6 +20,17 @@ user-select: none; width: 100%; } +} + +// If the full custom UI is supported +.plyr--full-ui .plyr__video-embed { + $height: 240; + $offset: to-percentage(($height - $embed-padding) / ($height / 50)); + + // To allow mouse events to be captured if full support + iframe { + pointer-events: none; + } // Vimeo hack > div { @@ -30,7 +39,3 @@ transform: translateY(-$offset); } } -// To allow mouse events to be captured if full support -.plyr--full-ui .plyr__video-embed iframe { - pointer-events: none; -} |