aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSam Potts <sam@potts.es>2018-05-26 13:55:22 +1000
committerGitHub <noreply@github.com>2018-05-26 13:55:22 +1000
commitf0be913dc384c5a229af70b69711ab246226c1a3 (patch)
tree1919f9f0ead11712f3a94eeed2e5b6da0130c8f9 /src
parentc41bb657ac490d032f2992845d21a457a7faedf1 (diff)
parentcd51788b980a7bc7b5caaf2d595d2077be4138f5 (diff)
downloadplyr-f0be913dc384c5a229af70b69711ab246226c1a3.tar.lz
plyr-f0be913dc384c5a229af70b69711ab246226c1a3.tar.xz
plyr-f0be913dc384c5a229af70b69711ab246226c1a3.zip
Merge pull request #975 from sampotts/develop
v3.3.8
Diffstat (limited to 'src')
-rw-r--r--src/js/controls.js7
-rw-r--r--src/js/defaults.js5
-rw-r--r--src/js/listeners.js145
-rw-r--r--src/js/plugins/vimeo.js74
-rw-r--r--src/js/plugins/youtube.js103
-rw-r--r--src/js/plyr.js130
-rw-r--r--src/js/plyr.polyfilled.js3
-rw-r--r--src/js/ui.js77
-rw-r--r--src/js/utils.js29
-rw-r--r--src/sass/components/poster.scss2
-rw-r--r--src/sass/plyr.scss1
-rw-r--r--src/sass/states/error.scss25
12 files changed, 301 insertions, 300 deletions
diff --git a/src/js/controls.js b/src/js/controls.js
index 3f720925..c76bd66b 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
@@ -601,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;
}
@@ -1163,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 f160b1aa..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',
@@ -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,13 +331,13 @@ 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',
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 e95c5e44..86236fe3 100644
--- a/src/js/listeners.js
+++ b/src/js/listeners.js
@@ -238,19 +238,42 @@ 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
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));
@@ -272,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));
@@ -283,9 +306,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', () => {
@@ -530,15 +550,35 @@ 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-value', 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 we're done seeking and it was playing, resume playback
+ 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(
@@ -546,12 +586,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');
+
+ // 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('seekNext');
+
+ seek.removeAttribute('seek-value');
+
this.player.currentTime = seekTo / seek.max * this.player.duration;
},
'seek',
@@ -592,26 +636,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/plugins/vimeo.js b/src/js/plugins/vimeo.js
index 0ceb89e5..46d4f3f9 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
@@ -99,11 +107,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
@@ -123,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 = () => {
@@ -146,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
+ });
},
});
@@ -318,17 +322,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 => {
@@ -359,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 4fde9319..67b8093e 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
@@ -162,7 +170,19 @@ 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))
+ .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
@@ -252,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();
};
@@ -273,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();
- }
},
});
@@ -407,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
@@ -426,7 +454,7 @@ const youtube = {
break;
case 0:
- player.media.paused = true;
+ assurePlaybackState.call(player, false);
// YouTube doesn't support loop for a single video, so mimick it.
if (player.media.loop) {
@@ -440,42 +468,39 @@ const youtube = {
break;
case 1:
- // If we were seeking, fire seeked event
- if (player.media.seeking) {
- utils.dispatchEvent.call(player, player.media, 'seeked');
- }
- player.media.seeking = false;
-
- // Only fire play if paused before
+ // Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
if (player.media.paused) {
- utils.dispatchEvent.call(player, player.media, 'play');
- }
- player.media.paused = false;
+ 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:
- player.media.paused = true;
-
- utils.dispatchEvent.call(player, player.media, 'pause');
+ // 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;
diff --git a/src/js/plyr.js b/src/js/plyr.js
index bed09827..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)
// ==========================================================================
@@ -802,10 +802,7 @@ class Plyr {
return;
}
- if (utils.is.string(input)) {
- this.media.setAttribute('poster', input);
- ui.setPoster.call(this);
- }
+ ui.setPoster.call(this, input);
}
/**
@@ -971,119 +968,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 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);
- // Don't hide if no UI support or it's audio
- if (!this.supported.ui || this.isAudio) {
- return;
- }
+ // Negate the argument if not undefined since adding the class to hides the controls
+ const force = typeof toggle === 'undefined' ? undefined : !toggle;
- let delay = 0;
- let show = toggle;
- let isEnterFullscreen = false;
+ // Apply and get updated state
+ const hiding = utils.toggleClass(this.elements.container, this.config.classNames.hideControls, force);
- // 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);
- }
- }
-
- // Clear timer on every call
- clearTimeout(this.timers.controls);
-
- // 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');
+ // Close menu
+ if (hiding && this.config.controls.includes('settings') && !utils.is.empty(this.config.settings)) {
+ controls.toggleMenu.call(this, false);
}
-
- // Always show controls when paused or if touch
- if (this.paused || this.loading) {
- return;
+ // Trigger event on change
+ if (hiding !== isHidden) {
+ const eventName = hiding ? 'controlshidden' : 'controlsshown';
+ utils.dispatchEvent.call(this, this.media, eventName);
}
-
- // Delay for hiding on touch
- if (this.touch) {
- delay = 3000;
- }
- }
-
- // 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 !hiding;
}
+ return false;
}
/**
diff --git a/src/js/plyr.polyfilled.js b/src/js/plyr.polyfilled.js
index 635dee63..9570d753 100644
--- a/src/js/plyr.polyfilled.js
+++ b/src/js/plyr.polyfilled.js
@@ -1,12 +1,13 @@
// ==========================================================================
// Plyr Polyfilled Build
-// plyr.js v3.3.7
+// plyr.js v3.3.8
// https://github.com/sampotts/plyr
// License: The MIT License (MIT)
// ==========================================================================
import 'babel-polyfill';
import 'custom-event-polyfill';
+import 'url-polyfill';
import Plyr from './plyr';
export default Plyr;
diff --git a/src/js/ui.js b/src/js/ui.js
index 8f3f6a77..3a8f2d05 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 element was created
+ if (this.poster && this.elements.poster && !this.elements.poster.style.backgroundImage) {
+ ui.setPoster.call(this, this.poster);
+ }
},
// Setup aria attribute for play and iframe title
@@ -146,15 +148,39 @@ 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}')`;
+ 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;
+ });
+
+ // 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
@@ -173,7 +199,7 @@ const ui = {
}
// Toggle controls
- this.toggleControls(!this.playing);
+ ui.toggleControls.call(this);
},
// Check if media is loading
@@ -188,35 +214,22 @@ 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);
},
- // Check if media failed to load
- checkFailed() {
- // https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/networkState
- this.failed = this.media.networkState === 3;
+ // Toggle controls based on state and `force` argument
+ toggleControls(force) {
+ const { controls } = this.elements;
- if (this.failed) {
- utils.toggleClass(this.elements.container, this.config.classNames.loading, false);
- utils.toggleClass(this.elements.container, this.config.classNames.error, true);
+ 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));
}
-
- // 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);
},
};
diff --git a/src/js/utils.js b/src/js/utils.js
index a58d8555..0c5a28d7 100644
--- a/src/js/utils.js
+++ b/src/js/utils.js
@@ -120,6 +120,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) => {
@@ -393,14 +408,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;
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;
}
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;
- }
-}