From 90d5b48845661ce99a204354f93fbbbc7a19f100 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 15 May 2018 04:23:27 +0200 Subject: Add async method to utils for loading/checking images --- src/js/utils.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'src') diff --git a/src/js/utils.js b/src/js/utils.js index ebfb9c96..0cd332dd 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -119,6 +119,21 @@ const utils = { }); }, + // Load image avoiding xhr/fetch CORS issues + // Server status can't be obtained this way unfortunately, so this uses "naturalWidth" to determine if the image has loaded. + // By default it checks if it is at least 1px, but you can add a second argument to change this. + loadImage(src, minWidth = 1) { + return new Promise((resolve, reject) => { + const image = new Image(); + const handler = () => { + delete image.onload; + delete image.onerror; + (image.naturalWidth >= minWidth ? resolve : reject)(image); + }; + Object.assign(image, {onload: handler, onerror: handler, src}); + }); + }, + // Load an external script loadScript(url) { return new Promise((resolve, reject) => { -- cgit v1.2.3 From 16c3a7d9e5be8ed2ffbbcee1c786b88d1cecc4cd Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 15 May 2018 05:16:06 +0200 Subject: Rewrite ui.setPoster to check that images arent broken or youtube fallback images. Only show poster element when valid --- src/js/defaults.js | 2 +- src/js/plugins/vimeo.js | 7 ++----- src/js/plugins/youtube.js | 8 +++++++- src/js/plyr.js | 5 +---- src/js/ui.js | 39 ++++++++++++++++++++++++++++++--------- src/sass/components/poster.scss | 2 +- 6 files changed, 42 insertions(+), 21 deletions(-) (limited to 'src') diff --git a/src/js/defaults.js b/src/js/defaults.js index f160b1aa..7cc5c082 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -199,7 +199,6 @@ const defaults = { youtube: { sdk: 'https://www.youtube.com/iframe_api', api: 'https://www.googleapis.com/youtube/v3/videos?id={0}&key={1}&fields=items(snippet(title))&part=snippet', - poster: 'https://img.youtube.com/vi/{0}/maxresdefault.jpg,https://img.youtube.com/vi/{0}/hqdefault.jpg', }, googleIMA: { sdk: 'https://imasdk.googleapis.com/js/sdkloader/ima3.js', @@ -332,6 +331,7 @@ const defaults = { embed: 'plyr__video-embed', embedContainer: 'plyr__video-embed__container', poster: 'plyr__poster', + posterEnabled: 'plyr__poster-enabled', ads: 'plyr__ads', control: 'plyr__control', playing: 'plyr--playing', diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 0ceb89e5..96b36781 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -99,11 +99,8 @@ const vimeo = { // Get original image url.pathname = `${url.pathname.split('_')[0]}.jpg`; - // Set attribute - player.media.setAttribute('poster', url.href); - - // Update - ui.setPoster.call(player); + // Set and show poster + ui.setPoster.call(player, url.href); }); // Setup instance diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 4fde9319..4ba8089b 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -162,7 +162,13 @@ const youtube = { player.media = utils.replaceElement(container, player.media); // Set poster image - player.media.setAttribute('poster', utils.format(player.config.urls.youtube.poster, videoId)); + const posterSrc = format => `https://img.youtube.com/vi/${videoId}/${format}default.jpg`; + + // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide) + utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded + .catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 + .catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists + .then(image => ui.setPoster.call(player, image.src)); // Setup instance // https://developers.google.com/youtube/iframe_api_reference diff --git a/src/js/plyr.js b/src/js/plyr.js index 6a3deade..dee90dd2 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -801,10 +801,7 @@ class Plyr { return; } - if (utils.is.string(input)) { - this.media.setAttribute('poster', input); - ui.setPoster.call(this); - } + ui.setPoster.call(this, input); } /** diff --git a/src/js/ui.js b/src/js/ui.js index 2347b5c8..50764a86 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -105,8 +105,10 @@ const ui = { // Set the title ui.setTitle.call(this); - // Set the poster image - ui.setPoster.call(this); + // Assure the poster image is set, if the property was added before the UI was created + if (this.poster) { + ui.setPoster.call(this, this.poster); + } }, // Setup aria attribute for play and iframe title @@ -146,15 +148,34 @@ const ui = { } }, - // Set the poster image - setPoster() { - if (!utils.is.element(this.elements.poster) || utils.is.empty(this.poster)) { - return; + // Toggle poster + togglePoster(enable) { + utils.toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable); + }, + + // Set the poster image (async) + setPoster(poster) { + // Set property regardless of validity + this.media.setAttribute('poster', poster); + + // Bail if element is missing + if (!utils.is.element(this.elements.poster)) { + return Promise.reject(); } - // Set the inline style - const posters = this.poster.split(','); - this.elements.poster.style.backgroundImage = posters.map(p => `url('${p}')`).join(','); + // Load the image, and set poster if successful + const loadPromise = utils.loadImage(poster) + .then(() => { + this.elements.poster.style.backgroundImage = `url('${poster}')`; + ui.togglePoster.call(this, true); + return poster; + }); + + // Hide the element if the poster can't be loaded (otherwise it will just be a black element covering the video) + loadPromise.catch(() => ui.togglePoster.call(this, false)); + + // Return the promise so the caller can use it as well + return loadPromise; }, // Check playing state diff --git a/src/sass/components/poster.scss b/src/sass/components/poster.scss index 92ab0fce..4bdb60d9 100644 --- a/src/sass/components/poster.scss +++ b/src/sass/components/poster.scss @@ -18,6 +18,6 @@ pointer-events: none; } -.plyr--stopped .plyr__poster { +.plyr--stopped.plyr__poster-enabled .plyr__poster { opacity: 1; } -- cgit v1.2.3 From c845558d960412ad5e942334fd9f60ed173e0a5a Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 15 May 2018 16:22:51 +0200 Subject: Youtube poster: Set css backgroundSize to 'cover' for padded youtube thumbnails --- src/js/plugins/youtube.js | 8 +++++++- src/js/ui.js | 9 +++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 4ba8089b..10283998 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -168,7 +168,13 @@ const youtube = { utils.loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded .catch(() => utils.loadImage(posterSrc('sd'), 121)) // 480p padded 4:3 .catch(() => utils.loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists - .then(image => ui.setPoster.call(player, image.src)); + .then(image => ui.setPoster.call(player, image.src)) + .then(posterSrc => { + // If the image is padded, use background-size "cover" instead (like youtube does too with their posters) + if (!posterSrc.includes('maxres')) { + player.elements.poster.style.backgroundSize = 'cover'; + } + }); // Setup instance // https://developers.google.com/youtube/iframe_api_reference diff --git a/src/js/ui.js b/src/js/ui.js index 50764a86..f844f93c 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -105,8 +105,8 @@ const ui = { // Set the title ui.setTitle.call(this); - // Assure the poster image is set, if the property was added before the UI was created - if (this.poster) { + // Assure the poster image is set, if the property was added before the element was created + if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) { ui.setPoster.call(this, this.poster); } }, @@ -167,6 +167,11 @@ const ui = { const loadPromise = utils.loadImage(poster) .then(() => { this.elements.poster.style.backgroundImage = `url('${poster}')`; + Object.assign(this.elements.poster.style, { + backgroundImage: `url('${poster}')`, + // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube) + backgroundSize: '', + }); ui.togglePoster.call(this, true); return poster; }); -- cgit v1.2.3 From 4db6bf7a2e2256c20c885a536fadd4a5f4433469 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Wed, 9 May 2018 22:08:09 +0200 Subject: Make utils.toggleClass() compatible with Element.classList.toggle (rename toggle argument to 'force' and make it optional) --- src/js/utils.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/js/utils.js b/src/js/utils.js index 5c4934a1..0a1c8fcc 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -390,14 +390,16 @@ const utils = { } }, - // Toggle class on an element - toggleClass(element, className, toggle) { + // Mirror Element.classList.toggle, with IE compatibility for "force" argument + toggleClass(element, className, force) { if (utils.is.element(element)) { - const contains = element.classList.contains(className); - - element.classList[toggle ? 'add' : 'remove'](className); + let method = 'toggle'; + if (typeof force !== 'undefined') { + method = force ? 'add' : 'remove'; + } - return (toggle && !contains) || (!toggle && contains); + element.classList[method](className); + return element.classList.contains(className); } return null; -- cgit v1.2.3 From d7356726a16d64d242cd20421bff431d66d146b8 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Thu, 10 May 2018 00:36:02 +0200 Subject: Remove ui.checkFailed() and error class --- src/js/defaults.js | 1 - src/js/listeners.js | 3 --- src/js/ui.js | 23 ----------------------- src/sass/plyr.scss | 1 - src/sass/states/error.scss | 25 ------------------------- 5 files changed, 53 deletions(-) delete mode 100644 src/sass/states/error.scss (limited to 'src') diff --git a/src/js/defaults.js b/src/js/defaults.js index f160b1aa..da089efc 100644 --- a/src/js/defaults.js +++ b/src/js/defaults.js @@ -338,7 +338,6 @@ const defaults = { paused: 'plyr--paused', stopped: 'plyr--stopped', loading: 'plyr--loading', - error: 'plyr--has-error', hover: 'plyr--hover', tooltip: 'plyr__tooltip', cues: 'plyr__cues', diff --git a/src/js/listeners.js b/src/js/listeners.js index f4e9ade3..ebcc5f06 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -283,9 +283,6 @@ class Listeners { // Loading state utils.on(this.player.media, 'waiting canplay seeked playing', event => ui.checkLoading.call(this.player, event)); - // Check if media failed to load - // utils.on(this.player.media, 'play', event => ui.checkFailed.call(this.player, event)); - // 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', () => { diff --git a/src/js/ui.js b/src/js/ui.js index 2347b5c8..ea592d82 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -196,29 +196,6 @@ const ui = { }, this.loading ? 250 : 0); }, - // Check if media failed to load - checkFailed() { - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState - this.failed = this.media.networkState === 3; - - if (this.failed) { - utils.toggleClass(this.elements.container, this.config.classNames.loading, false); - utils.toggleClass(this.elements.container, this.config.classNames.error, true); - } - - // Clear timer - clearTimeout(this.timers.failed); - - // Timer to prevent flicker when seeking - this.timers.loading = setTimeout(() => { - // Toggle container class hook - utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading); - - // Show controls if loading, hide if done - this.toggleControls(this.loading); - }, this.loading ? 250 : 0); - }, - // Update volume UI and storage updateVolume() { if (!this.supported.ui) { diff --git a/src/sass/plyr.scss b/src/sass/plyr.scss index 65134331..e934cf92 100644 --- a/src/sass/plyr.scss +++ b/src/sass/plyr.scss @@ -39,7 +39,6 @@ @import 'components/video'; @import 'components/volume'; -@import 'states/error'; @import 'states/fullscreen'; @import 'plugins/ads'; diff --git a/src/sass/states/error.scss b/src/sass/states/error.scss deleted file mode 100644 index 64d05c7b..00000000 --- a/src/sass/states/error.scss +++ /dev/null @@ -1,25 +0,0 @@ -// -------------------------------------------------------------- -// Error state -// -------------------------------------------------------------- - -.plyr--has-error { - pointer-events: none; - - &::after { - align-items: center; - background: rgba(#000, 90%); - color: #fff; - content: attr(data-plyr-error); - display: flex; - font-size: $plyr-font-size-base; - height: 100%; - justify-content: center; - left: 0; - position: absolute; - text-align: center; - text-shadow: 0 1px 1px rgba(#000, 10%); - top: 0; - width: 100%; - z-index: 10; - } -} -- cgit v1.2.3 From 37c5fbfe16ba0969b727b8359fdd04eb0bf7a021 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Wed, 16 May 2018 04:52:04 +0200 Subject: toggleControls() rewrite --- src/js/listeners.js | 92 ++++++++++++++++++++++++++++----------- src/js/plyr.js | 123 ++++++++-------------------------------------------- src/js/ui.js | 18 ++++++-- 3 files changed, 99 insertions(+), 134 deletions(-) (limited to 'src') diff --git a/src/js/listeners.js b/src/js/listeners.js index ebcc5f06..d095bc03 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -238,13 +238,36 @@ class Listeners { }, 0); }); - // Toggle controls visibility based on mouse movement - if (this.player.config.hideControls) { - // Toggle controls on mouse events and entering fullscreen - utils.on(this.player.elements.container, 'mouseenter mouseleave mousemove touchstart touchend touchmove enterfullscreen exitfullscreen', event => { - this.player.toggleControls(event); - }); - } + // Toggle controls on mouse events and entering fullscreen + utils.on(this.player.elements.container, 'mousemove mouseleave touchstart touchmove enterfullscreen exitfullscreen', event => { + const { controls } = this.player.elements; + + // Remove button states for fullscreen + if (event.type === 'enterfullscreen') { + controls.pressed = false; + controls.hover = false; + } + + // Show, then hide after a timeout unless another control event occurs + const show = [ + 'touchstart', + 'touchmove', + 'mousemove', + ].includes(event.type); + + let delay = 0; + + if (show) { + ui.toggleControls.call(this.player, true); + // Use longer timeout for touch devices + delay = this.player.touch ? 3000 : 2000; + } + + // Clear timer + clearTimeout(this.player.timers.controls); + // Timer to prevent flicker when seeking + this.player.timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); + }); } // Listen for media events @@ -570,26 +593,45 @@ class Listeners { // Seek tooltip on(this.player.elements.progress, 'mouseenter mouseleave mousemove', event => controls.updateSeekTooltip.call(this.player, event)); - // Toggle controls visibility based on mouse movement - if (this.player.config.hideControls) { - // Watch for cursor over controls so they don't hide when trying to interact - on(this.player.elements.controls, 'mouseenter mouseleave', event => { - this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; - }); + // Update controls.hover state (used for ui.toggleControls to avoid hiding when interacting) + on(this.player.elements.controls, 'mouseenter mouseleave', event => { + this.player.elements.controls.hover = !this.player.touch && event.type === 'mouseenter'; + }); - // Watch for cursor over controls so they don't hide when trying to interact - on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { - this.player.elements.controls.pressed = [ - 'mousedown', - 'touchstart', - ].includes(event.type); - }); + // Update controls.pressed state (used for ui.toggleControls to avoid hiding when interacting) + on(this.player.elements.controls, 'mousedown mouseup touchstart touchend touchcancel', event => { + this.player.elements.controls.pressed = [ + 'mousedown', + 'touchstart', + ].includes(event.type); + }); - // Focus in/out on controls - on(this.player.elements.controls, 'focusin focusout', event => { - this.player.toggleControls(event); - }); - } + // Focus in/out on controls + on(this.player.elements.controls, 'focusin focusout', event => { + const { config, elements, timers } = this.player; + + // Skip transition to prevent focus from scrolling the parent element + utils.toggleClass(elements.controls, config.classNames.noTransition, event.type === 'focusin'); + + // Toggle + ui.toggleControls.call(this.player, event.type === 'focusin'); + + // If focusin, hide again after delay + if (event.type === 'focusin') { + // Restore transition + setTimeout(() => { + utils.toggleClass(elements.controls, config.classNames.noTransition, false); + }, 0); + + // Delay a little more for keyboard users + const delay = this.touch ? 3000 : 4000; + + // Clear timer + clearTimeout(timers.controls); + // Hide + timers.controls = setTimeout(() => ui.toggleControls.call(this.player, false), delay); + } + }); // Mouse wheel for volume on( diff --git a/src/js/plyr.js b/src/js/plyr.js index bed09827..ffd2a1e3 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -971,119 +971,32 @@ class Plyr { /** * Toggle the player controls - * @param {boolean} toggle - Whether to show the controls + * @param {boolean} [toggle] - Whether to show the controls */ toggleControls(toggle) { - // We need controls of course... - if (!utils.is.element(this.elements.controls)) { - return; - } - - // Don't hide if no UI support or it's audio - if (!this.supported.ui || this.isAudio) { - return; - } - - let delay = 0; - let show = toggle; - let isEnterFullscreen = false; - - // Get toggle state if not set - if (!utils.is.boolean(toggle)) { - if (utils.is.event(toggle)) { - // Is the enter fullscreen event - isEnterFullscreen = toggle.type === 'enterfullscreen'; - - // Events that show the controls - const showEvents = [ - 'touchstart', - 'touchmove', - 'mouseenter', - 'mousemove', - 'focusin', - ]; - - // Events that delay hiding - const delayEvents = [ - 'touchmove', - 'touchend', - 'mousemove', - ]; - - // Whether to show controls - show = showEvents.includes(toggle.type); - - // Delay hiding on move events - if (delayEvents.includes(toggle.type)) { - delay = 2000; - } - - // Delay a little more for keyboard users - if (!this.touch && toggle.type === 'focusin') { - delay = 3000; - utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, true); - } - } else { - show = utils.hasClass(this.elements.container, this.config.classNames.hideControls); - } - } + // Don't toggle if missing UI support or if it's audio + if (this.supported.ui && !this.isAudio) { + // Get state before change + const isHidden = utils.hasClass(this.elements.container, this.config.classNames.hideControls); - // Clear timer on every call - clearTimeout(this.timers.controls); + // Negate the argument if not undefined since adding the class to hides the controls + const force = typeof toggle === 'undefined' ? undefined : !toggle; - // If the mouse is not over the controls, set a timeout to hide them - if (show || this.paused || this.loading) { - // Check if controls toggled - const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, false); - - // Trigger event - if (toggled) { - utils.dispatchEvent.call(this, this.media, 'controlsshown'); - } + // Apply and get updated state + const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force); - // Always show controls when paused or if touch - if (this.paused || this.loading) { - return; + // Close menu + if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) { + controls.toggleMenu.call(this, false); } - - // Delay for hiding on touch - if (this.touch) { - delay = 3000; + // Trigger event on change + if (hiding !== isHidden) { + const eventName = hiding ? 'controlshidden' : 'controlsshown'; + utils.dispatchEvent.call(this, this.media, eventName); } + return !hiding; } - - // If toggle is false or if we're playing (regardless of toggle), - // then set the timer to hide the controls - if (!show || this.playing) { - this.timers.controls = setTimeout(() => { - // We need controls of course... - if (!utils.is.element(this.elements.controls)) { - return; - } - - // If the mouse is over the controls (and not entering fullscreen), bail - if ((this.elements.controls.pressed || this.elements.controls.hover) && !isEnterFullscreen) { - return; - } - - // Restore transition behaviour - if (!utils.hasClass(this.elements.container, this.config.classNames.hideControls)) { - utils.toggleClass(this.elements.controls, this.config.classNames.noTransition, false); - } - - // Set hideControls class - const toggled = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, this.config.hideControls); - - // Trigger event and close menu - if (toggled) { - utils.dispatchEvent.call(this, this.media, 'controlshidden'); - - if (this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) { - controls.toggleMenu.call(this, false); - } - } - }, delay); - } + return false; } /** diff --git a/src/js/ui.js b/src/js/ui.js index ea592d82..557599da 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -173,7 +173,7 @@ const ui = { } // Toggle controls - this.toggleControls(!this.playing); + ui.toggleControls.call(this); }, // Check if media is loading @@ -188,14 +188,24 @@ const ui = { // Timer to prevent flicker when seeking this.timers.loading = setTimeout(() => { - // Toggle container class hook + // Update progress bar loading class state utils.toggleClass(this.elements.container, this.config.classNames.loading, this.loading); - // Show controls if loading, hide if done - this.toggleControls(this.loading); + // Update controls visibility + ui.toggleControls.call(this); }, this.loading ? 250 : 0); }, + // Toggle controls based on state and `force` argument + toggleControls(force) { + const { controls } = this.elements; + + if (controls && this.config.hideControls) { + // Show controls if force, loading, paused, or button interaction, otherwise hide + this.toggleControls(Boolean(force || this.loading || this.paused || controls.pressed || controls.hover)); + } + }, + // Update volume UI and storage updateVolume() { if (!this.supported.ui) { -- cgit v1.2.3 From 333435a9c2ce263686052318497e604c424d38f3 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 18 May 2018 00:34:59 +0200 Subject: Fix playback state (paused) and events (play/pause) --- src/js/plugins/vimeo.js | 27 ++++++++++++++------------- src/js/plugins/youtube.js | 26 ++++++++++++++++---------- 2 files changed, 30 insertions(+), 23 deletions(-) (limited to 'src') diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index 96b36781..ec047c9b 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -7,6 +7,14 @@ import controls from './../controls'; import ui from './../ui'; import utils from './../utils'; +// Set playback state and trigger change (only on actual change) +function assurePlaybackState(play) { + if (this.media.paused === play) { + this.media.paused = !play; + utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); + } +} + const vimeo = { setup() { // Add embed class for responsive @@ -120,15 +128,13 @@ const vimeo = { // Create a faux HTML5 API using the Vimeo API player.media.play = () => { - player.embed.play().then(() => { - player.media.paused = false; - }); + assurePlaybackState.call(player, true); + return player.embed.play(); }; player.media.pause = () => { - player.embed.pause().then(() => { - player.media.paused = true; - }); + assurePlaybackState.call(player, false); + return player.embed.pause(); }; player.media.stop = () => { @@ -315,17 +321,12 @@ const vimeo = { }); player.embed.on('play', () => { - // Only fire play if paused before - if (player.media.paused) { - utils.dispatchEvent.call(player, player.media, 'play'); - } - player.media.paused = false; + assurePlaybackState.call(player, true); utils.dispatchEvent.call(player, player.media, 'playing'); }); player.embed.on('pause', () => { - player.media.paused = true; - utils.dispatchEvent.call(player, player.media, 'pause'); + assurePlaybackState.call(player, false); }); player.embed.on('timeupdate', data => { diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 10283998..794d099b 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -64,6 +64,14 @@ function mapQualityUnits(levels) { return utils.dedupe(levels.map(level => mapQualityUnit(level))); } +// Set playback state and trigger change (only on actual change) +function assurePlaybackState(play) { + if (this.media.paused === play) { + this.media.paused = !play; + utils.dispatchEvent.call(this, this.media, play ? 'play' : 'pause'); + } +} + const youtube = { setup() { // Add embed class for responsive @@ -264,10 +272,12 @@ const youtube = { // Create a faux HTML5 API using the YouTube API player.media.play = () => { + assurePlaybackState.call(player, true); instance.playVideo(); }; player.media.pause = () => { + assurePlaybackState.call(player, false); instance.pauseVideo(); }; @@ -438,7 +448,8 @@ const youtube = { break; case 0: - player.media.paused = true; + // Assure state and event + assurePlaybackState.call(player, false); // YouTube doesn't support loop for a single video, so mimick it. if (player.media.loop) { @@ -455,14 +466,11 @@ const youtube = { // If we were seeking, fire seeked event if (player.media.seeking) { utils.dispatchEvent.call(player, player.media, 'seeked'); + player.media.seeking = false; } - player.media.seeking = false; - // Only fire play if paused before - if (player.media.paused) { - utils.dispatchEvent.call(player, player.media, 'play'); - } - player.media.paused = false; + // Assure state and event (must be done after seeked event) + assurePlaybackState.call(player, true); utils.dispatchEvent.call(player, player.media, 'playing'); @@ -485,9 +493,7 @@ const youtube = { break; case 2: - player.media.paused = true; - - utils.dispatchEvent.call(player, player.media, 'pause'); + assurePlaybackState.call(player, false); break; -- cgit v1.2.3 From f8c89e3e95cb01a621f59d66c60f0fa2d76c4d58 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Fri, 18 May 2018 04:42:58 +0200 Subject: Fix #876: YouTube and Vimeo autoplays on seek --- src/js/plugins/vimeo.js | 40 ++++++++++++++++---------------- src/js/plugins/youtube.js | 58 ++++++++++++++++++++++++----------------------- 2 files changed, 50 insertions(+), 48 deletions(-) (limited to 'src') diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js index ec047c9b..46d4f3f9 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -149,25 +149,26 @@ const vimeo = { return currentTime; }, set(time) { - // Get current paused state - // Vimeo will automatically play on seek - const { paused } = player.media; - - // Set seeking flag - player.media.seeking = true; - - // Trigger seeking - utils.dispatchEvent.call(player, player.media, 'seeking'); - - // Seek after events - player.embed.setCurrentTime(time).catch(() => { - // Do nothing - }); - - // Restore pause state - if (paused) { - player.pause(); - } + // Vimeo will automatically play on seek if the video hasn't been played before + + // Get current paused state and volume etc + const { embed, media, paused, volume } = player; + + // Set seeking state and trigger event + media.seeking = true; + utils.dispatchEvent.call(player, media, 'seeking'); + + // If paused, mute until seek is complete + Promise.resolve(paused && embed.setVolume(0)) + // Seek + .then(() => embed.setCurrentTime(time)) + // Restore paused + .then(() => paused && embed.pause()) + // Restore volume + .then(() => paused && embed.setVolume(volume)) + .catch(() => { + // Do nothing + }); }, }); @@ -357,7 +358,6 @@ const vimeo = { player.embed.on('seeked', () => { player.media.seeking = false; utils.dispatchEvent.call(player, player.media, 'seeked'); - utils.dispatchEvent.call(player, player.media, 'play'); }); player.embed.on('ended', () => { diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 794d099b..391da6ca 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -295,22 +295,17 @@ const youtube = { return Number(instance.getCurrentTime()); }, set(time) { - // Vimeo will automatically play on seek - const { paused } = player.media; + // If paused, mute audio preventively (YouTube starts playing on seek if the video hasn't been played yet). + if (player.paused) { + player.embed.mute(); + } - // Set seeking flag + // Set seeking state and trigger event player.media.seeking = true; - - // Trigger seeking utils.dispatchEvent.call(player, player.media, 'seeking'); // Seek after events sent instance.seekTo(time); - - // Restore pause state - if (paused) { - player.pause(); - } }, }); @@ -448,7 +443,6 @@ const youtube = { break; case 0: - // Assure state and event assurePlaybackState.call(player, false); // YouTube doesn't support loop for a single video, so mimick it. @@ -465,34 +459,42 @@ const youtube = { case 1: // If we were seeking, fire seeked event if (player.media.seeking) { - utils.dispatchEvent.call(player, player.media, 'seeked'); player.media.seeking = false; + utils.dispatchEvent.call(player, player.media, 'seeked'); } - // Assure state and event (must be done after seeked event) - assurePlaybackState.call(player, true); + // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) + if (player.media.paused) { + player.media.pause(); + } else { + assurePlaybackState.call(player, true); - utils.dispatchEvent.call(player, player.media, 'playing'); + utils.dispatchEvent.call(player, player.media, 'playing'); - // Poll to get playback progress - player.timers.playing = setInterval(() => { - utils.dispatchEvent.call(player, player.media, 'timeupdate'); - }, 50); + // Poll to get playback progress + player.timers.playing = setInterval(() => { + utils.dispatchEvent.call(player, player.media, 'timeupdate'); + }, 50); - // Check duration again due to YouTube bug - // https://github.com/sampotts/plyr/issues/374 - // https://code.google.com/p/gdata-issues/issues/detail?id=8690 - if (player.media.duration !== instance.getDuration()) { - player.media.duration = instance.getDuration(); - utils.dispatchEvent.call(player, player.media, 'durationchange'); - } + // Check duration again due to YouTube bug + // https://github.com/sampotts/plyr/issues/374 + // https://code.google.com/p/gdata-issues/issues/detail?id=8690 + if (player.media.duration !== instance.getDuration()) { + player.media.duration = instance.getDuration(); + utils.dispatchEvent.call(player, player.media, 'durationchange'); + } - // Get quality - controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); + // Get quality + controls.setQualityMenu.call(player, mapQualityUnits(instance.getAvailableQualityLevels())); + } break; case 2: + // Restore audio (YouTube starts playing on seek if the video hasn't been played yet) + if (!player.muted) { + player.embed.unMute(); + } assurePlaybackState.call(player, false); break; -- cgit v1.2.3 From 723298a07b4099486f6c071167979a8f8e2abed2 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sat, 12 May 2018 10:09:53 +0200 Subject: Fix #921: Trigger seeked event in youtube plugin if either playing or paused --- src/js/plugins/youtube.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js index 391da6ca..67b8093e 100644 --- a/src/js/plugins/youtube.js +++ b/src/js/plugins/youtube.js @@ -424,6 +424,17 @@ const youtube = { // Reset timer clearInterval(player.timers.playing); + const seeked = player.media.seeking && [ + 1, + 2, + ].includes(event.data); + + if (seeked) { + // Unset seeking and fire seeked event + player.media.seeking = false; + utils.dispatchEvent.call(player, player.media, 'seeked'); + } + // Handle events // -1 Unstarted // 0 Ended @@ -457,12 +468,6 @@ const youtube = { break; case 1: - // If we were seeking, fire seeked event - if (player.media.seeking) { - player.media.seeking = false; - utils.dispatchEvent.call(player, player.media, 'seeked'); - } - // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet) if (player.media.paused) { player.media.pause(); -- cgit v1.2.3 From aa8fc313a91ec38b6c430c9b88eec7944948c521 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sat, 12 May 2018 11:24:50 +0200 Subject: Fix #966: Add 'seeked' event listener to update progress (seeking doesn't have the correct time) --- src/js/controls.js | 1 + src/js/listeners.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 3f720925..61fb2039 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -481,6 +481,7 @@ const controls = { // Video playing case 'timeupdate': case 'seeking': + case 'seeked': value = utils.getPercentage(this.currentTime, this.duration); // Set seek range value only if it's a 'natural' time event diff --git a/src/js/listeners.js b/src/js/listeners.js index d5748806..99eeade4 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -273,7 +273,7 @@ class Listeners { // Listen for media events media() { // Time change on media - utils.on(this.player.media, 'timeupdate seeking', event => controls.timeUpdate.call(this.player, event)); + utils.on(this.player.media, 'timeupdate seeking seeked', event => controls.timeUpdate.call(this.player, event)); // Display duration utils.on(this.player.media, 'durationchange loadeddata loadedmetadata', event => controls.durationUpdate.call(this.player, event)); @@ -295,7 +295,7 @@ class Listeners { }); // Check for buffer progress - utils.on(this.player.media, 'progress playing', event => controls.updateProgress.call(this.player, event)); + utils.on(this.player.media, 'progress playing seeking seeked', event => controls.updateProgress.call(this.player, event)); // Handle volume changes utils.on(this.player.media, 'volumechange', event => controls.updateVolume.call(this.player, event)); -- cgit v1.2.3 From 121093ae7113cbd74d4202332a88d184c0049320 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Sat, 12 May 2018 11:54:28 +0200 Subject: Prevent durationchange events from showing time when invertTime is false --- src/js/controls.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index 61fb2039..d266ed6b 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -602,9 +602,10 @@ const controls = { controls.updateProgress.call(this, event); }, - // Show the duration on metadataloaded + // Show the duration on metadataloaded or durationchange events durationUpdate() { - if (!this.supported.ui) { + // Bail if no ui or durationchange event triggered after playing/seek when invertTime is false + if (!this.supported.ui || (!this.config.invertTime && this.currentTime)) { return; } -- cgit v1.2.3 From 41f9a87e0ec01146fe870fa2412ef6d4004221c1 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 20 May 2018 23:40:00 +1000 Subject: Add URL polyfill --- src/js/plyr.polyfilled.js | 1 + 1 file changed, 1 insertion(+) (limited to 'src') diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 635dee63..27b13dfd 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -7,6 +7,7 @@ import 'babel-polyfill'; import 'custom-event-polyfill'; +import 'url-polyfill'; import Plyr from './plyr'; export default Plyr; -- cgit v1.2.3 From d733454d7f1d8441c11c625ef56d1991984be70a Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 20 May 2018 23:40:28 +1000 Subject: Pause while seeking --- src/js/listeners.js | 43 +++++++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/js/listeners.js b/src/js/listeners.js index 99eeade4..b23238d2 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -550,15 +550,34 @@ class Listeners { }); // Set range input alternative "value", which matches the tooltip time (#954) - on( - this.player.elements.inputs.seek, - 'mousedown mousemove', - event => { - const clientRect = this.player.elements.progress.getBoundingClientRect(); - const percent = 100 / clientRect.width * (event.pageX - clientRect.left); - event.currentTarget.setAttribute('seekNext', percent); + on(this.player.elements.inputs.seek, 'mousedown mousemove', event => { + const clientRect = this.player.elements.progress.getBoundingClientRect(); + const percent = 100 / clientRect.width * (event.pageX - clientRect.left); + event.currentTarget.setAttribute('seek', percent); + }); + + // Pause while seeking + on(this.player.elements.inputs.seek, 'mousedown mouseup keydown keyup touchstart touchend', event => { + const seek = event.currentTarget; + + // Was playing before? + const play = seek.hasAttribute('play-on-seeked'); + + // Done seeking + const done = [ + 'mouseup', + 'touchend', + 'keyup', + ].includes(event.type); + + if (play && done) { + seek.removeAttribute('play-on-seeked'); + this.player.play(); + } else if (!done && this.player.playing) { + seek.setAttribute('play-on-seeked', ''); + this.player.pause(); } - ); + }); // Seek on( @@ -566,12 +585,16 @@ class Listeners { inputEvent, event => { const seek = event.currentTarget; + // If it exists, use seekNext instead of "value" for consistency with tooltip time (#954) - let seekTo = seek.getAttribute('seekNext'); + let seekTo = seek.getAttribute('seek'); + if (utils.is.empty(seekTo)) { seekTo = seek.value; } - seek.removeAttribute('seekNext'); + + seek.removeAttribute('seek'); + this.player.currentTime = seekTo / seek.max * this.player.duration; }, 'seek', -- cgit v1.2.3 From edd67b0da36ac57e98d70223d6f4eacd4548e096 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sun, 20 May 2018 23:44:40 +1000 Subject: Typo --- src/js/listeners.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/js/listeners.js b/src/js/listeners.js index b23238d2..86236fe3 100644 --- a/src/js/listeners.js +++ b/src/js/listeners.js @@ -553,7 +553,7 @@ class Listeners { on(this.player.elements.inputs.seek, 'mousedown mousemove', event => { const clientRect = this.player.elements.progress.getBoundingClientRect(); const percent = 100 / clientRect.width * (event.pageX - clientRect.left); - event.currentTarget.setAttribute('seek', percent); + event.currentTarget.setAttribute('seek-value', percent); }); // Pause while seeking @@ -570,6 +570,7 @@ class Listeners { 'keyup', ].includes(event.type); + // If we're done seeking and it was playing, resume playback if (play && done) { seek.removeAttribute('play-on-seeked'); this.player.play(); @@ -586,14 +587,14 @@ class Listeners { event => { const seek = event.currentTarget; - // If it exists, use seekNext instead of "value" for consistency with tooltip time (#954) - let seekTo = seek.getAttribute('seek'); + // If it exists, use seek-value instead of "value" for consistency with tooltip time (#954) + let seekTo = seek.getAttribute('seek-value'); if (utils.is.empty(seekTo)) { seekTo = seek.value; } - seek.removeAttribute('seek'); + seek.removeAttribute('seek-value'); this.player.currentTime = seekTo / seek.max * this.player.duration; }, -- cgit v1.2.3 From cd51788b980a7bc7b5caaf2d595d2077be4138f5 Mon Sep 17 00:00:00 2001 From: Sam Potts Date: Sat, 26 May 2018 13:53:15 +1000 Subject: v3.3.8 --- src/js/controls.js | 1 - src/js/defaults.js | 2 +- src/js/plyr.js | 2 +- src/js/plyr.polyfilled.js | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/js/controls.js b/src/js/controls.js index d266ed6b..c76bd66b 100644 --- a/src/js/controls.js +++ b/src/js/controls.js @@ -1165,7 +1165,6 @@ const controls = { const tooltip = utils.createElement( 'span', { - role: 'tooltip', class: this.config.classNames.tooltip, }, '00:00', diff --git a/src/js/defaults.js b/src/js/defaults.js index f66a7c2f..5b1a4dd3 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.3.7/plyr.svg', + iconUrl: 'https://cdn.plyr.io/3.3.8/plyr.svg', // Blank video (used to prevent errors on source change) blankVideo: 'https://cdn.plyr.io/static/blank.mp4', diff --git a/src/js/plyr.js b/src/js/plyr.js index 4c569fec..4c984fd7 100644 --- a/src/js/plyr.js +++ b/src/js/plyr.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr -// plyr.js v3.3.7 +// plyr.js v3.3.8 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js index 27b13dfd..9570d753 100644 --- a/src/js/plyr.polyfilled.js +++ b/src/js/plyr.polyfilled.js @@ -1,6 +1,6 @@ // ========================================================================== // Plyr Polyfilled Build -// plyr.js v3.3.7 +// plyr.js v3.3.8 // https://github.com/sampotts/plyr // License: The MIT License (MIT) // ========================================================================== -- cgit v1.2.3