Changeset 3293487
- Timestamp:
- 05/14/2025 07:45:36 PM (9 months ago)
- Location:
- enhanced-embed-block
- Files:
-
- 25 added
- 5 deleted
- 6 edited
- 1 copied
-
assets/blueprints (added)
-
assets/blueprints/blueprint.json (added)
-
assets/enhanced-embed-block-test-page.xml (added)
-
assets/icon.svg (deleted)
-
tags/1.2.0 (copied) (copied from enhanced-embed-block/trunk)
-
tags/1.2.0/.wordpress-org/blueprints (added)
-
tags/1.2.0/.wordpress-org/blueprints/blueprint.json (added)
-
tags/1.2.0/.wordpress-org/enhanced-embed-block-test-page.xml (added)
-
tags/1.2.0/.wordpress-org/icon.svg (deleted)
-
tags/1.2.0/css/lite-embed-fallback.css (added)
-
tags/1.2.0/css/lite-youtube-custom.css (deleted)
-
tags/1.2.0/enhanced-embed-block.php (modified) (4 diffs)
-
tags/1.2.0/inc (added)
-
tags/1.2.0/inc/generic.php (added)
-
tags/1.2.0/inc/vimeo.php (added)
-
tags/1.2.0/inc/youtube.php (added)
-
tags/1.2.0/readme.txt (modified) (7 diffs)
-
tags/1.2.0/vendor/lite-vimeo (added)
-
tags/1.2.0/vendor/lite-vimeo/LICENSE (added)
-
tags/1.2.0/vendor/lite-vimeo/lite-vimeo.js (added)
-
tags/1.2.0/vendor/lite-youtube/lite-youtube.js (modified) (4 diffs)
-
trunk/.wordpress-org/blueprints (added)
-
trunk/.wordpress-org/blueprints/blueprint.json (added)
-
trunk/.wordpress-org/enhanced-embed-block-test-page.xml (added)
-
trunk/.wordpress-org/icon.svg (deleted)
-
trunk/css/lite-embed-fallback.css (added)
-
trunk/css/lite-youtube-custom.css (deleted)
-
trunk/enhanced-embed-block.php (modified) (4 diffs)
-
trunk/inc (added)
-
trunk/inc/generic.php (added)
-
trunk/inc/vimeo.php (added)
-
trunk/inc/youtube.php (added)
-
trunk/readme.txt (modified) (7 diffs)
-
trunk/vendor/lite-vimeo (added)
-
trunk/vendor/lite-vimeo/LICENSE (added)
-
trunk/vendor/lite-vimeo/lite-vimeo.js (added)
-
trunk/vendor/lite-youtube/lite-youtube.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
enhanced-embed-block/tags/1.2.0/enhanced-embed-block.php
r3116570 r3293487 1 1 <?php 2 2 /** 3 * Plugin Name: Enhanced Embed Block for YouTube 3 * Plugin Name: Enhanced Embed Block for YouTube & Vimeo 4 4 * Plugin URI: https://mrwweb.com/wordpress-plugins/enhanced-embed-block/ 5 * Description: Enhance the default YouTube Embed block toload faster.5 * Description: Make the default YouTube and Vimeo Embed blocks load faster. 6 6 * Author: Mark Root-Wiley, MRW Web Design 7 7 * Author URI: https://MRWweb.com 8 8 * Text Domain: enhanced-embed-block 9 * Version: 1. 1.09 * Version: 1.2.0 10 10 * Requires at least: 6.5 11 11 * Requires PHP: 7.4 12 * GitHub Plugin URI: mrwweb/enhanced-embed-block 13 * Primary Branch: main 12 14 * License: GPLv3 or later 13 15 * License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 18 20 namespace EnhancedEmbedBlock; 19 21 20 use WP_HTML_TAG_Processor; 21 22 define( 'EEB_VERSION', '1.1.0' ); 22 define( 'EEB_VERSION', '1.2.0' ); 23 23 24 24 add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_lite_youtube_component' ); … … 33 33 plugins_url( 'vendor/lite-youtube/lite-youtube.js', __FILE__ ), 34 34 array(), 35 '1. 5.0',36 array( ' in_footer' => true )35 '1.8.1', 36 array( 'async' => true ) 37 37 ); 38 }39 38 40 add_action( 'after_setup_theme', __NAMESPACE__ . '\enqueue_embed_block_style' ); 41 function enqueue_embed_block_style() { 42 wp_enqueue_block_style( 43 'core/embed', 44 array( 45 'handle' => 'lite-youtube-custom', 46 'src' => plugins_url( 'css/lite-youtube-custom.css', __FILE__ ), 47 'path' => plugin_dir_path( __FILE__ ) . '/css/lite-youtube-custom.css', 48 'ver' => EEB_VERSION, 49 ) 39 wp_register_script_module( 40 'lite-vimeo', 41 plugins_url( 'vendor/lite-vimeo/lite-vimeo.js', __FILE__ ), 42 array(), 43 '1.0.2', 44 array( 'async' => true ) 45 ); 46 47 wp_register_style( 48 'lite-embed-fallback', 49 plugins_url( 'css/lite-embed-fallback.css', __FILE__ ), 50 array(), 51 EEB_VERSION, 50 52 ); 51 53 } 52 54 53 55 56 54 57 /* Pre-2020 Blocks */ 55 add_filter( 'render_block_core-embed/youtube', __NAMESPACE__ . '\replace_ youtube_embed_with_web_component', 10, 2 );58 add_filter( 'render_block_core-embed/youtube', __NAMESPACE__ . '\replace_embeds_with_web_components', 10, 2 ); 56 59 /* 2020-onward Block */ 57 add_filter( 'render_block_core/embed', __NAMESPACE__ . '\replace_ youtube_embed_with_web_component', 10, 2 );60 add_filter( 'render_block_core/embed', __NAMESPACE__ . '\replace_embeds_with_web_components', 10, 2 ); 58 61 /** 59 62 * Filter the Embed output and replace it with the web component … … 63 66 * @return string HTML for embed block 64 67 */ 65 function replace_youtube_embed_with_web_component( $content, $block ) { 66 $isValidYouTube = 'youtube' === $block['attrs']['providerNameSlug'] && isset( $block['attrs']['url'] ); 67 if( ! $isValidYouTube || is_feed() ) { 68 function replace_embeds_with_web_components( $content, $block ) { 69 70 if ( 71 ! isset( $block['attrs']['url'] ) || 72 is_feed() || 73 ! in_array( 74 $block['attrs']['providerNameSlug'], 75 array( 'youtube', 'vimeo' ), 76 true 77 ) 78 ) { 68 79 return $content; 69 80 } 70 81 71 $video_id = extract_youtube_id_from_uri( $block['attrs']['url'] ); 72 if ( ! $video_id ) { 73 return $content; 82 wp_enqueue_style( 'lite-embed-fallback' ); 83 84 switch ( $block['attrs']['providerNameSlug'] ) { 85 case 'youtube': 86 $content = render_youtube_embed( $content, $block ); 87 break; 88 89 case 'vimeo': 90 $content = render_vimeo_embed( $content, $block ); 91 break; 74 92 } 75 76 wp_enqueue_script_module( 'lite-youtube' );77 78 $video_title = extract_title_from_embed_code( $content );79 $start_time = extract_start_time_from_uri( $block['attrs']['url'] );80 $embed_caption = extract_figcaption_from_embed_code( $content );81 82 /* translators: %s: title from YouTube video */83 $play_button = sprintf( __( 'Play: %s', 'enhanced-embed-block' ), $video_title );84 85 /**86 * Filter the poster quality for the YouTube preview thumbnail87 *88 * @since 1.1.089 * @param string $quality One of mqdefault, hqdefault, sddefault, or maxresdefault (default)90 */91 $poster_quality = apply_filters( 'eeb_posterquality', 'maxresdefault' );92 93 /**94 * Filter to determine whether to load embed from nocookie YouTube domain95 *96 * @since 1.1.097 * @param bool $use_nocookie Whether to use the nocookie domain (default: true)98 */99 $nocookie = apply_filters( 'eeb_nocookie', true );100 101 /* Craft the new output: the web component with HTML fallback link */102 $content = sprintf(103 '<figure class="wp-block-embed-youtube wp-block-embed is-type-video is-provider-youtube">104 <div class="wp-block-embed__wrapper">105 <lite-youtube videoid="%1$s" videoplay="%2$s" videoStartAt="%3$d" posterquality="%4$s" posterloading="lazy"%5$s>106 <a href="%6$s" class="lite-youtube-fallback" target="_blank" rel="noreferrer noopenner">Watch "%7$s" on YouTube</a>107 </lite-youtube>108 </div>109 %8$s110 </figure>',111 esc_attr( $video_id ),112 esc_attr( $play_button ),113 $start_time ? intval( $start_time ) : 0,114 in_array( $poster_quality, array( 'mqdefault', 'hqdefault', 'sddefault', 'maxresdefault' ), true ) ? $poster_quality : 'maxresdefault',115 $nocookie ? ' nocookie' : '',116 esc_url( $block['attrs']['url'] ),117 esc_html( $video_title ),118 $embed_caption119 );120 93 121 94 return $content; 122 95 } 123 96 124 /** 125 * Extract the value of the title attribute from HTML that contains an iframe of an existing YouTube embed code 126 * 127 * @param string $html A block of HTML containing a YouTube iframe. 128 * @return string the title attribute valube 129 */ 130 function extract_title_from_embed_code( $html ) { 131 $processor = new WP_HTML_TAG_Processor( $html ); 132 $processor->next_tag( 'iframe' ); 133 $title = $processor->get_attribute( 'title' ); 134 135 return $title; 136 } 137 138 /** 139 * Extract the figcaption from the embed code 140 * 141 * @param string $html A block of HTML containing a YouTube iframe. 142 * @return string The figcaption OR an empty string 143 * 144 * @todo Replace this with HTML Tag Processor, if possible 145 */ 146 function extract_figcaption_from_embed_code( $html ) { 147 preg_match( '/<figcaption(.*?)<\/figcaption>/s', $html, $match ); 148 return isset( $match[0] ) ? $match[0] : false; 149 } 150 151 /** 152 * Get the YouTube video ID from a YouTube video URL, either the default youtube.com one or the shortened youtu.be one 153 * 154 * @param string $uri A YouTube video URL. 155 * @return string video ID for a YouTube video 156 */ 157 function extract_youtube_id_from_uri( $uri ) { 158 $host = wp_parse_url( $uri, PHP_URL_HOST ); 159 160 /* Handle Shortlinks */ 161 if ( 'youtu.be' === $host ) { 162 return ltrim( wp_parse_url( $uri, PHP_URL_PATH ), '/' ); 163 } 164 165 $params = wp_parse_url( $uri, PHP_URL_QUERY ); 166 parse_str( $params, $query ); 167 return $query['v'] ?? false; 168 } 169 170 /** 171 * Extract the start time parameter "t" from YouTube video URL 172 * 173 * @param string $uri URL of YouTube video that may or may not contain a start time parameter. 174 * @return string|bool The value of the t parameter or false if it isn't present 175 */ 176 function extract_start_time_from_uri( $uri ) { 177 $params = wp_parse_url( $uri, PHP_URL_QUERY ); 178 parse_str( $params, $query ); 179 return $query['t'] ?? false; 180 } 97 require_once plugin_dir_path( __FILE__ ) . 'inc/generic.php'; 98 require_once plugin_dir_path( __FILE__ ) . 'inc/vimeo.php'; 99 require_once plugin_dir_path( __FILE__ ) . 'inc/youtube.php'; -
enhanced-embed-block/tags/1.2.0/readme.txt
r3116570 r3293487 1 === Enhanced Embed Block for YouTube ===1 === Enhanced Embed Block for YouTube & Vimeo === 2 2 Contributors: mrwweb, cbirdsong 3 3 Donate link: https://paypal.me/rootwiley 4 Tags: YouTube, embed, video, block, performance4 Tags: YouTube, Vimeo, embed, video, block 5 5 Requires at least: 6.5 6 Tested up to: 6. 66 Tested up to: 6.8 7 7 Requires PHP: 7.4 8 Stable tag: 1. 1.08 Stable tag: 1.2.0 9 9 License: GPLv3 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-3.0.html 11 11 12 Enhance the default YouTube Embed Blockto load faster.12 Enhance the default YouTube and Vimeo Embed blocks to load faster. 13 13 14 14 == Description == … … 16 16 If you care about performance, privacy, and user experience, this block is for you. 17 17 18 This plugin enhances the default YouTube block—including any existing blocks—and changes their behavior to only load the video thumbnail until a visitor chooses to play the video.18 This plugin enhances the default YouTube and Vimeo blocks—including any existing blocks—and changes their behavior to only load the video thumbnail until a visitor chooses to play the video. 19 19 20 20 = Features = 21 21 22 22 * Load YouTube videos faster (uses the `lite-youtube` custom-element) 23 * Loads videos from nocookie.youtube.com for enhanced privacy 24 * Works without JavaScript (shows link to YouTube video instead) 25 * No plugin lock-in! Automatically improves all YouTube embeds. Turn it off and the behavior goes back to the WordPress default. 23 * Load Vimeo videos faster (uses the `lite-viemo` custom-element) 24 * Loads YouTube videos from nocookie.youtube.com for enhanced privacy 25 * Works without JavaScript (shows link to video instead in a player-like design) 26 * No plugin lock-in! Automatically improves the core Embed block. Turn the plugin off and the behavior goes back to the WordPress default. 26 27 27 28 = Want more features? = … … 38 39 * Full support for all YouTube query parameters (https://developers.google.com/youtube/player_parameters) 39 40 * Classic Editor / [embed] shortcode support 40 * Support similar features for Vimeo and other embed sources where possible41 41 42 42 If enough people express interest, I'll build it! [Let me know if you're interested!](https://mrwweb.com/wordpress-plugins/enhanced-embed-block/#pro) … … 49 49 50 50 1. From your WordPress site’s dashboard, go to Plugins > Add New. 51 2. Search for “Enhanced Embed Block for YouTube ”51 2. Search for “Enhanced Embed Block for YouTube and Vimeo” 52 52 3. Click “Install” 53 53 4. Click “Activate” … … 58 58 = Does this create a new block? = 59 59 60 No. It enhances the default WordPress embed block for YouTubevideos.60 No. It enhances the default WordPress Embed block for YouTube and Vimeo videos. 61 61 62 = Does it automatically enhance all my YouTube embeds? =62 = Does it automatically enhance all my YouTube and Vimeo embeds? = 63 63 64 It works for any embeds using the YouTube block. Embeds using the [embed] shortcode or literal YouTube embed code in HTML are not enhanced. Using the core WordPress YouTubeEmbed block is highly recommended!64 It works for any embeds using the YouTube or Vimeo variations of the Embed block. Embeds using the [embed] shortcode or literal YouTube embed code in HTML are not enhanced. Using the core WordPress Embed block is highly recommended! 65 65 66 = Why do esn't Google load allvideos this way by default? =66 = Why don't Google and Vimeo load all their videos this way by default? = 67 67 68 68 Great question! It sure seems like they should. If I had to guess, they are prioritizing usage tracking over fast load times and privacy. … … 80 80 This plugin uses the [`lite-youtube` custom-element](https://github.com/justinribeiro/lite-youtube) under the MIT license. Thank you to Paul Irish and Justin Ribiero for their work on that project. 81 81 82 This plugin uses the [`lite-vimeo` custom-element](https://github.com/cshawaus/lite-vimeo) under the MIT license. Thank you to Chris Shaw for their work on that project. 83 82 84 == Changelog == 83 85 84 = 1.1.0 = 86 = 1.2.0 (14 May 2025) = 87 88 - Add support for Vimeo! 89 - Upgrade `lite-youtube` to 1.8.1 (includes new native support for fallback thumbnail formats and sizes) 90 - Further performance improvements to load script asynchronously and only load styles when needed 91 - Fix undefined $params fatal error when trying to extract time code from YouTube URLs 92 - Code quality improvements 93 94 = 1.1.0 (11 July 2024) = 95 85 96 - Fix missing file on WordPress.org version of plugin due to misconfigured Github deployment 86 97 - MAJOR CHANGE: The default poster image is now the highest quality possible. There is a new `eeb_posterquality` filter to change that, if desired. (#5) … … 91 102 - Props to @cbirdsong for numerous issues on Github that led to most of these changes 92 103 93 = 1.0.0 = 104 = 1.0.0 (22 April 2024) = 105 94 106 - Initial release to the WordPress repository! 95 107 96 108 == Upgrade Notice == 97 109 98 = 1. 1.0 =99 Fix plugin in WordPress repository. Use higher quality poster image with new fallback detection. Don't apply embed changes to feeds. Developer improvements.110 = 1.2.0 = 111 Add Vimeo support! Upgrade lite-youtube custom element. Even faster loading times. -
enhanced-embed-block/tags/1.2.0/vendor/lite-youtube/lite-youtube.js
r3116570 r3293487 1 1 export class LiteYTEmbed extends HTMLElement { 2 constructor() { 3 super(); 4 this.isIframeLoaded = false; 5 this.setupDom(); 6 } 7 static get observedAttributes() { 8 return ["videoid", "playlistid"]; 9 } 10 connectedCallback() { 11 this.addEventListener("pointerover", LiteYTEmbed.warmConnections, { 12 once: true, 13 }); 14 this.addEventListener("click", () => this.addIframe()); 15 } 16 get videoId() { 17 return encodeURIComponent(this.getAttribute("videoid") || ""); 18 } 19 set videoId(id) { 20 this.setAttribute("videoid", id); 21 } 22 get playlistId() { 23 return encodeURIComponent(this.getAttribute("playlistid") || ""); 24 } 25 set playlistId(id) { 26 this.setAttribute("playlistid", id); 27 } 28 get videoTitle() { 29 return this.getAttribute("videotitle") || "Video"; 30 } 31 set videoTitle(title) { 32 this.setAttribute("videotitle", title); 33 } 34 get videoPlay() { 35 return this.getAttribute("videoPlay") || "Play"; 36 } 37 set videoPlay(name) { 38 this.setAttribute("videoPlay", name); 39 } 40 get videoStartAt() { 41 return this.getAttribute("videoStartAt") || "0"; 42 } 43 get autoLoad() { 44 return this.hasAttribute("autoload"); 45 } 46 get noCookie() { 47 return this.hasAttribute("nocookie"); 48 } 49 get posterQuality() { 50 return this.getAttribute("posterquality") || "hqdefault"; 51 } 52 get posterLoading() { 53 return this.getAttribute("posterloading") || "lazy"; 54 } 55 get params() { 56 return `start=${this.videoStartAt}&${this.getAttribute("params")}`; 57 } 58 set params(opts) { 59 this.setAttribute("params", opts); 60 } 61 setupDom() { 62 const shadowDom = this.attachShadow({ mode: "open" }); 63 let nonce = ""; 64 if (window.liteYouTubeNonce) { 65 nonce = `nonce="${window.liteYouTubeNonce}"`; 66 } 67 shadowDom.innerHTML = ` 2 constructor() { 3 super(); 4 this.isIframeLoaded = false; 5 this.setupDom(); 6 } 7 static get observedAttributes() { 8 return ['videoid', 'playlistid', 'videoplay', 'videotitle']; 9 } 10 connectedCallback() { 11 this.addEventListener('pointerover', () => LiteYTEmbed.warmConnections(this), { 12 once: true, 13 }); 14 this.addEventListener('click', () => this.addIframe()); 15 } 16 get videoId() { 17 return encodeURIComponent(this.getAttribute('videoid') || ''); 18 } 19 set videoId(id) { 20 this.setAttribute('videoid', id); 21 } 22 get playlistId() { 23 return encodeURIComponent(this.getAttribute('playlistid') || ''); 24 } 25 set playlistId(id) { 26 this.setAttribute('playlistid', id); 27 } 28 get videoTitle() { 29 return this.getAttribute('videotitle') || 'Video'; 30 } 31 set videoTitle(title) { 32 this.setAttribute('videotitle', title); 33 } 34 get videoPlay() { 35 return this.getAttribute('videoplay') || 'Play'; 36 } 37 set videoPlay(name) { 38 this.setAttribute('videoplay', name); 39 } 40 get videoStartAt() { 41 return this.getAttribute('videoStartAt') || '0'; 42 } 43 get autoLoad() { 44 return this.hasAttribute('autoload'); 45 } 46 get autoPause() { 47 return this.hasAttribute('autopause'); 48 } 49 get noCookie() { 50 return this.hasAttribute('nocookie'); 51 } 52 get posterQuality() { 53 return this.getAttribute('posterquality') || 'hqdefault'; 54 } 55 get posterLoading() { 56 return (this.getAttribute('posterloading') || 57 'lazy'); 58 } 59 get params() { 60 return `start=${this.videoStartAt}&${this.getAttribute('params')}`; 61 } 62 set params(opts) { 63 this.setAttribute('params', opts); 64 } 65 set posterQuality(opts) { 66 this.setAttribute('posterquality', opts); 67 } 68 get disableNoscript() { 69 return this.hasAttribute('disablenoscript'); 70 } 71 setupDom() { 72 const shadowDom = this.attachShadow({ mode: 'open' }); 73 let nonce = ''; 74 if (window.liteYouTubeNonce) { 75 nonce = `nonce="${window.liteYouTubeNonce}"`; 76 } 77 shadowDom.innerHTML = ` 68 78 <style ${nonce}> 69 79 :host { 80 --aspect-ratio: var(--lite-youtube-aspect-ratio, 16 / 9); 81 --aspect-ratio-short: var(--lite-youtube-aspect-ratio-short, 9 / 16); 82 --frame-shadow-visible: var(--lite-youtube-frame-shadow-visible, yes); 70 83 contain: content; 71 84 display: block; 72 85 position: relative; 73 86 width: 100%; 74 padding-bottom: calc(100% / (16 / 9));87 aspect-ratio: var(--aspect-ratio); 75 88 } 76 89 77 90 @media (max-width: 40em) { 78 91 :host([short]) { 79 padding-bottom: calc(100% / (9 / 16));92 aspect-ratio: var(--aspect-ratio-short); 80 93 } 81 94 } … … 86 99 height: 100%; 87 100 left: 0; 101 top: 0; 88 102 } 89 103 … … 92 106 } 93 107 94 #fallbackPlaceholder {108 #fallbackPlaceholder, slot[name=image]::slotted(*) { 95 109 object-fit: cover; 96 }97 98 #frame::before {99 content: '';100 display: block;101 position: absolute;102 top: 0;103 background-image: linear-gradient(180deg, #111 -20%, transparent 90%);104 height: 60px;105 110 width: 100%; 106 z-index: 1; 111 } 112 113 @container style(--frame-shadow-visible: yes) { 114 #frame::before { 115 content: ''; 116 display: block; 117 position: absolute; 118 top: 0; 119 background-image: linear-gradient(180deg, #111 -20%, transparent 90%); 120 height: 60px; 121 width: 100%; 122 z-index: 1; 123 } 107 124 } 108 125 … … 145 162 <div id="frame"> 146 163 <picture> 147 <source id="webpPlaceholder" type="image/webp"> 148 <source id="jpegPlaceholder" type="image/jpeg"> 149 <img id="fallbackPlaceholder" referrerpolicy="origin" loading="lazy"> 164 <slot name="image"> 165 <source id="webpPlaceholder" type="image/webp"> 166 <source id="jpegPlaceholder" type="image/jpeg"> 167 <img id="fallbackPlaceholder" referrerpolicy="origin" loading="lazy"> 168 </slot> 150 169 </picture> 151 <button id="playButton" ></button>170 <button id="playButton" part="playButton"></button> 152 171 </div> 153 172 `; 154 this.domRefFrame = shadowDom.querySelector("#frame"); 155 this.domRefImg = { 156 fallback: shadowDom.querySelector("#fallbackPlaceholder"), 157 webp: shadowDom.querySelector("#webpPlaceholder"), 158 jpeg: shadowDom.querySelector("#jpegPlaceholder"), 159 }; 160 this.domRefPlayButton = shadowDom.querySelector("#playButton"); 161 } 162 setupComponent() { 163 this.initImagePlaceholder(); 164 this.domRefPlayButton.setAttribute( 165 "aria-label", 166 `${this.videoPlay}: ${this.videoTitle}` 167 ); 168 this.setAttribute("title", `${this.videoPlay}: ${this.videoTitle}`); 169 if (this.autoLoad || this.isYouTubeShort()) { 170 this.initIntersectionObserver(); 171 } 172 } 173 attributeChangedCallback(name, oldVal, newVal) { 174 switch (name) { 175 case "videoid": 176 case "playlistid": 177 case "videoTitle": 178 case "videoPlay": { 179 if (oldVal !== newVal) { 180 this.setupComponent(); 181 if (this.domRefFrame.classList.contains("activated")) { 182 this.domRefFrame.classList.remove("activated"); 183 this.shadowRoot.querySelector("iframe").remove(); 184 this.isIframeLoaded = false; 185 } 186 } 187 break; 188 } 189 default: 190 break; 191 } 192 } 193 addIframe(isIntersectionObserver = false) { 194 if (!this.isIframeLoaded) { 195 let autoplay = isIntersectionObserver ? 0 : 1; 196 const wantsNoCookie = this.noCookie ? "-nocookie" : ""; 197 let embedTarget; 198 if (this.playlistId) { 199 embedTarget = `?listType=playlist&list=${this.playlistId}&`; 200 } else { 201 embedTarget = `${this.videoId}?`; 202 } 203 if (this.isYouTubeShort()) { 204 this.params = `loop=1&mute=1&modestbranding=1&playsinline=1&rel=0&enablejsapi=1&playlist=${this.videoId}`; 205 autoplay = 1; 206 } 207 const iframeHTML = ` 208 <iframe frameborder="0" title="${this.videoTitle}" 173 this.domRefFrame = shadowDom.querySelector('#frame'); 174 this.domRefImg = { 175 fallback: shadowDom.querySelector('#fallbackPlaceholder'), 176 webp: shadowDom.querySelector('#webpPlaceholder'), 177 jpeg: shadowDom.querySelector('#jpegPlaceholder'), 178 }; 179 this.domRefPlayButton = shadowDom.querySelector('#playButton'); 180 } 181 setupComponent() { 182 const hasImgSlot = this.shadowRoot.querySelector('slot[name=image]'); 183 if (hasImgSlot.assignedNodes().length === 0) { 184 this.initImagePlaceholder(); 185 } 186 this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); 187 this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`); 188 if (this.autoLoad || this.isYouTubeShort() || this.autoPause) { 189 this.initIntersectionObserver(); 190 } 191 if (!this.disableNoscript) { 192 this.injectSearchNoScript(); 193 } 194 } 195 attributeChangedCallback(name, oldVal, newVal) { 196 if (oldVal !== newVal) { 197 this.setupComponent(); 198 if (this.domRefFrame.classList.contains('activated')) { 199 this.domRefFrame.classList.remove('activated'); 200 this.shadowRoot.querySelector('iframe').remove(); 201 this.isIframeLoaded = false; 202 } 203 } 204 } 205 injectSearchNoScript() { 206 const eleNoScript = document.createElement('noscript'); 207 this.prepend(eleNoScript); 208 eleNoScript.innerHTML = this.generateIframe(); 209 } 210 generateIframe(isIntersectionObserver = false) { 211 let autoplay = isIntersectionObserver ? 0 : 1; 212 const wantsNoCookie = this.noCookie ? '-nocookie' : ''; 213 let embedTarget; 214 if (this.playlistId) { 215 embedTarget = `?listType=playlist&list=${this.playlistId}&`; 216 } 217 else { 218 embedTarget = `${this.videoId}?`; 219 } 220 if (this.autoPause) { 221 this.params = `enablejsapi=1`; 222 } 223 if (this.isYouTubeShort()) { 224 this.params = `loop=1&mute=1&modestbranding=1&playsinline=1&rel=0&enablejsapi=1&playlist=${this.videoId}`; 225 autoplay = 1; 226 } 227 return ` 228 <iframe credentialless frameborder="0" title="${this.videoTitle}" 209 229 allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen 210 230 src="https://www.youtube${wantsNoCookie}.com/embed/${embedTarget}autoplay=${autoplay}&${this.params}" 211 231 ></iframe>`; 212 this.domRefFrame.insertAdjacentHTML("beforeend", iframeHTML); 213 this.domRefFrame.classList.add("activated"); 214 this.isIframeLoaded = true; 215 this.attemptShortAutoPlay(); 216 this.dispatchEvent( 217 new CustomEvent("liteYoutubeIframeLoaded", { 218 detail: { 219 videoId: this.videoId, 220 }, 221 bubbles: true, 222 cancelable: true, 223 }) 224 ); 225 } 226 } 227 initImagePlaceholder() { 228 const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`; 229 const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/${this.posterQuality}.jpg`; 230 // PATCH: This changes the fallback img src to be the hqdefault quality no matter what since some old videos don't have higher resolution 231 const posterUrlFallback = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`; 232 this.domRefImg.fallback.loading = this.posterLoading; 233 this.domRefImg.webp.srcset = posterUrlWebp; 234 this.domRefImg.jpeg.srcset = posterUrlJpeg; 235 // PATCH: Change fallback src. See comment on line 230 for reasoning 236 this.domRefImg.fallback.src = posterUrlFallback; 237 this.domRefImg.fallback.setAttribute( 238 "aria-label", 239 `${this.videoPlay}: ${this.videoTitle}` 240 ); 241 this.domRefImg?.fallback?.setAttribute( 242 "alt", 243 `${this.videoPlay}: ${this.videoTitle}` 244 ); 245 // PATCH: Recursively load picture sources in order, deleting any where the image's natural width indicates that it is the YouTube image fallback placeholder rather than a real video poster. 246 this.domRefImg.fallback.onload = (e) => { 247 if (e.target.naturalWidth === 120) { 248 this.domRefImg.fallback.parentElement.firstElementChild.remove(); 249 } 250 }; 251 } 252 initIntersectionObserver() { 253 const options = { 254 root: null, 255 rootMargin: "0px", 256 threshold: 0, 257 }; 258 const observer = new IntersectionObserver((entries, observer) => { 259 entries.forEach((entry) => { 260 if (entry.isIntersecting && !this.isIframeLoaded) { 261 LiteYTEmbed.warmConnections(); 262 this.addIframe(true); 263 observer.unobserve(this); 264 } 265 }); 266 }, options); 267 observer.observe(this); 268 } 269 attemptShortAutoPlay() { 270 if (this.isYouTubeShort()) { 271 setTimeout(() => { 272 this.shadowRoot 273 .querySelector("iframe") 274 ?.contentWindow?.postMessage( 275 '{"event":"command","func":"' + 276 "playVideo" + 277 '","args":""}', 278 "*" 279 ); 280 }, 2000); 281 } 282 } 283 isYouTubeShort() { 284 return ( 285 this.getAttribute("short") === "" && 286 window.matchMedia("(max-width: 40em)").matches 287 ); 288 } 289 static addPrefetch(kind, url) { 290 const linkElem = document.createElement("link"); 291 linkElem.rel = kind; 292 linkElem.href = url; 293 linkElem.crossOrigin = "true"; 294 document.head.append(linkElem); 295 } 296 static warmConnections() { 297 if (LiteYTEmbed.isPreconnected || window.liteYouTubeIsPreconnected) 298 return; 299 LiteYTEmbed.addPrefetch("preconnect", "https://i.ytimg.com/"); 300 LiteYTEmbed.addPrefetch("preconnect", "https://s.ytimg.com"); 301 LiteYTEmbed.addPrefetch("preconnect", "https://www.youtube.com"); 302 LiteYTEmbed.addPrefetch("preconnect", "https://www.google.com"); 303 LiteYTEmbed.addPrefetch( 304 "preconnect", 305 "https://googleads.g.doubleclick.net" 306 ); 307 LiteYTEmbed.addPrefetch("preconnect", "https://static.doubleclick.net"); 308 LiteYTEmbed.isPreconnected = true; 309 window.liteYouTubeIsPreconnected = true; 310 } 232 } 233 addIframe(isIntersectionObserver = false) { 234 if (!this.isIframeLoaded) { 235 const iframeHTML = this.generateIframe(isIntersectionObserver); 236 this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML); 237 this.domRefFrame.classList.add('activated'); 238 this.isIframeLoaded = true; 239 this.attemptShortAutoPlay(); 240 this.dispatchEvent(new CustomEvent('liteYoutubeIframeLoaded', { 241 detail: { 242 videoId: this.videoId, 243 }, 244 bubbles: true, 245 cancelable: true, 246 })); 247 } 248 } 249 initImagePlaceholder() { 250 this.testPosterImage(); 251 this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); 252 this.domRefImg?.fallback?.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`); 253 } 254 async testPosterImage() { 255 setTimeout(() => { 256 const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`; 257 const img = new Image(); 258 img.fetchPriority = 'low'; 259 img.referrerPolicy = 'origin'; 260 img.src = webpUrl; 261 img.onload = async (e) => { 262 const target = e.target; 263 const noPoster = target?.naturalHeight == 90 && target?.naturalWidth == 120; 264 if (noPoster) { 265 this.posterQuality = 'hqdefault'; 266 } 267 const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`; 268 this.domRefImg.webp.srcset = posterUrlWebp; 269 const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/${this.posterQuality}.jpg`; 270 this.domRefImg.fallback.loading = this.posterLoading; 271 this.domRefImg.jpeg.srcset = posterUrlJpeg; 272 this.domRefImg.fallback.src = posterUrlJpeg; 273 this.domRefImg.fallback.loading = this.posterLoading; 274 }; 275 }, 100); 276 } 277 initIntersectionObserver() { 278 const options = { 279 root: null, 280 rootMargin: '0px', 281 threshold: 0, 282 }; 283 const observer = new IntersectionObserver((entries, observer) => { 284 entries.forEach(entry => { 285 if (entry.isIntersecting && !this.isIframeLoaded) { 286 LiteYTEmbed.warmConnections(this); 287 this.addIframe(true); 288 observer.unobserve(this); 289 } 290 }); 291 }, options); 292 observer.observe(this); 293 if (this.autoPause) { 294 const windowPause = new IntersectionObserver((e, o) => { 295 e.forEach(entry => { 296 if (entry.intersectionRatio !== 1) { 297 this.shadowRoot 298 .querySelector('iframe') 299 ?.contentWindow?.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); 300 } 301 }); 302 }, { threshold: 1 }); 303 windowPause.observe(this); 304 } 305 } 306 attemptShortAutoPlay() { 307 if (this.isYouTubeShort()) { 308 setTimeout(() => { 309 this.shadowRoot 310 .querySelector('iframe') 311 ?.contentWindow?.postMessage('{"event":"command","func":"' + 'playVideo' + '","args":""}', '*'); 312 }, 2000); 313 } 314 } 315 isYouTubeShort() { 316 return (this.getAttribute('short') === '' && 317 window.matchMedia('(max-width: 40em)').matches); 318 } 319 static addPrefetch(kind, url) { 320 const linkElem = document.createElement('link'); 321 linkElem.rel = kind; 322 linkElem.href = url; 323 linkElem.crossOrigin = 'true'; 324 document.head.append(linkElem); 325 } 326 static warmConnections(context) { 327 if (LiteYTEmbed.isPreconnected || window.liteYouTubeIsPreconnected) 328 return; 329 LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/'); 330 LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com'); 331 if (!context.noCookie) { 332 LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com'); 333 LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com'); 334 LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net'); 335 LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net'); 336 } 337 else { 338 LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com'); 339 } 340 LiteYTEmbed.isPreconnected = true; 341 window.liteYouTubeIsPreconnected = true; 342 } 311 343 } 312 344 LiteYTEmbed.isPreconnected = false; 313 customElements.define( "lite-youtube", LiteYTEmbed);345 customElements.define('lite-youtube', LiteYTEmbed); 314 346 //# sourceMappingURL=lite-youtube.js.map -
enhanced-embed-block/trunk/enhanced-embed-block.php
r3116570 r3293487 1 1 <?php 2 2 /** 3 * Plugin Name: Enhanced Embed Block for YouTube 3 * Plugin Name: Enhanced Embed Block for YouTube & Vimeo 4 4 * Plugin URI: https://mrwweb.com/wordpress-plugins/enhanced-embed-block/ 5 * Description: Enhance the default YouTube Embed block toload faster.5 * Description: Make the default YouTube and Vimeo Embed blocks load faster. 6 6 * Author: Mark Root-Wiley, MRW Web Design 7 7 * Author URI: https://MRWweb.com 8 8 * Text Domain: enhanced-embed-block 9 * Version: 1. 1.09 * Version: 1.2.0 10 10 * Requires at least: 6.5 11 11 * Requires PHP: 7.4 12 * GitHub Plugin URI: mrwweb/enhanced-embed-block 13 * Primary Branch: main 12 14 * License: GPLv3 or later 13 15 * License URI: https://www.gnu.org/licenses/gpl-3.0.html … … 18 20 namespace EnhancedEmbedBlock; 19 21 20 use WP_HTML_TAG_Processor; 21 22 define( 'EEB_VERSION', '1.1.0' ); 22 define( 'EEB_VERSION', '1.2.0' ); 23 23 24 24 add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_lite_youtube_component' ); … … 33 33 plugins_url( 'vendor/lite-youtube/lite-youtube.js', __FILE__ ), 34 34 array(), 35 '1. 5.0',36 array( ' in_footer' => true )35 '1.8.1', 36 array( 'async' => true ) 37 37 ); 38 }39 38 40 add_action( 'after_setup_theme', __NAMESPACE__ . '\enqueue_embed_block_style' ); 41 function enqueue_embed_block_style() { 42 wp_enqueue_block_style( 43 'core/embed', 44 array( 45 'handle' => 'lite-youtube-custom', 46 'src' => plugins_url( 'css/lite-youtube-custom.css', __FILE__ ), 47 'path' => plugin_dir_path( __FILE__ ) . '/css/lite-youtube-custom.css', 48 'ver' => EEB_VERSION, 49 ) 39 wp_register_script_module( 40 'lite-vimeo', 41 plugins_url( 'vendor/lite-vimeo/lite-vimeo.js', __FILE__ ), 42 array(), 43 '1.0.2', 44 array( 'async' => true ) 45 ); 46 47 wp_register_style( 48 'lite-embed-fallback', 49 plugins_url( 'css/lite-embed-fallback.css', __FILE__ ), 50 array(), 51 EEB_VERSION, 50 52 ); 51 53 } 52 54 53 55 56 54 57 /* Pre-2020 Blocks */ 55 add_filter( 'render_block_core-embed/youtube', __NAMESPACE__ . '\replace_ youtube_embed_with_web_component', 10, 2 );58 add_filter( 'render_block_core-embed/youtube', __NAMESPACE__ . '\replace_embeds_with_web_components', 10, 2 ); 56 59 /* 2020-onward Block */ 57 add_filter( 'render_block_core/embed', __NAMESPACE__ . '\replace_ youtube_embed_with_web_component', 10, 2 );60 add_filter( 'render_block_core/embed', __NAMESPACE__ . '\replace_embeds_with_web_components', 10, 2 ); 58 61 /** 59 62 * Filter the Embed output and replace it with the web component … … 63 66 * @return string HTML for embed block 64 67 */ 65 function replace_youtube_embed_with_web_component( $content, $block ) { 66 $isValidYouTube = 'youtube' === $block['attrs']['providerNameSlug'] && isset( $block['attrs']['url'] ); 67 if( ! $isValidYouTube || is_feed() ) { 68 function replace_embeds_with_web_components( $content, $block ) { 69 70 if ( 71 ! isset( $block['attrs']['url'] ) || 72 is_feed() || 73 ! in_array( 74 $block['attrs']['providerNameSlug'], 75 array( 'youtube', 'vimeo' ), 76 true 77 ) 78 ) { 68 79 return $content; 69 80 } 70 81 71 $video_id = extract_youtube_id_from_uri( $block['attrs']['url'] ); 72 if ( ! $video_id ) { 73 return $content; 82 wp_enqueue_style( 'lite-embed-fallback' ); 83 84 switch ( $block['attrs']['providerNameSlug'] ) { 85 case 'youtube': 86 $content = render_youtube_embed( $content, $block ); 87 break; 88 89 case 'vimeo': 90 $content = render_vimeo_embed( $content, $block ); 91 break; 74 92 } 75 76 wp_enqueue_script_module( 'lite-youtube' );77 78 $video_title = extract_title_from_embed_code( $content );79 $start_time = extract_start_time_from_uri( $block['attrs']['url'] );80 $embed_caption = extract_figcaption_from_embed_code( $content );81 82 /* translators: %s: title from YouTube video */83 $play_button = sprintf( __( 'Play: %s', 'enhanced-embed-block' ), $video_title );84 85 /**86 * Filter the poster quality for the YouTube preview thumbnail87 *88 * @since 1.1.089 * @param string $quality One of mqdefault, hqdefault, sddefault, or maxresdefault (default)90 */91 $poster_quality = apply_filters( 'eeb_posterquality', 'maxresdefault' );92 93 /**94 * Filter to determine whether to load embed from nocookie YouTube domain95 *96 * @since 1.1.097 * @param bool $use_nocookie Whether to use the nocookie domain (default: true)98 */99 $nocookie = apply_filters( 'eeb_nocookie', true );100 101 /* Craft the new output: the web component with HTML fallback link */102 $content = sprintf(103 '<figure class="wp-block-embed-youtube wp-block-embed is-type-video is-provider-youtube">104 <div class="wp-block-embed__wrapper">105 <lite-youtube videoid="%1$s" videoplay="%2$s" videoStartAt="%3$d" posterquality="%4$s" posterloading="lazy"%5$s>106 <a href="%6$s" class="lite-youtube-fallback" target="_blank" rel="noreferrer noopenner">Watch "%7$s" on YouTube</a>107 </lite-youtube>108 </div>109 %8$s110 </figure>',111 esc_attr( $video_id ),112 esc_attr( $play_button ),113 $start_time ? intval( $start_time ) : 0,114 in_array( $poster_quality, array( 'mqdefault', 'hqdefault', 'sddefault', 'maxresdefault' ), true ) ? $poster_quality : 'maxresdefault',115 $nocookie ? ' nocookie' : '',116 esc_url( $block['attrs']['url'] ),117 esc_html( $video_title ),118 $embed_caption119 );120 93 121 94 return $content; 122 95 } 123 96 124 /** 125 * Extract the value of the title attribute from HTML that contains an iframe of an existing YouTube embed code 126 * 127 * @param string $html A block of HTML containing a YouTube iframe. 128 * @return string the title attribute valube 129 */ 130 function extract_title_from_embed_code( $html ) { 131 $processor = new WP_HTML_TAG_Processor( $html ); 132 $processor->next_tag( 'iframe' ); 133 $title = $processor->get_attribute( 'title' ); 134 135 return $title; 136 } 137 138 /** 139 * Extract the figcaption from the embed code 140 * 141 * @param string $html A block of HTML containing a YouTube iframe. 142 * @return string The figcaption OR an empty string 143 * 144 * @todo Replace this with HTML Tag Processor, if possible 145 */ 146 function extract_figcaption_from_embed_code( $html ) { 147 preg_match( '/<figcaption(.*?)<\/figcaption>/s', $html, $match ); 148 return isset( $match[0] ) ? $match[0] : false; 149 } 150 151 /** 152 * Get the YouTube video ID from a YouTube video URL, either the default youtube.com one or the shortened youtu.be one 153 * 154 * @param string $uri A YouTube video URL. 155 * @return string video ID for a YouTube video 156 */ 157 function extract_youtube_id_from_uri( $uri ) { 158 $host = wp_parse_url( $uri, PHP_URL_HOST ); 159 160 /* Handle Shortlinks */ 161 if ( 'youtu.be' === $host ) { 162 return ltrim( wp_parse_url( $uri, PHP_URL_PATH ), '/' ); 163 } 164 165 $params = wp_parse_url( $uri, PHP_URL_QUERY ); 166 parse_str( $params, $query ); 167 return $query['v'] ?? false; 168 } 169 170 /** 171 * Extract the start time parameter "t" from YouTube video URL 172 * 173 * @param string $uri URL of YouTube video that may or may not contain a start time parameter. 174 * @return string|bool The value of the t parameter or false if it isn't present 175 */ 176 function extract_start_time_from_uri( $uri ) { 177 $params = wp_parse_url( $uri, PHP_URL_QUERY ); 178 parse_str( $params, $query ); 179 return $query['t'] ?? false; 180 } 97 require_once plugin_dir_path( __FILE__ ) . 'inc/generic.php'; 98 require_once plugin_dir_path( __FILE__ ) . 'inc/vimeo.php'; 99 require_once plugin_dir_path( __FILE__ ) . 'inc/youtube.php'; -
enhanced-embed-block/trunk/readme.txt
r3116570 r3293487 1 === Enhanced Embed Block for YouTube ===1 === Enhanced Embed Block for YouTube & Vimeo === 2 2 Contributors: mrwweb, cbirdsong 3 3 Donate link: https://paypal.me/rootwiley 4 Tags: YouTube, embed, video, block, performance4 Tags: YouTube, Vimeo, embed, video, block 5 5 Requires at least: 6.5 6 Tested up to: 6. 66 Tested up to: 6.8 7 7 Requires PHP: 7.4 8 Stable tag: 1. 1.08 Stable tag: 1.2.0 9 9 License: GPLv3 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-3.0.html 11 11 12 Enhance the default YouTube Embed Blockto load faster.12 Enhance the default YouTube and Vimeo Embed blocks to load faster. 13 13 14 14 == Description == … … 16 16 If you care about performance, privacy, and user experience, this block is for you. 17 17 18 This plugin enhances the default YouTube block—including any existing blocks—and changes their behavior to only load the video thumbnail until a visitor chooses to play the video.18 This plugin enhances the default YouTube and Vimeo blocks—including any existing blocks—and changes their behavior to only load the video thumbnail until a visitor chooses to play the video. 19 19 20 20 = Features = 21 21 22 22 * Load YouTube videos faster (uses the `lite-youtube` custom-element) 23 * Loads videos from nocookie.youtube.com for enhanced privacy 24 * Works without JavaScript (shows link to YouTube video instead) 25 * No plugin lock-in! Automatically improves all YouTube embeds. Turn it off and the behavior goes back to the WordPress default. 23 * Load Vimeo videos faster (uses the `lite-viemo` custom-element) 24 * Loads YouTube videos from nocookie.youtube.com for enhanced privacy 25 * Works without JavaScript (shows link to video instead in a player-like design) 26 * No plugin lock-in! Automatically improves the core Embed block. Turn the plugin off and the behavior goes back to the WordPress default. 26 27 27 28 = Want more features? = … … 38 39 * Full support for all YouTube query parameters (https://developers.google.com/youtube/player_parameters) 39 40 * Classic Editor / [embed] shortcode support 40 * Support similar features for Vimeo and other embed sources where possible41 41 42 42 If enough people express interest, I'll build it! [Let me know if you're interested!](https://mrwweb.com/wordpress-plugins/enhanced-embed-block/#pro) … … 49 49 50 50 1. From your WordPress site’s dashboard, go to Plugins > Add New. 51 2. Search for “Enhanced Embed Block for YouTube ”51 2. Search for “Enhanced Embed Block for YouTube and Vimeo” 52 52 3. Click “Install” 53 53 4. Click “Activate” … … 58 58 = Does this create a new block? = 59 59 60 No. It enhances the default WordPress embed block for YouTubevideos.60 No. It enhances the default WordPress Embed block for YouTube and Vimeo videos. 61 61 62 = Does it automatically enhance all my YouTube embeds? =62 = Does it automatically enhance all my YouTube and Vimeo embeds? = 63 63 64 It works for any embeds using the YouTube block. Embeds using the [embed] shortcode or literal YouTube embed code in HTML are not enhanced. Using the core WordPress YouTubeEmbed block is highly recommended!64 It works for any embeds using the YouTube or Vimeo variations of the Embed block. Embeds using the [embed] shortcode or literal YouTube embed code in HTML are not enhanced. Using the core WordPress Embed block is highly recommended! 65 65 66 = Why do esn't Google load allvideos this way by default? =66 = Why don't Google and Vimeo load all their videos this way by default? = 67 67 68 68 Great question! It sure seems like they should. If I had to guess, they are prioritizing usage tracking over fast load times and privacy. … … 80 80 This plugin uses the [`lite-youtube` custom-element](https://github.com/justinribeiro/lite-youtube) under the MIT license. Thank you to Paul Irish and Justin Ribiero for their work on that project. 81 81 82 This plugin uses the [`lite-vimeo` custom-element](https://github.com/cshawaus/lite-vimeo) under the MIT license. Thank you to Chris Shaw for their work on that project. 83 82 84 == Changelog == 83 85 84 = 1.1.0 = 86 = 1.2.0 (14 May 2025) = 87 88 - Add support for Vimeo! 89 - Upgrade `lite-youtube` to 1.8.1 (includes new native support for fallback thumbnail formats and sizes) 90 - Further performance improvements to load script asynchronously and only load styles when needed 91 - Fix undefined $params fatal error when trying to extract time code from YouTube URLs 92 - Code quality improvements 93 94 = 1.1.0 (11 July 2024) = 95 85 96 - Fix missing file on WordPress.org version of plugin due to misconfigured Github deployment 86 97 - MAJOR CHANGE: The default poster image is now the highest quality possible. There is a new `eeb_posterquality` filter to change that, if desired. (#5) … … 91 102 - Props to @cbirdsong for numerous issues on Github that led to most of these changes 92 103 93 = 1.0.0 = 104 = 1.0.0 (22 April 2024) = 105 94 106 - Initial release to the WordPress repository! 95 107 96 108 == Upgrade Notice == 97 109 98 = 1. 1.0 =99 Fix plugin in WordPress repository. Use higher quality poster image with new fallback detection. Don't apply embed changes to feeds. Developer improvements.110 = 1.2.0 = 111 Add Vimeo support! Upgrade lite-youtube custom element. Even faster loading times. -
enhanced-embed-block/trunk/vendor/lite-youtube/lite-youtube.js
r3116570 r3293487 1 1 export class LiteYTEmbed extends HTMLElement { 2 constructor() { 3 super(); 4 this.isIframeLoaded = false; 5 this.setupDom(); 6 } 7 static get observedAttributes() { 8 return ["videoid", "playlistid"]; 9 } 10 connectedCallback() { 11 this.addEventListener("pointerover", LiteYTEmbed.warmConnections, { 12 once: true, 13 }); 14 this.addEventListener("click", () => this.addIframe()); 15 } 16 get videoId() { 17 return encodeURIComponent(this.getAttribute("videoid") || ""); 18 } 19 set videoId(id) { 20 this.setAttribute("videoid", id); 21 } 22 get playlistId() { 23 return encodeURIComponent(this.getAttribute("playlistid") || ""); 24 } 25 set playlistId(id) { 26 this.setAttribute("playlistid", id); 27 } 28 get videoTitle() { 29 return this.getAttribute("videotitle") || "Video"; 30 } 31 set videoTitle(title) { 32 this.setAttribute("videotitle", title); 33 } 34 get videoPlay() { 35 return this.getAttribute("videoPlay") || "Play"; 36 } 37 set videoPlay(name) { 38 this.setAttribute("videoPlay", name); 39 } 40 get videoStartAt() { 41 return this.getAttribute("videoStartAt") || "0"; 42 } 43 get autoLoad() { 44 return this.hasAttribute("autoload"); 45 } 46 get noCookie() { 47 return this.hasAttribute("nocookie"); 48 } 49 get posterQuality() { 50 return this.getAttribute("posterquality") || "hqdefault"; 51 } 52 get posterLoading() { 53 return this.getAttribute("posterloading") || "lazy"; 54 } 55 get params() { 56 return `start=${this.videoStartAt}&${this.getAttribute("params")}`; 57 } 58 set params(opts) { 59 this.setAttribute("params", opts); 60 } 61 setupDom() { 62 const shadowDom = this.attachShadow({ mode: "open" }); 63 let nonce = ""; 64 if (window.liteYouTubeNonce) { 65 nonce = `nonce="${window.liteYouTubeNonce}"`; 66 } 67 shadowDom.innerHTML = ` 2 constructor() { 3 super(); 4 this.isIframeLoaded = false; 5 this.setupDom(); 6 } 7 static get observedAttributes() { 8 return ['videoid', 'playlistid', 'videoplay', 'videotitle']; 9 } 10 connectedCallback() { 11 this.addEventListener('pointerover', () => LiteYTEmbed.warmConnections(this), { 12 once: true, 13 }); 14 this.addEventListener('click', () => this.addIframe()); 15 } 16 get videoId() { 17 return encodeURIComponent(this.getAttribute('videoid') || ''); 18 } 19 set videoId(id) { 20 this.setAttribute('videoid', id); 21 } 22 get playlistId() { 23 return encodeURIComponent(this.getAttribute('playlistid') || ''); 24 } 25 set playlistId(id) { 26 this.setAttribute('playlistid', id); 27 } 28 get videoTitle() { 29 return this.getAttribute('videotitle') || 'Video'; 30 } 31 set videoTitle(title) { 32 this.setAttribute('videotitle', title); 33 } 34 get videoPlay() { 35 return this.getAttribute('videoplay') || 'Play'; 36 } 37 set videoPlay(name) { 38 this.setAttribute('videoplay', name); 39 } 40 get videoStartAt() { 41 return this.getAttribute('videoStartAt') || '0'; 42 } 43 get autoLoad() { 44 return this.hasAttribute('autoload'); 45 } 46 get autoPause() { 47 return this.hasAttribute('autopause'); 48 } 49 get noCookie() { 50 return this.hasAttribute('nocookie'); 51 } 52 get posterQuality() { 53 return this.getAttribute('posterquality') || 'hqdefault'; 54 } 55 get posterLoading() { 56 return (this.getAttribute('posterloading') || 57 'lazy'); 58 } 59 get params() { 60 return `start=${this.videoStartAt}&${this.getAttribute('params')}`; 61 } 62 set params(opts) { 63 this.setAttribute('params', opts); 64 } 65 set posterQuality(opts) { 66 this.setAttribute('posterquality', opts); 67 } 68 get disableNoscript() { 69 return this.hasAttribute('disablenoscript'); 70 } 71 setupDom() { 72 const shadowDom = this.attachShadow({ mode: 'open' }); 73 let nonce = ''; 74 if (window.liteYouTubeNonce) { 75 nonce = `nonce="${window.liteYouTubeNonce}"`; 76 } 77 shadowDom.innerHTML = ` 68 78 <style ${nonce}> 69 79 :host { 80 --aspect-ratio: var(--lite-youtube-aspect-ratio, 16 / 9); 81 --aspect-ratio-short: var(--lite-youtube-aspect-ratio-short, 9 / 16); 82 --frame-shadow-visible: var(--lite-youtube-frame-shadow-visible, yes); 70 83 contain: content; 71 84 display: block; 72 85 position: relative; 73 86 width: 100%; 74 padding-bottom: calc(100% / (16 / 9));87 aspect-ratio: var(--aspect-ratio); 75 88 } 76 89 77 90 @media (max-width: 40em) { 78 91 :host([short]) { 79 padding-bottom: calc(100% / (9 / 16));92 aspect-ratio: var(--aspect-ratio-short); 80 93 } 81 94 } … … 86 99 height: 100%; 87 100 left: 0; 101 top: 0; 88 102 } 89 103 … … 92 106 } 93 107 94 #fallbackPlaceholder {108 #fallbackPlaceholder, slot[name=image]::slotted(*) { 95 109 object-fit: cover; 96 }97 98 #frame::before {99 content: '';100 display: block;101 position: absolute;102 top: 0;103 background-image: linear-gradient(180deg, #111 -20%, transparent 90%);104 height: 60px;105 110 width: 100%; 106 z-index: 1; 111 } 112 113 @container style(--frame-shadow-visible: yes) { 114 #frame::before { 115 content: ''; 116 display: block; 117 position: absolute; 118 top: 0; 119 background-image: linear-gradient(180deg, #111 -20%, transparent 90%); 120 height: 60px; 121 width: 100%; 122 z-index: 1; 123 } 107 124 } 108 125 … … 145 162 <div id="frame"> 146 163 <picture> 147 <source id="webpPlaceholder" type="image/webp"> 148 <source id="jpegPlaceholder" type="image/jpeg"> 149 <img id="fallbackPlaceholder" referrerpolicy="origin" loading="lazy"> 164 <slot name="image"> 165 <source id="webpPlaceholder" type="image/webp"> 166 <source id="jpegPlaceholder" type="image/jpeg"> 167 <img id="fallbackPlaceholder" referrerpolicy="origin" loading="lazy"> 168 </slot> 150 169 </picture> 151 <button id="playButton" ></button>170 <button id="playButton" part="playButton"></button> 152 171 </div> 153 172 `; 154 this.domRefFrame = shadowDom.querySelector("#frame"); 155 this.domRefImg = { 156 fallback: shadowDom.querySelector("#fallbackPlaceholder"), 157 webp: shadowDom.querySelector("#webpPlaceholder"), 158 jpeg: shadowDom.querySelector("#jpegPlaceholder"), 159 }; 160 this.domRefPlayButton = shadowDom.querySelector("#playButton"); 161 } 162 setupComponent() { 163 this.initImagePlaceholder(); 164 this.domRefPlayButton.setAttribute( 165 "aria-label", 166 `${this.videoPlay}: ${this.videoTitle}` 167 ); 168 this.setAttribute("title", `${this.videoPlay}: ${this.videoTitle}`); 169 if (this.autoLoad || this.isYouTubeShort()) { 170 this.initIntersectionObserver(); 171 } 172 } 173 attributeChangedCallback(name, oldVal, newVal) { 174 switch (name) { 175 case "videoid": 176 case "playlistid": 177 case "videoTitle": 178 case "videoPlay": { 179 if (oldVal !== newVal) { 180 this.setupComponent(); 181 if (this.domRefFrame.classList.contains("activated")) { 182 this.domRefFrame.classList.remove("activated"); 183 this.shadowRoot.querySelector("iframe").remove(); 184 this.isIframeLoaded = false; 185 } 186 } 187 break; 188 } 189 default: 190 break; 191 } 192 } 193 addIframe(isIntersectionObserver = false) { 194 if (!this.isIframeLoaded) { 195 let autoplay = isIntersectionObserver ? 0 : 1; 196 const wantsNoCookie = this.noCookie ? "-nocookie" : ""; 197 let embedTarget; 198 if (this.playlistId) { 199 embedTarget = `?listType=playlist&list=${this.playlistId}&`; 200 } else { 201 embedTarget = `${this.videoId}?`; 202 } 203 if (this.isYouTubeShort()) { 204 this.params = `loop=1&mute=1&modestbranding=1&playsinline=1&rel=0&enablejsapi=1&playlist=${this.videoId}`; 205 autoplay = 1; 206 } 207 const iframeHTML = ` 208 <iframe frameborder="0" title="${this.videoTitle}" 173 this.domRefFrame = shadowDom.querySelector('#frame'); 174 this.domRefImg = { 175 fallback: shadowDom.querySelector('#fallbackPlaceholder'), 176 webp: shadowDom.querySelector('#webpPlaceholder'), 177 jpeg: shadowDom.querySelector('#jpegPlaceholder'), 178 }; 179 this.domRefPlayButton = shadowDom.querySelector('#playButton'); 180 } 181 setupComponent() { 182 const hasImgSlot = this.shadowRoot.querySelector('slot[name=image]'); 183 if (hasImgSlot.assignedNodes().length === 0) { 184 this.initImagePlaceholder(); 185 } 186 this.domRefPlayButton.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); 187 this.setAttribute('title', `${this.videoPlay}: ${this.videoTitle}`); 188 if (this.autoLoad || this.isYouTubeShort() || this.autoPause) { 189 this.initIntersectionObserver(); 190 } 191 if (!this.disableNoscript) { 192 this.injectSearchNoScript(); 193 } 194 } 195 attributeChangedCallback(name, oldVal, newVal) { 196 if (oldVal !== newVal) { 197 this.setupComponent(); 198 if (this.domRefFrame.classList.contains('activated')) { 199 this.domRefFrame.classList.remove('activated'); 200 this.shadowRoot.querySelector('iframe').remove(); 201 this.isIframeLoaded = false; 202 } 203 } 204 } 205 injectSearchNoScript() { 206 const eleNoScript = document.createElement('noscript'); 207 this.prepend(eleNoScript); 208 eleNoScript.innerHTML = this.generateIframe(); 209 } 210 generateIframe(isIntersectionObserver = false) { 211 let autoplay = isIntersectionObserver ? 0 : 1; 212 const wantsNoCookie = this.noCookie ? '-nocookie' : ''; 213 let embedTarget; 214 if (this.playlistId) { 215 embedTarget = `?listType=playlist&list=${this.playlistId}&`; 216 } 217 else { 218 embedTarget = `${this.videoId}?`; 219 } 220 if (this.autoPause) { 221 this.params = `enablejsapi=1`; 222 } 223 if (this.isYouTubeShort()) { 224 this.params = `loop=1&mute=1&modestbranding=1&playsinline=1&rel=0&enablejsapi=1&playlist=${this.videoId}`; 225 autoplay = 1; 226 } 227 return ` 228 <iframe credentialless frameborder="0" title="${this.videoTitle}" 209 229 allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen 210 230 src="https://www.youtube${wantsNoCookie}.com/embed/${embedTarget}autoplay=${autoplay}&${this.params}" 211 231 ></iframe>`; 212 this.domRefFrame.insertAdjacentHTML("beforeend", iframeHTML); 213 this.domRefFrame.classList.add("activated"); 214 this.isIframeLoaded = true; 215 this.attemptShortAutoPlay(); 216 this.dispatchEvent( 217 new CustomEvent("liteYoutubeIframeLoaded", { 218 detail: { 219 videoId: this.videoId, 220 }, 221 bubbles: true, 222 cancelable: true, 223 }) 224 ); 225 } 226 } 227 initImagePlaceholder() { 228 const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`; 229 const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/${this.posterQuality}.jpg`; 230 // PATCH: This changes the fallback img src to be the hqdefault quality no matter what since some old videos don't have higher resolution 231 const posterUrlFallback = `https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg`; 232 this.domRefImg.fallback.loading = this.posterLoading; 233 this.domRefImg.webp.srcset = posterUrlWebp; 234 this.domRefImg.jpeg.srcset = posterUrlJpeg; 235 // PATCH: Change fallback src. See comment on line 230 for reasoning 236 this.domRefImg.fallback.src = posterUrlFallback; 237 this.domRefImg.fallback.setAttribute( 238 "aria-label", 239 `${this.videoPlay}: ${this.videoTitle}` 240 ); 241 this.domRefImg?.fallback?.setAttribute( 242 "alt", 243 `${this.videoPlay}: ${this.videoTitle}` 244 ); 245 // PATCH: Recursively load picture sources in order, deleting any where the image's natural width indicates that it is the YouTube image fallback placeholder rather than a real video poster. 246 this.domRefImg.fallback.onload = (e) => { 247 if (e.target.naturalWidth === 120) { 248 this.domRefImg.fallback.parentElement.firstElementChild.remove(); 249 } 250 }; 251 } 252 initIntersectionObserver() { 253 const options = { 254 root: null, 255 rootMargin: "0px", 256 threshold: 0, 257 }; 258 const observer = new IntersectionObserver((entries, observer) => { 259 entries.forEach((entry) => { 260 if (entry.isIntersecting && !this.isIframeLoaded) { 261 LiteYTEmbed.warmConnections(); 262 this.addIframe(true); 263 observer.unobserve(this); 264 } 265 }); 266 }, options); 267 observer.observe(this); 268 } 269 attemptShortAutoPlay() { 270 if (this.isYouTubeShort()) { 271 setTimeout(() => { 272 this.shadowRoot 273 .querySelector("iframe") 274 ?.contentWindow?.postMessage( 275 '{"event":"command","func":"' + 276 "playVideo" + 277 '","args":""}', 278 "*" 279 ); 280 }, 2000); 281 } 282 } 283 isYouTubeShort() { 284 return ( 285 this.getAttribute("short") === "" && 286 window.matchMedia("(max-width: 40em)").matches 287 ); 288 } 289 static addPrefetch(kind, url) { 290 const linkElem = document.createElement("link"); 291 linkElem.rel = kind; 292 linkElem.href = url; 293 linkElem.crossOrigin = "true"; 294 document.head.append(linkElem); 295 } 296 static warmConnections() { 297 if (LiteYTEmbed.isPreconnected || window.liteYouTubeIsPreconnected) 298 return; 299 LiteYTEmbed.addPrefetch("preconnect", "https://i.ytimg.com/"); 300 LiteYTEmbed.addPrefetch("preconnect", "https://s.ytimg.com"); 301 LiteYTEmbed.addPrefetch("preconnect", "https://www.youtube.com"); 302 LiteYTEmbed.addPrefetch("preconnect", "https://www.google.com"); 303 LiteYTEmbed.addPrefetch( 304 "preconnect", 305 "https://googleads.g.doubleclick.net" 306 ); 307 LiteYTEmbed.addPrefetch("preconnect", "https://static.doubleclick.net"); 308 LiteYTEmbed.isPreconnected = true; 309 window.liteYouTubeIsPreconnected = true; 310 } 232 } 233 addIframe(isIntersectionObserver = false) { 234 if (!this.isIframeLoaded) { 235 const iframeHTML = this.generateIframe(isIntersectionObserver); 236 this.domRefFrame.insertAdjacentHTML('beforeend', iframeHTML); 237 this.domRefFrame.classList.add('activated'); 238 this.isIframeLoaded = true; 239 this.attemptShortAutoPlay(); 240 this.dispatchEvent(new CustomEvent('liteYoutubeIframeLoaded', { 241 detail: { 242 videoId: this.videoId, 243 }, 244 bubbles: true, 245 cancelable: true, 246 })); 247 } 248 } 249 initImagePlaceholder() { 250 this.testPosterImage(); 251 this.domRefImg.fallback.setAttribute('aria-label', `${this.videoPlay}: ${this.videoTitle}`); 252 this.domRefImg?.fallback?.setAttribute('alt', `${this.videoPlay}: ${this.videoTitle}`); 253 } 254 async testPosterImage() { 255 setTimeout(() => { 256 const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`; 257 const img = new Image(); 258 img.fetchPriority = 'low'; 259 img.referrerPolicy = 'origin'; 260 img.src = webpUrl; 261 img.onload = async (e) => { 262 const target = e.target; 263 const noPoster = target?.naturalHeight == 90 && target?.naturalWidth == 120; 264 if (noPoster) { 265 this.posterQuality = 'hqdefault'; 266 } 267 const posterUrlWebp = `https://i.ytimg.com/vi_webp/${this.videoId}/${this.posterQuality}.webp`; 268 this.domRefImg.webp.srcset = posterUrlWebp; 269 const posterUrlJpeg = `https://i.ytimg.com/vi/${this.videoId}/${this.posterQuality}.jpg`; 270 this.domRefImg.fallback.loading = this.posterLoading; 271 this.domRefImg.jpeg.srcset = posterUrlJpeg; 272 this.domRefImg.fallback.src = posterUrlJpeg; 273 this.domRefImg.fallback.loading = this.posterLoading; 274 }; 275 }, 100); 276 } 277 initIntersectionObserver() { 278 const options = { 279 root: null, 280 rootMargin: '0px', 281 threshold: 0, 282 }; 283 const observer = new IntersectionObserver((entries, observer) => { 284 entries.forEach(entry => { 285 if (entry.isIntersecting && !this.isIframeLoaded) { 286 LiteYTEmbed.warmConnections(this); 287 this.addIframe(true); 288 observer.unobserve(this); 289 } 290 }); 291 }, options); 292 observer.observe(this); 293 if (this.autoPause) { 294 const windowPause = new IntersectionObserver((e, o) => { 295 e.forEach(entry => { 296 if (entry.intersectionRatio !== 1) { 297 this.shadowRoot 298 .querySelector('iframe') 299 ?.contentWindow?.postMessage('{"event":"command","func":"pauseVideo","args":""}', '*'); 300 } 301 }); 302 }, { threshold: 1 }); 303 windowPause.observe(this); 304 } 305 } 306 attemptShortAutoPlay() { 307 if (this.isYouTubeShort()) { 308 setTimeout(() => { 309 this.shadowRoot 310 .querySelector('iframe') 311 ?.contentWindow?.postMessage('{"event":"command","func":"' + 'playVideo' + '","args":""}', '*'); 312 }, 2000); 313 } 314 } 315 isYouTubeShort() { 316 return (this.getAttribute('short') === '' && 317 window.matchMedia('(max-width: 40em)').matches); 318 } 319 static addPrefetch(kind, url) { 320 const linkElem = document.createElement('link'); 321 linkElem.rel = kind; 322 linkElem.href = url; 323 linkElem.crossOrigin = 'true'; 324 document.head.append(linkElem); 325 } 326 static warmConnections(context) { 327 if (LiteYTEmbed.isPreconnected || window.liteYouTubeIsPreconnected) 328 return; 329 LiteYTEmbed.addPrefetch('preconnect', 'https://i.ytimg.com/'); 330 LiteYTEmbed.addPrefetch('preconnect', 'https://s.ytimg.com'); 331 if (!context.noCookie) { 332 LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube.com'); 333 LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com'); 334 LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net'); 335 LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net'); 336 } 337 else { 338 LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com'); 339 } 340 LiteYTEmbed.isPreconnected = true; 341 window.liteYouTubeIsPreconnected = true; 342 } 311 343 } 312 344 LiteYTEmbed.isPreconnected = false; 313 customElements.define( "lite-youtube", LiteYTEmbed);345 customElements.define('lite-youtube', LiteYTEmbed); 314 346 //# sourceMappingURL=lite-youtube.js.map
Note: See TracChangeset
for help on using the changeset viewer.