diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/js/config/defaults.js | 9 | ||||
-rw-r--r-- | src/js/fullscreen.js | 4 | ||||
-rw-r--r-- | src/js/plugins/vimeo.js | 38 | ||||
-rw-r--r-- | src/js/plugins/youtube.js | 61 | ||||
-rw-r--r-- | src/js/plyr.d.ts | 109 | ||||
-rw-r--r-- | src/js/plyr.js | 6 | ||||
-rw-r--r-- | src/js/ui.js | 2 | ||||
-rw-r--r-- | src/js/utils/style.js | 6 | ||||
-rw-r--r-- | src/sass/base.scss | 1 | ||||
-rw-r--r-- | src/sass/components/poster.scss | 1 |
10 files changed, 141 insertions, 96 deletions
diff --git a/src/js/config/defaults.js b/src/js/config/defaults.js index 8f5d081b..80ab190f 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..8bc58c00 100644 --- a/src/js/fullscreen.js +++ b/src/js/fullscreen.js @@ -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/plugins/vimeo.js b/src/js/plugins/vimeo.js index 2e12d160..cd19b097 100644 --- a/src/js/plugins/vimeo.js +++ b/src/js/plugins/vimeo.js @@ -112,31 +112,35 @@ 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, id), 'json').then((response) => { - if (is.empty(response)) { - return; - } + if (!config.customControls) { + fetch(format(player.config.urls.vimeo.api, id), 'json').then((response) => { + if (is.empty(response)) { + return; + } - // Get the URL for thumbnail - const url = new URL(response[0].thumbnail_large); + // Get the URL for thumbnail + const url = new URL(response[0].thumbnail_large); - // Get original image - url.pathname = `${url.pathname.split('_')[0]}.jpg`; + // Get original image + url.pathname = `${url.pathname.split('_')[0]}.jpg`; - // Set and show poster - ui.setPoster.call(player, url.href).catch(() => {}); - }); + // Set and show poster + ui.setPoster.call(player, url.href).catch(() => {}); + }); + } // Setup instance // https://github.com/vimeo/player.js @@ -407,7 +411,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..cc604e38 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,29 +122,26 @@ 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; + 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 @@ -153,11 +151,16 @@ const youtube = { 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, @@ -302,7 +305,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 +338,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 +391,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..f882fc2b 100644 --- a/src/js/ui.js +++ b/src/js/ui.js @@ -270,7 +270,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/poster.scss b/src/sass/components/poster.scss index 2e966a32..6b2d7b6b 100644 --- a/src/sass/components/poster.scss +++ b/src/sass/components/poster.scss @@ -10,6 +10,7 @@ height: 100%; left: 0; opacity: 0; + pointer-events: none; position: absolute; top: 0; transition: opacity 0.2s ease; |