| @@ -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 | } |