Progressively enhance <iframe> embeds
This commit is contained in:
		| @ -5,8 +5,8 @@ | ||||
| const noop = () => {}; | ||||
|  | ||||
| export default class Console { | ||||
|     constructor(player) { | ||||
|         this.enabled = window.console && player.config.debug; | ||||
|     constructor(enabled = false) { | ||||
|         this.enabled = window.console && enabled; | ||||
|  | ||||
|         if (this.enabled) { | ||||
|             this.log('Debugging enabled'); | ||||
|  | ||||
| @ -50,7 +50,7 @@ const media = { | ||||
|         } | ||||
|  | ||||
|         // Inject the player wrapper | ||||
|         if (this.isVideo || this.isYouTube || this.isVimeo) { | ||||
|         if (this.isVideo) { | ||||
|             // Create the wrapper div | ||||
|             this.elements.wrapper = utils.createElement('div', { | ||||
|                 class: this.config.classNames.video, | ||||
|  | ||||
| @ -8,19 +8,12 @@ import ui from './../ui'; | ||||
|  | ||||
| const vimeo = { | ||||
|     setup() { | ||||
|         // Remove old containers | ||||
|         const containers = utils.getElements.call(this, `[id^="${this.provider}-"]`); | ||||
|         Array.from(containers).forEach(utils.removeElement); | ||||
|  | ||||
|         // Add embed class for responsive | ||||
|         utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); | ||||
|  | ||||
|         // Set intial ratio | ||||
|         vimeo.setAspectRatio.call(this); | ||||
|  | ||||
|         // Set ID | ||||
|         this.media.setAttribute('id', utils.generateId(this.provider)); | ||||
|  | ||||
|         // Load the API if not already | ||||
|         if (!utils.is.object(window.Vimeo)) { | ||||
|             utils.loadScript(this.config.urls.vimeo.api, () => { | ||||
| @ -57,15 +50,21 @@ const vimeo = { | ||||
|             transparent: 0, | ||||
|             gesture: 'media', | ||||
|         }; | ||||
|         const params = utils.buildUrlParameters(options); | ||||
|         const id = utils.parseVimeoId(player.embedId); | ||||
|         const params = utils.buildUrlParams(options); | ||||
|         const id = utils.parseVimeoId(player.media.getAttribute('src')); | ||||
|  | ||||
|         // Build an iframe | ||||
|         const iframe = utils.createElement('iframe'); | ||||
|         const src = `https://player.vimeo.com/video/${id}?${params}`; | ||||
|         iframe.setAttribute('src', src); | ||||
|         iframe.setAttribute('allowfullscreen', ''); | ||||
|         player.media.appendChild(iframe); | ||||
|         iframe.setAttribute('allowtransparency', ''); | ||||
|         iframe.setAttribute('allow', 'autoplay'); | ||||
|  | ||||
|         // Inject the package | ||||
|         const wrapper = utils.createElement('div'); | ||||
|         wrapper.appendChild(iframe); | ||||
|         player.media = utils.replaceElement(wrapper, player.media); | ||||
|  | ||||
|         // Setup instance | ||||
|         // https://github.com/vimeo/player.js | ||||
|  | ||||
| @ -8,24 +8,15 @@ import ui from './../ui'; | ||||
|  | ||||
| const youtube = { | ||||
|     setup() { | ||||
|         const videoId = utils.parseYouTubeId(this.embedId); | ||||
|  | ||||
|         // Remove old containers | ||||
|         const containers = utils.getElements.call(this, `[id^="${this.provider}-"]`); | ||||
|         Array.from(containers).forEach(utils.removeElement); | ||||
|  | ||||
|         // Add embed class for responsive | ||||
|         utils.toggleClass(this.elements.wrapper, this.config.classNames.embed, true); | ||||
|  | ||||
|         // Set aspect ratio | ||||
|         youtube.setAspectRatio.call(this); | ||||
|  | ||||
|         // Set ID | ||||
|         this.media.setAttribute('id', utils.generateId(this.provider)); | ||||
|  | ||||
|         // Setup API | ||||
|         if (utils.is.object(window.YT)) { | ||||
|             youtube.ready.call(this, videoId); | ||||
|         if (utils.is.object(window.YT) && utils.is.function(window.YT.Player)) { | ||||
|             youtube.ready.call(this); | ||||
|         } else { | ||||
|             // Load the API | ||||
|             utils.loadScript(this.config.urls.youtube.api); | ||||
| @ -36,7 +27,7 @@ const youtube = { | ||||
|  | ||||
|             // Add to queue | ||||
|             window.onYouTubeReadyCallbacks.push(() => { | ||||
|                 youtube.ready.call(this, videoId); | ||||
|                 youtube.ready.call(this); | ||||
|             }); | ||||
|  | ||||
|             // Set callback to process queue | ||||
| @ -49,7 +40,7 @@ const youtube = { | ||||
|     }, | ||||
|  | ||||
|     // Get the media title | ||||
|     getTitle() { | ||||
|     getTitle(videoId) { | ||||
|         // Try via undocumented API method first | ||||
|         // This method disappears now and then though... | ||||
|         // https://github.com/sampotts/plyr/issues/709 | ||||
| @ -65,7 +56,6 @@ const youtube = { | ||||
|  | ||||
|         // Or via Google API | ||||
|         const key = this.config.keys.google; | ||||
|         const videoId = utils.parseYouTubeId(this.embedId); | ||||
|         if (utils.is.string(key) && !utils.is.empty(key)) { | ||||
|             const url = `https://www.googleapis.com/youtube/v3/videos?id=${videoId}&key=${key}&fields=items(snippet(title))&part=snippet`; | ||||
|  | ||||
| @ -88,12 +78,24 @@ const youtube = { | ||||
|     }, | ||||
|  | ||||
|     // API ready | ||||
|     ready(videoId) { | ||||
|     ready() { | ||||
|         const player = this; | ||||
|  | ||||
|         // Ignore already setup (race condition) | ||||
|         const currentId = player.media.getAttribute('id'); | ||||
|         if (!utils.is.empty(currentId) && currentId.startsWith('youtube-')) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         // Replace the <iframe> with a <div> due to YouTube API issues | ||||
|         const videoId = utils.parseYouTubeId(player.media.getAttribute('src')); | ||||
|         const id = utils.generateId(player.provider); | ||||
|         const container = utils.createElement('div', { id }); | ||||
|         player.media = utils.replaceElement(container, player.media); | ||||
|  | ||||
|         // Setup instance | ||||
|         // https://developers.google.com/youtube/iframe_api_reference | ||||
|         player.embed = new window.YT.Player(player.media.id, { | ||||
|         player.embed = new window.YT.Player(id, { | ||||
|             videoId, | ||||
|             playerVars: { | ||||
|                 autoplay: player.config.autoplay ? 1 : 0, // Autoplay | ||||
| @ -110,8 +112,8 @@ const youtube = { | ||||
|                 widget_referrer: window && window.location.href, | ||||
|  | ||||
|                 // Captions are flaky on YouTube | ||||
|                 cc_load_policy: this.captions.active ? 1 : 0, | ||||
|                 cc_lang_pref: this.config.captions.language, | ||||
|                 cc_load_policy: player.captions.active ? 1 : 0, | ||||
|                 cc_lang_pref: player.config.captions.language, | ||||
|             }, | ||||
|             events: { | ||||
|                 onError(event) { | ||||
| @ -179,7 +181,7 @@ const youtube = { | ||||
|                     const instance = event.target; | ||||
|  | ||||
|                     // Get the title | ||||
|                     youtube.getTitle.call(player); | ||||
|                     youtube.getTitle.call(player, videoId); | ||||
|  | ||||
|                     // Create a faux HTML5 API using the YouTube API | ||||
|                     player.media.play = () => { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| // ========================================================================== | ||||
| // Plyr | ||||
| // plyr.js v3.0.0-beta.1 | ||||
| // plyr.js v3.0.0-beta.2 | ||||
| // https://github.com/sampotts/plyr | ||||
| // License: The MIT License (MIT) | ||||
| // ========================================================================== | ||||
| @ -66,7 +66,7 @@ class Plyr { | ||||
|                 } catch (e) { | ||||
|                     return {}; | ||||
|                 } | ||||
|             })() | ||||
|             })(), | ||||
|         ); | ||||
|  | ||||
|         // Elements cache | ||||
| @ -103,7 +103,7 @@ class Plyr { | ||||
|  | ||||
|         // Debugging | ||||
|         // TODO: move to globals | ||||
|         this.debug = new Console(this); | ||||
|         this.debug = new Console(this.config.debug); | ||||
|  | ||||
|         // Log config options and support | ||||
|         this.debug.log('Config', this.config); | ||||
| @ -141,35 +141,61 @@ class Plyr { | ||||
|         // Supported: video, audio, vimeo, youtube | ||||
|         const type = this.media.tagName.toLowerCase(); | ||||
|  | ||||
|         // Embed attributes | ||||
|         const attributes = { | ||||
|             provider: 'data-plyr-provider', | ||||
|             id: 'data-plyr-embed-id', | ||||
|         }; | ||||
|         // Embed properties | ||||
|         let iframe = null; | ||||
|         let url = null; | ||||
|         let params = null; | ||||
|  | ||||
|         // Different setup based on type | ||||
|         switch (type) { | ||||
|             // TODO: Handle passing an iframe for true progressive enhancement | ||||
|             // case 'iframe': | ||||
|             case 'div': | ||||
|                 this.type = types.video; // Audio will come later for external providers | ||||
|                 this.provider = this.media.getAttribute(attributes.provider); | ||||
|                 this.embedId = this.media.getAttribute(attributes.id); | ||||
|                 // Find the frame | ||||
|                 iframe = this.media.querySelector('iframe'); | ||||
|  | ||||
|                 // <iframe> required | ||||
|                 if (!utils.is.element(iframe)) { | ||||
|                     this.debug.error('Setup failed: <iframe> is missing'); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 // Audio will come later for external providers | ||||
|                 this.type = types.video; | ||||
|  | ||||
|                 // Detect provider | ||||
|                 url = iframe.getAttribute('src'); | ||||
|                 this.provider = utils.getProviderByUrl(url); | ||||
|  | ||||
|                 // Get attributes from URL and set config | ||||
|                 params = utils.getUrlParams(url); | ||||
|                 if (!utils.is.empty(params)) { | ||||
|                     const truthy = [ | ||||
|                         '1', | ||||
|                         'true', | ||||
|                     ]; | ||||
|  | ||||
|                     if (truthy.includes(params.autoplay)) { | ||||
|                         this.config.autoplay = true; | ||||
|                     } | ||||
|                     if (truthy.includes(params.playsinline)) { | ||||
|                         this.config.inline = true; | ||||
|                     } | ||||
|                     if (truthy.includes(params.loop)) { | ||||
|                         this.config.loop.active = true; | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // Unsupported provider | ||||
|                 if (utils.is.empty(this.provider) || !Object.keys(providers).includes(this.provider)) { | ||||
|                     this.debug.error('Setup failed: Invalid provider'); | ||||
|                     return; | ||||
|                 } | ||||
|  | ||||
|                 // Try and get the embed id | ||||
|                 if (utils.is.empty(this.embedId)) { | ||||
|                     this.debug.error('Setup failed: Embed ID or URL missing'); | ||||
|                     return; | ||||
|                 } | ||||
|                 // Rework elements | ||||
|                 this.elements.container = this.media; | ||||
|                 this.media = iframe; | ||||
|  | ||||
|                 // Clean up | ||||
|                 this.media.removeAttribute(attributes.provider); | ||||
|                 this.media.removeAttribute(attributes.id); | ||||
|                 // Reset classname | ||||
|                 this.elements.container.className = ''; | ||||
|  | ||||
|                 break; | ||||
|  | ||||
| @ -178,22 +204,19 @@ class Plyr { | ||||
|                 this.type = type; | ||||
|                 this.provider = providers.html5; | ||||
|  | ||||
|                 // Get config from attributes | ||||
|                 if (this.media.hasAttribute('crossorigin')) { | ||||
|                     this.config.crossorigin = true; | ||||
|                 } | ||||
|  | ||||
|                 if (this.media.hasAttribute('autoplay')) { | ||||
|                     this.config.autoplay = true; | ||||
|                 } | ||||
|  | ||||
|                 if (this.media.hasAttribute('playsinline')) { | ||||
|                     this.config.inline = true; | ||||
|                 } | ||||
|  | ||||
|                 if (this.media.hasAttribute('muted')) { | ||||
|                     this.config.muted = true; | ||||
|                 } | ||||
|  | ||||
|                 if (this.media.hasAttribute('loop')) { | ||||
|                     this.config.loop.active = true; | ||||
|                 } | ||||
| @ -221,8 +244,10 @@ class Plyr { | ||||
|         this.media.plyr = this; | ||||
|  | ||||
|         // Wrap media | ||||
|         this.elements.container = utils.createElement('div'); | ||||
|         utils.wrap(this.media, this.elements.container); | ||||
|         if (!utils.is.element(this.elements.container)) { | ||||
|             this.elements.container = utils.createElement('div'); | ||||
|             utils.wrap(this.media, this.elements.container); | ||||
|         } | ||||
|  | ||||
|         // Allow focus to be captured | ||||
|         this.elements.container.setAttribute('tabindex', 0); | ||||
| @ -1054,7 +1079,6 @@ class Plyr { | ||||
|  | ||||
|             // GC for embed | ||||
|             this.embed = null; | ||||
|             this.embedId = null; | ||||
|  | ||||
|             // If it's a soft destroy, make minimal changes | ||||
|             if (soft) { | ||||
| @ -1082,11 +1106,7 @@ class Plyr { | ||||
|                 } | ||||
|             } else { | ||||
|                 // Replace the container with the original element provided | ||||
|                 const parent = this.elements.container.parentNode; | ||||
|  | ||||
|                 if (utils.is.element(parent)) { | ||||
|                     parent.replaceChild(this.elements.original, this.elements.container); | ||||
|                 } | ||||
|                 utils.replaceElement(this.elements.original, this.elements.container); | ||||
|  | ||||
|                 // Event | ||||
|                 utils.dispatchEvent.call(this, this.elements.original, 'destroyed', true); | ||||
| @ -1119,7 +1139,9 @@ class Plyr { | ||||
|                 window.clearInterval(this.timers.playing); | ||||
|  | ||||
|                 // Destroy YouTube API | ||||
|                 this.embed.destroy(); | ||||
|                 if (this.embed !== null) { | ||||
|                     this.embed.destroy(); | ||||
|                 } | ||||
|  | ||||
|                 // Clean up | ||||
|                 done(); | ||||
| @ -1129,7 +1151,9 @@ class Plyr { | ||||
|             case 'vimeo:video': | ||||
|                 // Destroy Vimeo API | ||||
|                 // then clean up (wait, to prevent postmessage errors) | ||||
|                 this.embed.unload().then(done); | ||||
|                 if (this.embed !== null) { | ||||
|                     this.embed.unload().then(done); | ||||
|                 } | ||||
|  | ||||
|                 // Vimeo does not always return | ||||
|                 window.setTimeout(done, 200); | ||||
|  | ||||
| @ -67,8 +67,9 @@ const source = { | ||||
|  | ||||
|                     case 'youtube:video': | ||||
|                     case 'vimeo:video': | ||||
|                         this.media = utils.createElement('div'); | ||||
|                         this.embedId = input.sources[0].src; | ||||
|                         this.media = utils.createElement('div', { | ||||
|                             src: input.sources[0].src, | ||||
|                         }); | ||||
|                         break; | ||||
|  | ||||
|                     default: | ||||
| @ -136,7 +137,7 @@ const source = { | ||||
|                     ui.build.call(this); | ||||
|                 } | ||||
|             }, | ||||
|             true | ||||
|             true, | ||||
|         ); | ||||
|     }, | ||||
| }; | ||||
|  | ||||
| @ -3,6 +3,7 @@ | ||||
| // ========================================================================== | ||||
|  | ||||
| import support from './support'; | ||||
| import { providers } from './types'; | ||||
|  | ||||
| const utils = { | ||||
|     // Check variable types | ||||
| @ -103,7 +104,7 @@ const utils = { | ||||
|                     element.callbacks.forEach(cb => cb.call(null, event)); | ||||
|                     element.callbacks = null; | ||||
|                 }, | ||||
|                 false | ||||
|                 false, | ||||
|             ); | ||||
|         } | ||||
|  | ||||
| @ -168,7 +169,7 @@ const utils = { | ||||
|                             prefix + id, | ||||
|                             JSON.stringify({ | ||||
|                                 content: text, | ||||
|                             }) | ||||
|                             }), | ||||
|                         ); | ||||
|                     } | ||||
|  | ||||
| @ -274,6 +275,17 @@ const utils = { | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Replace element | ||||
|     replaceElement(newChild, oldChild) { | ||||
|         if (!utils.is.element(oldChild) || !utils.is.element(oldChild.parentNode) || !utils.is.element(newChild)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         oldChild.parentNode.replaceChild(newChild, oldChild); | ||||
|  | ||||
|         return newChild; | ||||
|     }, | ||||
|  | ||||
|     // Set attributes | ||||
|     setAttributes(element, attributes) { | ||||
|         if (!utils.is.element(element) || utils.is.empty(attributes)) { | ||||
| @ -491,7 +503,7 @@ const utils = { | ||||
|                     event.preventDefault(); | ||||
|                 } | ||||
|             }, | ||||
|             false | ||||
|             false, | ||||
|         ); | ||||
|     }, | ||||
|  | ||||
| @ -617,14 +629,37 @@ const utils = { | ||||
|         return utils.extend(target, ...sources); | ||||
|     }, | ||||
|  | ||||
|     // Get the provider for a given URL | ||||
|     getProviderByUrl(url) { | ||||
|         // YouTube | ||||
|         if (/^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.?be)\/.+$/.test(url)) { | ||||
|             return providers.youtube; | ||||
|         } | ||||
|  | ||||
|         // Vimeo | ||||
|         if (/^https?:\/\/player.vimeo.com\/video\/\d{8,}(?=\b|\/)/.test(url)) { | ||||
|             return providers.vimeo; | ||||
|         } | ||||
|  | ||||
|         return null; | ||||
|     }, | ||||
|  | ||||
|     // Parse YouTube ID from URL | ||||
|     parseYouTubeId(url) { | ||||
|         if (utils.is.empty(url)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         const regex = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/; | ||||
|         return url.match(regex) ? RegExp.$2 : url; | ||||
|     }, | ||||
|  | ||||
|     // Parse Vimeo ID from URL | ||||
|     parseVimeoId(url) { | ||||
|         if (utils.is.empty(url)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         if (utils.is.number(Number(url))) { | ||||
|             return url; | ||||
|         } | ||||
| @ -633,8 +668,40 @@ const utils = { | ||||
|         return url.match(regex) ? RegExp.$2 : url; | ||||
|     }, | ||||
|  | ||||
|     // Convert a URL to a location object | ||||
|     parseUrl(url) { | ||||
|         const parser = document.createElement('a'); | ||||
|         parser.href = url; | ||||
|         return parser; | ||||
|     }, | ||||
|  | ||||
|     // Get URL query parameters | ||||
|     getUrlParams(input) { | ||||
|         let search = input; | ||||
|  | ||||
|         // Parse URL if needed | ||||
|         if (input.startsWith('http://') || input.startsWith('https://')) { | ||||
|             ({ search } = this.parseUrl(input)); | ||||
|         } | ||||
|  | ||||
|         if (this.is.empty(search)) { | ||||
|             return null; | ||||
|         } | ||||
|  | ||||
|         const hashes = search.slice(search.indexOf('?') + 1).split('&'); | ||||
|  | ||||
|         return hashes.reduce((params, hash) => { | ||||
|             const [ | ||||
|                 key, | ||||
|                 val, | ||||
|             ] = hash.split('='); | ||||
|  | ||||
|             return Object.assign(params, { [key]: decodeURIComponent(val) }); | ||||
|         }, {}); | ||||
|     }, | ||||
|  | ||||
|     // Convert object to URL parameters | ||||
|     buildUrlParameters(input) { | ||||
|     buildUrlParams(input) { | ||||
|         if (!utils.is.object(input)) { | ||||
|             return ''; | ||||
|         } | ||||
|  | ||||
| @ -11,6 +11,7 @@ | ||||
|  | ||||
|     height: 0; | ||||
|     padding-bottom: to-percentage($padding); | ||||
|     position: relative; | ||||
|  | ||||
|     iframe { | ||||
|         border: 0; | ||||
|  | ||||
		Reference in New Issue
	
	Block a user