diff --git a/assets/js/video_facade.js b/assets/js/video_facade.js
new file mode 100644
index 000000000..ae8d28172
--- /dev/null
+++ b/assets/js/video_facade.js
@@ -0,0 +1,101 @@
+(function () {
+ "use strict";
+
+ var SEL = ".litespeed-video-facade";
+
+ function decode(f) {
+ try {
+ return JSON.parse(atob(f.dataset.lsvfAttrs || "")) || {};
+ } catch (e) {
+ return {};
+ }
+ }
+
+ function swap(f) {
+ if (f.dataset.lsvfSwapped) return;
+ f.dataset.lsvfSwapped = "1";
+
+ var a = decode(f),
+ i = document.createElement("iframe");
+
+ Object.keys(a).forEach(function (k) {
+ if (k === "src") return;
+ i.setAttribute(k, a[k]);
+ });
+
+ i.src = f.dataset.lsvfSrc;
+
+ if (!i.getAttribute("allow"))
+ i.setAttribute("allow", "autoplay; encrypted-media; fullscreen; picture-in-picture");
+
+ if (!i.hasAttribute("allowfullscreen"))
+ i.setAttribute("allowfullscreen", "");
+
+ f.parentNode.replaceChild(i, f);
+ }
+
+ // Vertical space (px) a pseudo-element reserves, or 0 if it isn't rendered.
+ // WP core block embeds reserve the ratio via `::before{content:"";padding-top:56.25%}`.
+ function pseudoReserved(p, pe) {
+ var s = getComputedStyle(p, pe);
+ if (!s || s.content === "none") return 0; // pseudo not generated -> reserves nothing
+ return (parseFloat(s.paddingTop) || 0) + (parseFloat(s.paddingBottom) || 0) + (parseFloat(s.height) || 0);
+ }
+
+ // Many themes (incl. WP core / block themes like Twenty Twenty-Six) wrap a
+ // video in a responsive container that reserves the aspect ratio with a
+ // percentage spacer and positions the *iframe* absolutely to fill it. Our
+ // facade replaces the iframe with a
, which the theme's `iframe` rule
+ // no longer matches, so the facade falls into normal flow and stacks below
+ // the reserved space -- leaving empty padding above/under the video. When we
+ // detect such a parent, mirror what the theme does to its iframe: absolutely
+ // fill the reserved box and drop our own intrinsic (aspect-ratio) sizing.
+ function fitToWrapper(f) {
+ var p = f.parentNode;
+ if (!p || p.nodeType !== 1 || typeof getComputedStyle !== "function") return;
+
+ var cs = getComputedStyle(p);
+ if (cs.position === "static") return; // responsive ratio wrappers are positioned
+
+ // Any video ratio reserves >= ~42% of the wrapper width (21:9); cosmetic
+ // padding is far smaller. Gate on that so we never collapse a normal parent.
+ var w = p.clientWidth || 0;
+ var threshold = Math.max(40, w * 0.3);
+
+ var reserved = Math.max(
+ (parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0),
+ pseudoReserved(p, "::before"),
+ pseudoReserved(p, "::after")
+ );
+ if (reserved < threshold) return;
+
+ f.style.position = "absolute";
+ f.style.top = "0";
+ f.style.left = "0";
+ f.style.right = "0";
+ f.style.bottom = "0";
+ f.style.width = "100%";
+ f.style.height = "100%";
+ f.style.maxWidth = "none";
+ f.style.setProperty("aspect-ratio", "auto");
+ }
+
+ function init() {
+ var fs = document.querySelectorAll(SEL);
+ if (!fs.length) return;
+
+ Array.prototype.forEach.call(fs, function (f) {
+ fitToWrapper(f);
+ f.addEventListener("click", function (e) {
+ e.preventDefault();
+ swap(f);
+ });
+ });
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init);
+ } else {
+ init();
+ }
+})();
diff --git a/assets/js/video_facade.min.js b/assets/js/video_facade.min.js
new file mode 100644
index 000000000..0037887a2
--- /dev/null
+++ b/assets/js/video_facade.min.js
@@ -0,0 +1 @@
+(function(){"use strict";var SEL=".litespeed-video-facade";function decode(f){try{return JSON.parse(atob(f.dataset.lsvfAttrs||""))||{}}catch(e){return{}}}function swap(f){if(f.dataset.lsvfSwapped)return;f.dataset.lsvfSwapped="1";var a=decode(f),i=document.createElement("iframe");Object.keys(a).forEach(function(k){if(k==="src")return;i.setAttribute(k,a[k])});i.src=f.dataset.lsvfSrc;if(!i.getAttribute("allow"))i.setAttribute("allow","autoplay; encrypted-media; fullscreen; picture-in-picture");if(!i.hasAttribute("allowfullscreen"))i.setAttribute("allowfullscreen","");f.parentNode.replaceChild(i,f)}function pseudoReserved(p,pe){var s=getComputedStyle(p,pe);if(!s||s.content==="none")return 0;return(parseFloat(s.paddingTop)||0)+(parseFloat(s.paddingBottom)||0)+(parseFloat(s.height)||0)}function fitToWrapper(f){var p=f.parentNode;if(!p||p.nodeType!==1||typeof getComputedStyle!=="function")return;var cs=getComputedStyle(p);if(cs.position==="static")return;var w=p.clientWidth||0,threshold=Math.max(40,w*0.3);var reserved=Math.max((parseFloat(cs.paddingTop)||0)+(parseFloat(cs.paddingBottom)||0),pseudoReserved(p,"::before"),pseudoReserved(p,"::after"));if(reserved
0,
self::O_MEDIA_PLACEHOLDER_RESP_ASYNC => false,
self::O_MEDIA_IFRAME_LAZY => false,
+ self::O_MEDIA_IFRAME_LAZY_VIDEO_IMG => false,
self::O_MEDIA_ADD_MISSING_SIZES => false,
self::O_MEDIA_LAZY_EXC => [],
self::O_MEDIA_LAZY_CLS_EXC => [],
diff --git a/src/lang.cls.php b/src/lang.cls.php
index 7ff654c99..0c70bcda3 100644
--- a/src/lang.cls.php
+++ b/src/lang.cls.php
@@ -215,6 +215,7 @@ public static function title( $id ) {
self::O_MEDIA_LQIP_MIN_W => __( 'LQIP Minimum Dimensions', 'litespeed-cache' ),
self::O_MEDIA_PLACEHOLDER_RESP_ASYNC => __( 'Generate LQIP In Background', 'litespeed-cache' ),
self::O_MEDIA_IFRAME_LAZY => __( 'Lazy Load Iframes', 'litespeed-cache' ),
+ self::O_MEDIA_IFRAME_LAZY_VIDEO_IMG => __( 'Load Video Image', 'litespeed-cache' ),
self::O_MEDIA_ADD_MISSING_SIZES => __( 'Add Missing Sizes', 'litespeed-cache' ),
self::O_MEDIA_VPI => __( 'Viewport Images', 'litespeed-cache' ),
self::O_MEDIA_VPI_CRON => __( 'Viewport Images Cron', 'litespeed-cache' ),
diff --git a/src/media.cls.php b/src/media.cls.php
index 93a4f13e5..2fe0ef608 100644
--- a/src/media.cls.php
+++ b/src/media.cls.php
@@ -21,6 +21,8 @@ class Media extends Root {
const LIB_FILE_IMG_LAZYLOAD = 'assets/js/lazyload.min.js';
+ const LIB_FILE_VIDEO_FACADE = 'assets/js/video_facade.min.js';
+
const TYPE_BATCH_RESCALE_ORI = 'batch_rescale_ori';
/**
@@ -801,6 +803,7 @@ public function finalize( $content ) {
* Run lazyload replacement for images in buffer.
*
* @since 1.4
+ * @since 7.9 Added Video LazyLoad image facade support.
* @access private
* @return void
*/
@@ -831,6 +834,11 @@ private function _finalize() {
$cfg_lazy = ( defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_LAZY ) ) && ! $this->cls( 'Metabox' )->setting( 'litespeed_no_image_lazy' );
$cfg_iframe_lazy = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_IFRAME_LAZY );
+ $cfg_video_img_set = $cfg_iframe_lazy && $this->conf( Base::O_MEDIA_IFRAME_LAZY_VIDEO_IMG );
+ $cfg_video_img = apply_filters( 'litespeed_media_iframe_lazy_video_img', $cfg_video_img_set );
+ if ( (bool) $cfg_video_img !== (bool) $cfg_video_img_set ) {
+ Video::debug( 'Video image facade setting overridden by filter.' );
+ }
$cfg_js_delay = defined( 'LITESPEED_GUEST_OPTM' ) || 2 === $this->conf( Base::O_OPTM_JS_DEFER );
$cfg_trim_noscript = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_OPTM_NOSCRIPT_RM );
$cfg_vpi = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_VPI );
@@ -866,6 +874,12 @@ private function _finalize() {
$this->content = str_replace( $html_list_ori, $html_list, $this->content );
}
+ // Replace known video iframes with image facades (must run before generic iframe lazy).
+ $video_facade_used = false;
+ if ( $cfg_video_img ) {
+ $video_facade_used = $this->_replace_video_iframes();
+ }
+
// iframe lazy load.
if ( $cfg_iframe_lazy ) {
$html_list = $this->_parse_iframe();
@@ -890,6 +904,10 @@ private function _finalize() {
// Include lazyload lib js and init lazyload.
if ( $cfg_lazy || $cfg_iframe_lazy ) {
$lazy_lib = '';
+ if ( $video_facade_used ) {
+ $lazy_lib .= '';
+ $lazy_lib .= '';
+ }
if ( $cfg_js_delay ) {
// Load JS delay lib.
if ( ! defined( 'LITESPEED_JS_DELAY_LIB_LOADED' ) ) {
@@ -1167,6 +1185,94 @@ private function _detect_dimensions( $src ) {
return false;
}
+ /**
+ * Replace known video iframes (YouTube/Vimeo/Wistia/Dailymotion) with click-to-play
+ * image facades. The original