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 +115 -0 1.3.0 → 1.3.6 View file →
@@ -229,8 +229,14 @@
229 229 * standalone layout.
230 230 */
231 231 public static function facade_style(): string {
232 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;'
233 239 . 'width:100%!important;height:100%!important;aspect-ratio:auto!important}';
234 240 }
235 241
236 242 /**
@@ -260,8 +266,12 @@
260 266 return;
261 267 }
262 268 var u=b.getAttribute('data-xspeed-video');if(!u)return;
263 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');
264 274 f.setAttribute('src',u);
265 275 f.setAttribute('frameborder','0');
266 276 f.setAttribute('allow','accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture');
267 277 f.setAttribute('allowfullscreen','');
@@ -268,7 +278,112 @@
268 278 f.setAttribute('style','width:100%;aspect-ratio:16/9;border:0;');
269 279 var t=b.getAttribute('aria-label');if(t)f.setAttribute('title',t);
270 280 b.parentNode.replaceChild(f,b);
271 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 +})();
272 387 JS;
273 388 }
274 389 }