From 90d5b48845661ce99a204354f93fbbbc7a19f100 Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 15 May 2018 04:23:27 +0200 Subject: [PATCH 1/3] Add async method to utils for loading/checking images --- src/js/utils.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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) => { From 16c3a7d9e5be8ed2ffbbcee1c786b88d1cecc4cd Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 15 May 2018 05:16:06 +0200 Subject: [PATCH 2/3] 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(-) 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; } From c845558d960412ad5e942334fd9f60ed173e0a5a Mon Sep 17 00:00:00 2001 From: Albin Larsson Date: Tue, 15 May 2018 16:22:51 +0200 Subject: [PATCH 3/3] 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(-) 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; });