PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.5
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.5
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 1.1.6 All 31 releases
← All changes | includes/class-video-facade.php +109 -0 1.3.01.3.5 View file →
@@ -260,8 +260,12 @@
260 260 return;
261 261 }
262 262 var u=b.getAttribute('data-xspeed-video');if(!u)return;
263 263 var f=document.createElement('iframe');
264 +// Released BEFORE src: the observer script's interceptor holds any
265 +// provider src it sees, and the player the visitor just asked for is
266 +// the one iframe that must load immediately.
267 +f.setAttribute('data-xspeed-loaded','1');
264 268 f.setAttribute('src',u);
265 269 f.setAttribute('frameborder','0');
266 270 f.setAttribute('allow','accelerometer;autoplay;clipboard-write;encrypted-media;gyroscope;picture-in-picture');
267 271 f.setAttribute('allowfullscreen','');
@@ -268,7 +272,112 @@
268 272 f.setAttribute('style','width:100%;aspect-ratio:16/9;border:0;');
269 273 var t=b.getAttribute('aria-label');if(t)f.setAttribute('title',t);
270 274 b.parentNode.replaceChild(f,b);
271 275 },false);
276 +JS;
277 + }
278 +
279 + /**
280 + * The early interceptor for embeds that never appear in the HTML.
281 + *
282 + * A page-builder video widget builds its YouTube/Vimeo <iframe> from
283 + * its own script, so the server pass — which rewrites the response
284 + * buffer — never sees an element to replace, and the full player
285 + * (3MB+ of JS per embed) downloads on page load anyway. Measured live:
286 + * one homepage carried three JS-built embeds and 22MB of YouTube
287 + * player resources with the facade "on".
288 + *
289 + * Same playbook as Lazy_Loader::autoplay_script(), for the same
290 + * reason: builders set `src` BEFORE inserting the element, so a
291 + * MutationObserver alone is always too late — the fetch starts
292 + * off-DOM. So the property setter and setAttribute are wrapped, a
293 + * recognised provider src is parked in data-xspeed-held instead of
294 + * applied, and the observer only has to dress the inert element as a
295 + * facade once it lands in the DOM. The click handler above does the
296 + * swap; its data-xspeed-loaded release mark is honoured here so the
297 + * player it builds is never re-held.
298 + *
299 + * The provider patterns mirror parse_embed()/poster_url() and must
300 + * stay in step with them — one facade, two capture paths.
301 + */
302 + public static function observer_script(): string {
303 + $label = wp_json_encode( __( 'Play video', 'xspeed' ) );
304 +
305 + return <<<JS
306 +(function(){
307 +var L={$label};
308 +function parse(u){
309 +if(!u)return null;
310 +u=String(u).replace(/^\/\//,'https://');
311 +var m=u.match(/^https?:\/\/(?:www\.)?(?:youtube(?:-nocookie)?\.com\/embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,20})/i);
312 +if(m)return{p:'youtube',id:m[1]};
313 +m=u.match(/^https?:\/\/player\.vimeo\.com\/video\/(\d{6,12})/i);
314 +if(m)return{p:'vimeo',id:m[1]};
315 +return null;
316 +}
317 +function hold(el,val){
318 +if(!el||el.tagName!=='IFRAME')return false;
319 +if(el.getAttribute('data-xspeed-loaded')||el.hasAttribute('data-skip-lazy'))return false;
320 +if(!parse(val))return false;
321 +el.setAttribute('data-xspeed-held',String(val));
322 +return true;
323 +}
324 +try{
325 +var IP=window.HTMLIFrameElement&&HTMLIFrameElement.prototype;
326 +var SD=IP&&Object.getOwnPropertyDescriptor(IP,'src');
327 +if(SD&&SD.set){
328 +Object.defineProperty(IP,'src',{configurable:true,enumerable:SD.enumerable,
329 +get:function(){return SD.get.call(this);},
330 +set:function(v){if(hold(this,v))return;return SD.set.call(this,v);}});
331 +}
332 +var SA=Element.prototype.setAttribute;
333 +Element.prototype.setAttribute=function(n,v){
334 +if(n==='src'&&hold(this,v))return;
335 +return SA.call(this,n,v);
336 +};
337 +}catch(e){}
338 +function dress(f){
339 +if(!f.parentNode)return;
340 +var s=f.getAttribute('data-xspeed-held');
341 +var e=parse(s);
342 +if(!e)return;
343 +var u=s+(s.indexOf('autoplay=')>-1?'':(s.indexOf('?')>-1?'&':'?')+'autoplay=1');
344 +var b=document.createElement('button');
345 +b.type='button';
346 +b.className='xspeed-video-facade';
347 +b.setAttribute('data-xspeed-video',u);
348 +var t=f.getAttribute('title');
349 +b.setAttribute('aria-label',t?L+': '+t:L);
350 +var st='position:relative;display:block;width:100%;padding:0;border:0;cursor:pointer;background:#000;aspect-ratio:16/9;';
351 +if(e.p==='youtube'&&!/^(videoseries|live_stream)$/i.test(e.id))
352 +st+='background-image:url(https://i.ytimg.com/vi/'+encodeURIComponent(e.id)+'/hqdefault.jpg);background-size:cover;background-position:center;';
353 +b.setAttribute('style',st);
354 +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>';
355 +f.parentNode.replaceChild(b,f);
356 +}
357 +function sweep(root){
358 +if(!root||!root.querySelectorAll)return;
359 +if(root.tagName==='IFRAME'&&root.getAttribute('data-xspeed-held')){dress(root);return;}
360 +var h=root.querySelectorAll('iframe[data-xspeed-held]');
361 +for(var i=0;i<h.length;i++)dress(h[i]);
362 +// An embed written via innerHTML never passed through the wrapped
363 +// setters — its src is live, but the parser has only just created it,
364 +// so replacing it here still cancels the load before the player runs.
365 +if(root.tagName==='IFRAME'&&!root.getAttribute('data-xspeed-loaded')&&!root.hasAttribute('data-skip-lazy')&&parse(root.getAttribute('src'))){
366 +root.setAttribute('data-xspeed-held',root.getAttribute('src'));dress(root);return;
367 +}
368 +var f=root.querySelectorAll('iframe[src]:not([data-xspeed-loaded]):not([data-skip-lazy])');
369 +for(var j=0;j<f.length;j++){
370 +if(parse(f[j].getAttribute('src'))){f[j].setAttribute('data-xspeed-held',f[j].getAttribute('src'));dress(f[j]);}
371 +}
372 +}
373 +try{
374 +new MutationObserver(function(ms){
375 +for(var i=0;i<ms.length;i++)for(var j=0;j<ms[i].addedNodes.length;j++)sweep(ms[i].addedNodes[j]);
376 +}).observe(document.documentElement,{childList:true,subtree:true});
377 +}catch(e){}
378 +if(document.readyState!=='loading')sweep(document);
379 +else document.addEventListener('DOMContentLoaded',function(){sweep(document);});
380 +})();
272 381 JS;
273 382 }
274 383 }