PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.6
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.6
1.3.6 1.3.5 1.3.4 1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 All 32 releases
← All changes | includes/class-video-facade.php +171 -0 1.1.5 → 1.3.6 View file →
@@ -170,8 +170,51 @@
170 170 return $markup;
171 171 }
172 172
173 173 /**
174 + * Build the facade markup for one self-hosted <video>. (#309)
175 + *
176 + * Same contract as render(): a focusable <button>, the original
177 + * element preserved whole inside <noscript>, the swap done by
178 + * facade_script(). Differences that matter:
179 + *
180 + * - the poster comes from the element's own poster attribute, never
181 + * derived — the caller has already refused to build a facade
182 + * without one, because a blank black box is worse than the video.
183 + * - the source URL travels in data-xspeed-video-native, a separate
184 + * attribute from the iframe player URL, so the click handler knows
185 + * to build a <video controls autoplay> rather than an <iframe>.
186 + *
187 + * @param string $original_markup The untouched <video …>…</video> element.
188 + * @param string $src The video file URL to load on click.
189 + * @param string $poster The element's own poster URL.
190 + * @param string $title Accessible label for the play button.
191 + */
192 + public static function render_native( string $original_markup, string $src, string $poster, string $title = '' ): string {
193 + $label = '' !== $title
194 + ? sprintf(
195 + /* translators: %s: video title. */
196 + __( 'Play video: %s', 'xspeed' ),
197 + $title
198 + )
199 + : __( 'Play video', 'xspeed' );
200 +
201 + $style = 'position:relative;display:block;width:100%;padding:0;border:0;cursor:pointer;background:#000;aspect-ratio:16/9;';
202 + $style .= 'background-image:url(' . esc_url( $poster ) . ');background-size:cover;background-position:center;';
203 +
204 + $markup = '<button type="button" class="xspeed-video-facade" data-xspeed-video-native="' . esc_url( $src ) . '"';
205 + $markup .= ' data-xspeed-poster="' . esc_url( $poster ) . '"';
206 + $markup .= ' aria-label="' . esc_attr( $label ) . '" style="' . esc_attr( $style ) . '">';
207 + $markup .= '<span class="xspeed-video-facade__play" aria-hidden="true" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:68px;height:48px;border-radius:14px;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;">';
208 + $markup .= '<svg width="24" height="24" viewBox="0 0 24 24" fill="#fff" focusable="false"><path d="M8 5v14l11-7z"/></svg>';
209 + $markup .= '</span>';
210 + $markup .= '</button>';
211 + $markup .= '<noscript>' . $original_markup . '</noscript>';
212 +
213 + return $markup;
214 + }
215 +
216 + /**
174 217 * The one CSS rule the facade can't express as an inline style.
175 218 *
176 219 * Gutenberg wraps an embed in
177 220 * `figure.wp-has-aspect-ratio > div.wp-block-embed__wrapper`, gives the
@@ -186,8 +229,14 @@
186 229 * standalone layout.
187 230 */
188 231 public static function facade_style(): string {
189 232 return '.wp-has-aspect-ratio .xspeed-video-facade{position:absolute!important;top:0;right:0;bottom:0;left:0;'
233 + . 'width:100%!important;height:100%!important;aspect-ratio:auto!important}'
234 + // Magnific Popup's iframe scaler positions only `iframe` children;
235 + // a facade that reaches a popup at runtime (a template class the
236 + // server pass doesn't know) would otherwise collapse to nothing
237 + // and the modal opens blank. Same absolute-fill treatment.
238 + . '.mfp-iframe-scaler .xspeed-video-facade{position:absolute!important;top:0;right:0;bottom:0;left:0;'
190 239 . 'width:100%!important;height:100%!important;aspect-ratio:auto!important}';
191 240 }
192 241
193 242 /**
@@ -202,10 +251,27 @@
202 251 return <<<'JS'
203 252 document.addEventListener('click',function(e){
204 253 var b=e.target.closest&&e.target.closest('.xspeed-video-facade');
205 254 if(!b)return;
255 +var n=b.getAttribute('data-xspeed-video-native');
256 +if(n){
257 +var v=document.createElement('video');
258 +v.setAttribute('src',n);
259 +var p=b.getAttribute('data-xspeed-poster');if(p)v.setAttribute('poster',p);
260 +v.setAttribute('controls','');
261 +v.setAttribute('autoplay','');
262 +v.setAttribute('playsinline','');
263 +v.setAttribute('style','width:100%;aspect-ratio:16/9;background:#000;');
264 +var nt=b.getAttribute('aria-label');if(nt)v.setAttribute('title',nt);
265 +b.parentNode.replaceChild(v,b);
266 +return;
267 +}
206 268 var u=b.getAttribute('data-xspeed-video');if(!u)return;
207 269 var f=document.createElement('iframe');
270 +// Released BEFORE src: the observer script's interceptor holds any
271 +// provider src it sees, and the player the visitor just asked for is
272 +// the one iframe that must load immediately.
273 +f.setAttribute('data-xspeed-loaded','1');
208 274 f.setAttribute('src',u);
209 275 f.setAttribute('frameborder','0');
210 276 f.setAttribute('allow','accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture');
211 277 f.setAttribute('allowfullscreen','');
@@ -212,7 +278,112 @@
212 278 f.setAttribute('style','width:100%;aspect-ratio:16/9;border:0;');
213 279 var t=b.getAttribute('aria-label');if(t)f.setAttribute('title',t);
214 280 b.parentNode.replaceChild(f,b);
215 281 },false);
282 +JS;
283 + }
284 +
285 + /**
286 + * The early interceptor for embeds that never appear in the HTML.
287 + *
288 + * A page-builder video widget builds its YouTube/Vimeo <iframe> from
289 + * its own script, so the server pass — which rewrites the response
290 + * buffer — never sees an element to replace, and the full player
291 + * (3MB+ of JS per embed) downloads on page load anyway. Measured live:
292 + * one homepage carried three JS-built embeds and 22MB of YouTube
293 + * player resources with the facade "on".
294 + *
295 + * Same playbook as Lazy_Loader::autoplay_script(), for the same
296 + * reason: builders set `src` BEFORE inserting the element, so a
297 + * MutationObserver alone is always too late — the fetch starts
298 + * off-DOM. So the property setter and setAttribute are wrapped, a
299 + * recognised provider src is parked in data-xspeed-held instead of
300 + * applied, and the observer only has to dress the inert element as a
301 + * facade once it lands in the DOM. The click handler above does the
302 + * swap; its data-xspeed-loaded release mark is honoured here so the
303 + * player it builds is never re-held.
304 + *
305 + * The provider patterns mirror parse_embed()/poster_url() and must
306 + * stay in step with them — one facade, two capture paths.
307 + */
308 + public static function observer_script(): string {
309 + $label = wp_json_encode( __( 'Play video', 'xspeed' ) );
310 +
311 + return <<<JS
312 +(function(){
313 +var L={$label};
314 +function parse(u){
315 +if(!u)return null;
316 +u=String(u).replace(/^\/\//,'https://');
317 +var m=u.match(/^https?:\/\/(?:www\.)?(?:youtube(?:-nocookie)?\.com\/embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,20})/i);
318 +if(m)return{p:'youtube',id:m[1]};
319 +m=u.match(/^https?:\/\/player\.vimeo\.com\/video\/(\d{6,12})/i);
320 +if(m)return{p:'vimeo',id:m[1]};
321 +return null;
322 +}
323 +function hold(el,val){
324 +if(!el||el.tagName!=='IFRAME')return false;
325 +if(el.getAttribute('data-xspeed-loaded')||el.hasAttribute('data-skip-lazy'))return false;
326 +if(!parse(val))return false;
327 +el.setAttribute('data-xspeed-held',String(val));
328 +return true;
329 +}
330 +try{
331 +var IP=window.HTMLIFrameElement&&HTMLIFrameElement.prototype;
332 +var SD=IP&&Object.getOwnPropertyDescriptor(IP,'src');
333 +if(SD&&SD.set){
334 +Object.defineProperty(IP,'src',{configurable:true,enumerable:SD.enumerable,
335 +get:function(){return SD.get.call(this);},
336 +set:function(v){if(hold(this,v))return;return SD.set.call(this,v);}});
337 +}
338 +var SA=Element.prototype.setAttribute;
339 +Element.prototype.setAttribute=function(n,v){
340 +if(n==='src'&&hold(this,v))return;
341 +return SA.call(this,n,v);
342 +};
343 +}catch(e){}
344 +function dress(f){
345 +if(!f.parentNode)return;
346 +var s=f.getAttribute('data-xspeed-held');
347 +var e=parse(s);
348 +if(!e)return;
349 +var u=s+(s.indexOf('autoplay=')>-1?'':(s.indexOf('?')>-1?'&':'?')+'autoplay=1');
350 +var b=document.createElement('button');
351 +b.type='button';
352 +b.className='xspeed-video-facade';
353 +b.setAttribute('data-xspeed-video',u);
354 +var t=f.getAttribute('title');
355 +b.setAttribute('aria-label',t?L+': '+t:L);
356 +var st='position:relative;display:block;width:100%;padding:0;border:0;cursor:pointer;background:#000;aspect-ratio:16/9;';
357 +if(e.p==='youtube'&&!/^(videoseries|live_stream)$/i.test(e.id))
358 +st+='background-image:url(https://i.ytimg.com/vi/'+encodeURIComponent(e.id)+'/hqdefault.jpg);background-size:cover;background-position:center;';
359 +b.setAttribute('style',st);
360 +b.innerHTML='<span class="xspeed-video-facade__play" aria-hidden="true" style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:68px;height:48px;border-radius:14px;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;"><svg width="24" height="24" viewBox="0 0 24 24" fill="#fff" focusable="false"><path d="M8 5v14l11-7z"/></svg></span>';
361 +f.parentNode.replaceChild(b,f);
362 +}
363 +function sweep(root){
364 +if(!root||!root.querySelectorAll)return;
365 +if(root.tagName==='IFRAME'&&root.getAttribute('data-xspeed-held')){dress(root);return;}
366 +var h=root.querySelectorAll('iframe[data-xspeed-held]');
367 +for(var i=0;i<h.length;i++)dress(h[i]);
368 +// An embed written via innerHTML never passed through the wrapped
369 +// setters — its src is live, but the parser has only just created it,
370 +// so replacing it here still cancels the load before the player runs.
371 +if(root.tagName==='IFRAME'&&!root.getAttribute('data-xspeed-loaded')&&!root.hasAttribute('data-skip-lazy')&&parse(root.getAttribute('src'))){
372 +root.setAttribute('data-xspeed-held',root.getAttribute('src'));dress(root);return;
373 +}
374 +var f=root.querySelectorAll('iframe[src]:not([data-xspeed-loaded]):not([data-skip-lazy])');
375 +for(var j=0;j<f.length;j++){
376 +if(parse(f[j].getAttribute('src'))){f[j].setAttribute('data-xspeed-held',f[j].getAttribute('src'));dress(f[j]);}
377 +}
378 +}
379 +try{
380 +new MutationObserver(function(ms){
381 +for(var i=0;i<ms.length;i++)for(var j=0;j<ms[i].addedNodes.length;j++)sweep(ms[i].addedNodes[j]);
382 +}).observe(document.documentElement,{childList:true,subtree:true});
383 +}catch(e){}
384 +if(document.readyState!=='loading')sweep(document);
385 +else document.addEventListener('DOMContentLoaded',function(){sweep(document);});
386 +})();
216 387 JS;
217 388 }
218 389 }