aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/js/config/defaults.js9
-rw-r--r--src/js/fullscreen.js6
-rw-r--r--src/js/listeners.js27
-rw-r--r--src/js/media.js1
-rw-r--r--src/js/plugins/vimeo.js34
-rw-r--r--src/js/plugins/youtube.js65
-rw-r--r--src/js/plyr.d.ts109
-rw-r--r--src/js/plyr.js6
-rw-r--r--src/js/ui.js5
-rw-r--r--src/js/utils/style.js6
-rw-r--r--src/sass/base.scss1
-rw-r--r--src/sass/components/sliders.scss2
-rw-r--r--src/sass/components/times.scss2
13 files changed, 173 insertions, 100 deletions
diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js
index 03c75150..8938ede9 100644
--- a/src/js/config/defaults.js
+++ b/src/js/config/defaults.js
@@ -422,20 +422,23 @@ const defaults = {
title: false,
speed: true,
transparent: false,
+ // Custom settings from Plyr
+ customControls: false,
+ referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
// Whether the owner of the video has a Pro or Business account
// (which allows us to properly hide controls without CSS hacks, etc)
premium: false,
- // Custom settings from Plyr
- referrerPolicy: null, // https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/referrerPolicy
},
// YouTube plugin
youtube: {
- noCookie: true, // Whether to use an alternative version of YouTube without cookies
rel: 0, // No related vids
showinfo: 0, // Hide info
iv_load_policy: 3, // Hide annotations
modestbranding: 1, // Hide logos as much as possible (they still show one in the corner when paused)
+ // Custom settings from Plyr
+ customControls: false,
+ noCookie: false, // Whether to use an alternative version of YouTube without cookies
},
};
diff --git a/src/js/fullscreen.js b/src/js/fullscreen.js
index d545d144..7bb22391 100644
--- a/src/js/fullscreen.js
+++ b/src/js/fullscreen.js
@@ -49,7 +49,7 @@ class Fullscreen {
return;
}
- this.toggle();
+ this.player.listeners.proxy(event, this.toggle, 'fullscreen');
});
// Tap focus when in fullscreen
@@ -145,8 +145,10 @@ class Fullscreen {
button.pressed = this.active;
}
+ // Always trigger events on the plyr / media element (not a fullscreen container) and let them bubble up
+ const target = this.target === this.player.media ? this.target : this.player.elements.container;
// Trigger an event
- triggerEvent.call(this.player, this.target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
+ triggerEvent.call(this.player, target, this.active ? 'enterfullscreen' : 'exitfullscreen', true);
}
toggleFallback(toggle = false) {
diff --git a/src/js/listeners.js b/src/js/listeners.js
index 8b41f25d..48734bcf 100644
--- a/src/js/listeners.js
+++ b/src/js/listeners.js
@@ -356,6 +356,11 @@ class Listeners {
// Set Vimeo gutter
setGutter(ratio, padding, isEnter);
+ // Horrible hack for Safari 14 not repainting properly on entering fullscreen
+ if (isEnter) {
+ setTimeout(() => repaint(elements.container), 100);
+ }
+
// If not using native browser fullscreen API, we need to check for resizes of viewport
if (!usingNative) {
if (isEnter) {
@@ -569,10 +574,28 @@ class Listeners {
this.bind(elements.buttons.restart, 'click', player.restart, 'restart');
// Rewind
- this.bind(elements.buttons.rewind, 'click', player.rewind, 'rewind');
+ this.bind(
+ elements.buttons.rewind,
+ 'click',
+ () => {
+ // Record seek time so we can prevent hiding controls for a few seconds after rewind
+ player.lastSeekTime = Date.now();
+ player.rewind();
+ },
+ 'rewind',
+ );
// Rewind
- this.bind(elements.buttons.fastForward, 'click', player.forward, 'fastForward');
+ this.bind(
+ elements.buttons.fastForward,
+ 'click',
+ () => {
+ // Record seek time so we can prevent hiding controls for a few seconds after fast forward
+ player.lastSeekTime = Date.now();
+ player.forward();
+ },
+ 'fastForward',
+ );
// Mute toggle
this.bind(
diff --git a/src/js/media.js b/src/js/media.js
index 4584fea3..ddac5ebf 100644
--- a/src/js/media.js
+++ b/src/js/media.js
@@ -41,6 +41,7 @@ const media = {
// Poster image container
this.elements.poster = createElement('div', {
class: this.config.classNames.poster,
+ hidden: '',
});
this.elements.wrapper.appendChild(this.elements.poster);
diff --git a/src/js/plugins/vimeo.js b/src/js/plugins/vimeo.js
index a336ea48..b050cc53 100644
--- a/src/js/plugins/vimeo.js
+++ b/src/js/plugins/vimeo.js
@@ -112,25 +112,29 @@ const vimeo = {
}
// Inject the package
- const { poster } = player;
- if (premium) {
- iframe.setAttribute('data-poster', poster);
+ if (premium || !config.customControls) {
+ iframe.setAttribute('data-poster', player.poster);
player.media = replaceElement(iframe, player.media);
} else {
- const wrapper = createElement('div', { class: player.config.classNames.embedContainer, 'data-poster': poster });
+ const wrapper = createElement('div', {
+ class: player.config.classNames.embedContainer,
+ 'data-poster': player.poster,
+ });
wrapper.appendChild(iframe);
player.media = replaceElement(wrapper, player.media);
}
-
+
// Get poster image
- fetch(format(player.config.urls.vimeo.api, src)).then(response => {
- if (is.empty(response) || !response.thumbnail_url) {
- return;
- }
-
- // Set and show poster
- ui.setPoster.call(player, response.thumbnail_url).catch(() => { });
- });
+ if (!config.customControls) {
+ fetch(format(player.config.urls.vimeo.api, src)).then((response) => {
+ if (is.empty(response) || !response.thumbnail_url) {
+ return;
+ }
+
+ // Set and show poster
+ ui.setPoster.call(player, response.thumbnail_url).catch(() => {});
+ });
+ }
// Setup instance
// https://github.com/vimeo/player.js
@@ -401,7 +405,9 @@ const vimeo = {
});
// Rebuild UI
- setTimeout(() => ui.build.call(player), 0);
+ if (config.customControls) {
+ setTimeout(() => ui.build.call(player), 0);
+ }
},
};
diff --git a/src/js/plugins/youtube.js b/src/js/plugins/youtube.js
index 88601d5e..db5781e6 100644
--- a/src/js/plugins/youtube.js
+++ b/src/js/plugins/youtube.js
@@ -104,6 +104,7 @@ const youtube = {
// API ready
ready() {
const player = this;
+ const config = player.config.youtube;
// Ignore already setup (race condition)
const currentId = player.media && player.media.getAttribute('id');
if (!is.empty(currentId) && currentId.startsWith('youtube-')) {
@@ -121,43 +122,46 @@ const youtube = {
// Replace the <iframe> with a <div> due to YouTube API issues
const videoId = parseId(source);
const id = generateId(player.provider);
- // Get poster, if already set
- const { poster } = player;
// Replace media element
- const container = createElement('div', { id, 'data-poster': poster });
+ const container = createElement('div', { id, 'data-poster': config.customControls ? player.poster : undefined });
player.media = replaceElement(container, player.media);
- // Id to poster wrapper
- const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
-
- // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
- loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
- .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
- .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
- .then((image) => ui.setPoster.call(player, image.src))
- .then((src) => {
- // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
- if (!src.includes('maxres')) {
- player.elements.poster.style.backgroundSize = 'cover';
- }
- })
- .catch(() => {});
-
- const config = player.config.youtube;
+ // Only load the poster when using custom controls
+ if (config.customControls) {
+ const posterSrc = (s) => `https://i.ytimg.com/vi/${videoId}/${s}default.jpg`;
+
+ // Check thumbnail images in order of quality, but reject fallback thumbnails (120px wide)
+ loadImage(posterSrc('maxres'), 121) // Higest quality and unpadded
+ .catch(() => loadImage(posterSrc('sd'), 121)) // 480p padded 4:3
+ .catch(() => loadImage(posterSrc('hq'))) // 360p padded 4:3. Always exists
+ .then((image) => ui.setPoster.call(player, image.src))
+ .then((src) => {
+ // If the image is padded, use background-size "cover" instead (like youtube does too with their posters)
+ if (!src.includes('maxres')) {
+ player.elements.poster.style.backgroundSize = 'cover';
+ }
+ })
+ .catch(() => {});
+ }
// Setup instance
// https://developers.google.com/youtube/iframe_api_reference
- player.embed = new window.YT.Player(id, {
+ player.embed = new window.YT.Player(player.media, {
videoId,
host: getHost(config),
playerVars: extend(
{},
{
- autoplay: player.config.autoplay ? 1 : 0, // Autoplay
- hl: player.config.hl, // iframe interface language
- controls: player.supported.ui ? 0 : 1, // Only show controls if not fully supported
- disablekb: 1, // Disable keyboard as we handle it
- playsinline: !player.config.fullscreen.iosNative ? 1 : 0, // Allow iOS inline playback
+ // Autoplay
+ autoplay: player.config.autoplay ? 1 : 0,
+ // iframe interface language
+ hl: player.config.hl,
+ // Only show controls if not fully supported or opted out
+ controls: player.supported.ui && config.customControls ? 0 : 1,
+ // Disable keyboard as we handle it
+ disablekb: 1,
+ // Allow iOS inline playback
+ playsinline: !player.config.fullscreen.iosNative ? 1 : 0,
// Captions are flaky on YouTube
cc_load_policy: player.captions.active ? 1 : 0,
cc_lang_pref: player.config.captions.language,
@@ -278,6 +282,7 @@ const youtube = {
const toggle = is.boolean(input) ? input : muted;
muted = toggle;
instance[toggle ? 'mute' : 'unMute']();
+ instance.setVolume(volume * 100);
triggerEvent.call(player, player.media, 'volumechange');
},
});
@@ -302,7 +307,7 @@ const youtube = {
player.options.speed = speeds.filter((s) => player.config.speed.options.includes(s));
// Set the tabindex to avoid focus entering iframe
- if (player.supported.ui) {
+ if (player.supported.ui && config.customControls) {
player.media.setAttribute('tabindex', -1);
}
@@ -335,7 +340,9 @@ const youtube = {
}, 200);
// Rebuild UI
- setTimeout(() => ui.build.call(player), 50);
+ if (config.customControls) {
+ setTimeout(() => ui.build.call(player), 50);
+ }
},
onStateChange(event) {
// Get the instance
@@ -386,7 +393,7 @@ const youtube = {
case 1:
// Restore paused state (YouTube starts playing on seek if the video hasn't been played yet)
- if (!player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
+ if (config.customControls && !player.config.autoplay && player.media.paused && !player.embed.hasPlayed) {
player.media.pause();
} else {
assurePlaybackState.call(player, true);
diff --git a/src/js/plyr.d.ts b/src/js/plyr.d.ts
index 13523b35..4b332aeb 100644
--- a/src/js/plyr.d.ts
+++ b/src/js/plyr.d.ts
@@ -214,26 +214,17 @@ declare class Plyr {
/**
* Add an event listener for the specified event.
*/
- on(
- event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
- callback: (this: this, event: Plyr.PlyrEvent) => void,
- ): void;
+ on<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Add an event listener for the specified event once.
*/
- once(
- event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
- callback: (this: this, event: Plyr.PlyrEvent) => void,
- ): void;
+ once<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Remove an event listener for the specified event.
*/
- off(
- event: Plyr.StandardEvent | Plyr.Html5Event | Plyr.YoutubeEvent,
- callback: (this: this, event: Plyr.PlyrEvent) => void,
- ): void;
+ off<K extends keyof Plyr.PlyrEventMap>(event: K, callback: (this: this, event: Plyr.PlyrEventMap[K]) => void): void;
/**
* Check support for a mime type.
@@ -249,37 +240,51 @@ declare class Plyr {
declare namespace Plyr {
type MediaType = 'audio' | 'video';
type Provider = 'html5' | 'youtube' | 'vimeo';
- type StandardEvent =
- | 'progress'
- | 'playing'
- | 'play'
- | 'pause'
- | 'timeupdate'
- | 'volumechange'
- | 'seeking'
- | 'seeked'
- | 'ratechange'
- | 'ended'
- | 'enterfullscreen'
- | 'exitfullscreen'
- | 'captionsenabled'
- | 'captionsdisabled'
- | 'languagechange'
- | 'controlshidden'
- | 'controlsshown'
- | 'ready';
- type Html5Event =
- | 'loadstart'
- | 'loadeddata'
- | 'loadedmetadata'
- | 'canplay'
- | 'canplaythrough'
- | 'stalled'
- | 'waiting'
- | 'emptied'
- | 'cuechange'
- | 'error';
- type YoutubeEvent = 'statechange' | 'qualitychange' | 'qualityrequested';
+ type StandardEventMap = {
+ progress: PlyrEvent;
+ playing: PlyrEvent;
+ play: PlyrEvent;
+ pause: PlyrEvent;
+ timeupdate: PlyrEvent;
+ volumechange: PlyrEvent;
+ seeking: PlyrEvent;
+ seeked: PlyrEvent;
+ ratechange: PlyrEvent;
+ ended: PlyrEvent;
+ enterfullscreen: PlyrEvent;
+ exitfullscreen: PlyrEvent;
+ captionsenabled: PlyrEvent;
+ captionsdisabled: PlyrEvent;
+ languagechange: PlyrEvent;
+ controlshidden: PlyrEvent;
+ controlsshown: PlyrEvent;
+ ready: PlyrEvent;
+ };
+ // For retrocompatibility, we keep StandadEvent
+ type StandadEvent = keyof Plyr.StandardEventMap;
+ type Html5EventMap = {
+ loadstart: PlyrEvent;
+ loadeddata: PlyrEvent;
+ loadedmetadata: PlyrEvent;
+ canplay: PlyrEvent;
+ canplaythrough: PlyrEvent;
+ stalled: PlyrEvent;
+ waiting: PlyrEvent;
+ emptied: PlyrEvent;
+ cuechange: PlyrEvent;
+ error: PlyrEvent;
+ };
+ // For retrocompatibility, we keep Html5Event
+ type Html5Event = keyof Plyr.Html5EventMap;
+ type YoutubeEventMap = {
+ statechange: PlyrStateChangeEvent;
+ qualitychange: PlyrEvent;
+ qualityrequested: PlyrEvent;
+ };
+ // For retrocompatibility, we keep YoutubeEvent
+ type YoutubeEvent = keyof Plyr.YoutubeEventMap;
+
+ type PlyrEventMap = StandardEventMap & Html5EventMap & YoutubeEventMap;
interface FullscreenControl {
/**
@@ -552,7 +557,7 @@ declare namespace Plyr {
interface PreviewThumbnailsOptions {
enabled?: boolean;
- src?: string;
+ src?: string | string[];
}
interface SourceInfo {
@@ -623,6 +628,22 @@ declare namespace Plyr {
readonly detail: { readonly plyr: Plyr };
}
+ enum YoutubeState {
+ UNSTARTED = -1,
+ ENDED = 0,
+ PLAYING = 1,
+ PAUSED = 2,
+ BUFFERING = 3,
+ CUED = 5,
+ }
+
+ interface PlyrStateChangeEvent extends CustomEvent {
+ readonly detail: {
+ readonly plyr: Plyr;
+ readonly code: YoutubeState;
+ };
+ }
+
interface Support {
api: boolean;
ui: boolean;
diff --git a/src/js/plyr.js b/src/js/plyr.js
index 420910a0..f1dcc68a 100644
--- a/src/js/plyr.js
+++ b/src/js/plyr.js
@@ -12,6 +12,7 @@ import { getProviderByUrl, providers, types } from './config/types';
import Console from './console';
import controls from './controls';
import Fullscreen from './fullscreen';
+import html5 from './html5';
import Listeners from './listeners';
import media from './media';
import Ads from './plugins/ads';
@@ -308,7 +309,7 @@ class Plyr {
// Autoplay if required
if (this.isHTML5 && this.config.autoplay) {
- setTimeout(() => silencePromise(this.play()), 10);
+ this.once('canplay', () => silencePromise(this.play()));
}
// Seek time will be recorded (in listeners.js) so we can prevent hiding controls for a few seconds after seek
@@ -1145,6 +1146,9 @@ class Plyr {
// Unbind listeners
unbindListeners.call(this);
+ // Cancel current network requests
+ html5.cancelRequests.call(this);
+
// Replace the container with the original element provided
replaceElement(this.elements.original, this.elements.container);
diff --git a/src/js/ui.js b/src/js/ui.js
index f5868788..c8b19677 100644
--- a/src/js/ui.js
+++ b/src/js/ui.js
@@ -172,6 +172,9 @@ const ui = {
// Set property synchronously to respect the call order
this.media.setAttribute('data-poster', poster);
+ // Show the poster
+ this.elements.poster.removeAttribute('hidden');
+
// Wait until ui is ready
return (
ready
@@ -270,7 +273,7 @@ const ui = {
// Loop through values (as they are the keys when the object is spread 🤔)
Object.values({ ...this.media.style })
// We're only fussed about Plyr specific properties
- .filter((key) => !is.empty(key) && key.startsWith('--plyr'))
+ .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))
.forEach((key) => {
// Set on the container
this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));
diff --git a/src/js/utils/style.js b/src/js/utils/style.js
index fcb089b4..f02b0ba5 100644
--- a/src/js/utils/style.js
+++ b/src/js/utils/style.js
@@ -68,7 +68,11 @@ export function setAspectRatio(input) {
const height = (100 / this.media.offsetWidth) * parseInt(window.getComputedStyle(this.media).paddingBottom, 10);
const offset = (height - padding) / (height / 50);
- this.media.style.transform = `translateY(-${offset}%)`;
+ if (this.fullscreen.active) {
+ wrapper.style.paddingBottom = null;
+ } else {
+ this.media.style.transform = `translateY(-${offset}%)`;
+ }
} else if (this.isHTML5) {
wrapper.classList.toggle(this.config.classNames.videoFixedRatio, ratio !== null);
}
diff --git a/src/sass/base.scss b/src/sass/base.scss
index 8ab3e1a8..93f91bd9 100644
--- a/src/sass/base.scss
+++ b/src/sass/base.scss
@@ -12,7 +12,6 @@
font-family: $plyr-font-family;
font-variant-numeric: tabular-nums; // Force monosace-esque number widths
font-weight: $plyr-font-weight-regular;
- height: 100%;
line-height: $plyr-line-height;
max-width: 100%;
min-width: 200px;
diff --git a/src/sass/components/sliders.scss b/src/sass/components/sliders.scss
index b90e7229..69947003 100644
--- a/src/sass/components/sliders.scss
+++ b/src/sass/components/sliders.scss
@@ -3,7 +3,6 @@
// --------------------------------------------------------------
.plyr--full-ui input[type='range'] {
- // WebKit
-webkit-appearance: none; /* stylelint-disable-line */
background: transparent;
border: 0;
@@ -13,6 +12,7 @@
display: block;
height: calc((#{$plyr-range-thumb-active-shadow-width} * 2) + #{$plyr-range-thumb-height});
margin: 0;
+ min-width: 0;
padding: 0;
transition: box-shadow 0.3s ease;
width: 100%;
diff --git a/src/sass/components/times.scss b/src/sass/components/times.scss
index db41275d..c9f957bb 100644
--- a/src/sass/components/times.scss
+++ b/src/sass/components/times.scss
@@ -14,7 +14,7 @@
margin-right: $plyr-control-spacing;
}
- @media (max-width: calc(#{$plyr-bp-md} - 1)) {
+ @media (max-width: calc(#{$plyr-bp-md} - 1px)) {
display: none;
}
}