diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/config/defaults.js | 9 | ||||
-rw-r--r-- | src/js/fullscreen.js | 6 | ||||
-rw-r--r-- | src/js/listeners.js | 27 | ||||
-rw-r--r-- | src/js/media.js | 1 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 34 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 65 | ||||
-rw-r--r-- | src/js/plyr.d.ts | 109 | ||||
-rw-r--r-- | src/js/plyr.js | 6 | ||||
-rw-r--r-- | src/js/ui.js | 5 | ||||
-rw-r--r-- | src/js/utils/style.js | 6 | ||||
-rw-r--r-- | src/sass/base.scss | 1 | ||||
-rw-r--r-- | src/sass/components/sliders.scss | 2 | ||||
-rw-r--r-- | src/sass/components/times.scss | 2 |
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; } } |