Skip to content

Commit bb44cba

Browse files
Lazy Load Iframes video show image
Add functionality - Video Lazyload Add filter for play button
1 parent 8436765 commit bb44cba

8 files changed

Lines changed: 484 additions & 0 deletions

File tree

assets/js/video_facade.js

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
(function () {
2+
"use strict";
3+
4+
var SEL = ".litespeed-video-facade";
5+
6+
function decode(f) {
7+
try {
8+
return JSON.parse(atob(f.dataset.lsvfAttrs || "")) || {};
9+
} catch (e) {
10+
return {};
11+
}
12+
}
13+
14+
function swap(f) {
15+
if (f.dataset.lsvfSwapped) return;
16+
f.dataset.lsvfSwapped = "1";
17+
18+
var a = decode(f),
19+
i = document.createElement("iframe");
20+
21+
Object.keys(a).forEach(function (k) {
22+
if (k === "src") return;
23+
i.setAttribute(k, a[k]);
24+
});
25+
26+
i.src = f.dataset.lsvfSrc;
27+
28+
if (!i.getAttribute("allow"))
29+
i.setAttribute("allow", "autoplay; encrypted-media; fullscreen; picture-in-picture");
30+
31+
if (!i.hasAttribute("allowfullscreen"))
32+
i.setAttribute("allowfullscreen", "");
33+
34+
f.parentNode.replaceChild(i, f);
35+
}
36+
37+
// Vertical space (px) a pseudo-element reserves, or 0 if it isn't rendered.
38+
// WP core block embeds reserve the ratio via `::before{content:"";padding-top:56.25%}`.
39+
function pseudoReserved(p, pe) {
40+
var s = getComputedStyle(p, pe);
41+
if (!s || s.content === "none") return 0; // pseudo not generated -> reserves nothing
42+
return (parseFloat(s.paddingTop) || 0) + (parseFloat(s.paddingBottom) || 0) + (parseFloat(s.height) || 0);
43+
}
44+
45+
// Many themes (incl. WP core / block themes like Twenty Twenty-Six) wrap a
46+
// video in a responsive container that reserves the aspect ratio with a
47+
// percentage spacer and positions the *iframe* absolutely to fill it. Our
48+
// facade replaces the iframe with a <div>, which the theme's `iframe` rule
49+
// no longer matches, so the facade falls into normal flow and stacks below
50+
// the reserved space -- leaving empty padding above/under the video. When we
51+
// detect such a parent, mirror what the theme does to its iframe: absolutely
52+
// fill the reserved box and drop our own intrinsic (aspect-ratio) sizing.
53+
function fitToWrapper(f) {
54+
var p = f.parentNode;
55+
if (!p || p.nodeType !== 1 || typeof getComputedStyle !== "function") return;
56+
57+
var cs = getComputedStyle(p);
58+
if (cs.position === "static") return; // responsive ratio wrappers are positioned
59+
60+
// Any video ratio reserves >= ~42% of the wrapper width (21:9); cosmetic
61+
// padding is far smaller. Gate on that so we never collapse a normal parent.
62+
var w = p.clientWidth || 0;
63+
var threshold = Math.max(40, w * 0.3);
64+
65+
var reserved = Math.max(
66+
(parseFloat(cs.paddingTop) || 0) + (parseFloat(cs.paddingBottom) || 0),
67+
pseudoReserved(p, "::before"),
68+
pseudoReserved(p, "::after")
69+
);
70+
if (reserved < threshold) return;
71+
72+
f.style.position = "absolute";
73+
f.style.top = "0";
74+
f.style.left = "0";
75+
f.style.right = "0";
76+
f.style.bottom = "0";
77+
f.style.width = "100%";
78+
f.style.height = "100%";
79+
f.style.maxWidth = "none";
80+
f.style.setProperty("aspect-ratio", "auto");
81+
}
82+
83+
function init() {
84+
var fs = document.querySelectorAll(SEL);
85+
if (!fs.length) return;
86+
87+
Array.prototype.forEach.call(fs, function (f) {
88+
fitToWrapper(f);
89+
f.addEventListener("click", function (e) {
90+
e.preventDefault();
91+
swap(f);
92+
});
93+
});
94+
}
95+
96+
if (document.readyState === "loading") {
97+
document.addEventListener("DOMContentLoaded", init);
98+
} else {
99+
init();
100+
}
101+
})();

assets/js/video_facade.min.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

autoload.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
'src/ucss.cls.php',
7777
'src/utility.cls.php',
7878
'src/vary.cls.php',
79+
'src/video.cls.php',
7980
'src/vpi.cls.php',
8081

8182
// Extra CDN cls files

src/base.cls.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ class Base extends Root {
211211
const O_MEDIA_LQIP_MIN_H = 'media-lqip_min_h';
212212
const O_MEDIA_PLACEHOLDER_RESP_ASYNC = 'media-placeholder_resp_async';
213213
const O_MEDIA_IFRAME_LAZY = 'media-iframe_lazy';
214+
const O_MEDIA_IFRAME_LAZY_VIDEO_IMG = 'media-iframe_lazy_video_img';
214215
const O_MEDIA_ADD_MISSING_SIZES = 'media-add_missing_sizes';
215216
const O_MEDIA_LAZY_EXC = 'media-lazy_exc';
216217
const O_MEDIA_LAZY_CLS_EXC = 'media-lazy_cls_exc';
@@ -511,6 +512,7 @@ class Base extends Root {
511512
self::O_MEDIA_LQIP_MIN_H => 0,
512513
self::O_MEDIA_PLACEHOLDER_RESP_ASYNC => false,
513514
self::O_MEDIA_IFRAME_LAZY => false,
515+
self::O_MEDIA_IFRAME_LAZY_VIDEO_IMG => false,
514516
self::O_MEDIA_ADD_MISSING_SIZES => false,
515517
self::O_MEDIA_LAZY_EXC => [],
516518
self::O_MEDIA_LAZY_CLS_EXC => [],

src/lang.cls.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ public static function title( $id ) {
215215
self::O_MEDIA_LQIP_MIN_W => __( 'LQIP Minimum Dimensions', 'litespeed-cache' ),
216216
self::O_MEDIA_PLACEHOLDER_RESP_ASYNC => __( 'Generate LQIP In Background', 'litespeed-cache' ),
217217
self::O_MEDIA_IFRAME_LAZY => __( 'Lazy Load Iframes', 'litespeed-cache' ),
218+
self::O_MEDIA_IFRAME_LAZY_VIDEO_IMG => __( 'Load Video Image', 'litespeed-cache' ),
218219
self::O_MEDIA_ADD_MISSING_SIZES => __( 'Add Missing Sizes', 'litespeed-cache' ),
219220
self::O_MEDIA_VPI => __( 'Viewport Images', 'litespeed-cache' ),
220221
self::O_MEDIA_VPI_CRON => __( 'Viewport Images Cron', 'litespeed-cache' ),

src/media.cls.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class Media extends Root {
2121

2222
const LIB_FILE_IMG_LAZYLOAD = 'assets/js/lazyload.min.js';
2323

24+
const LIB_FILE_VIDEO_FACADE = 'assets/js/video_facade.min.js';
25+
2426
const TYPE_BATCH_RESCALE_ORI = 'batch_rescale_ori';
2527

2628
/**
@@ -831,6 +833,11 @@ private function _finalize() {
831833

832834
$cfg_lazy = ( defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_LAZY ) ) && ! $this->cls( 'Metabox' )->setting( 'litespeed_no_image_lazy' );
833835
$cfg_iframe_lazy = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_IFRAME_LAZY );
836+
$cfg_video_img_set = $cfg_iframe_lazy && $this->conf( Base::O_MEDIA_IFRAME_LAZY_VIDEO_IMG );
837+
$cfg_video_img = apply_filters( 'litespeed_media_iframe_lazy_video_img', $cfg_video_img_set );
838+
if ( (bool) $cfg_video_img !== (bool) $cfg_video_img_set ) {
839+
Video::debug( 'Video image facade setting overridden by filter.' );
840+
}
834841
$cfg_js_delay = defined( 'LITESPEED_GUEST_OPTM' ) || 2 === $this->conf( Base::O_OPTM_JS_DEFER );
835842
$cfg_trim_noscript = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_OPTM_NOSCRIPT_RM );
836843
$cfg_vpi = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_VPI );
@@ -866,6 +873,12 @@ private function _finalize() {
866873
$this->content = str_replace( $html_list_ori, $html_list, $this->content );
867874
}
868875

876+
// Replace known video iframes with image facades (must run before generic iframe lazy).
877+
$video_facade_used = false;
878+
if ( $cfg_video_img ) {
879+
$video_facade_used = $this->_replace_video_iframes();
880+
}
881+
869882
// iframe lazy load.
870883
if ( $cfg_iframe_lazy ) {
871884
$html_list = $this->_parse_iframe();
@@ -890,6 +903,10 @@ private function _finalize() {
890903
// Include lazyload lib js and init lazyload.
891904
if ( $cfg_lazy || $cfg_iframe_lazy ) {
892905
$lazy_lib = '<script data-no-optimize="1">window.lazyLoadOptions=Object.assign({},{threshold:' . apply_filters( 'litespeed_lazyload_threshold', 300 ) . '},window.lazyLoadOptions||{});' . File::read( LSCWP_DIR . self::LIB_FILE_IMG_LAZYLOAD ) . '</script>';
906+
if ( $video_facade_used ) {
907+
$lazy_lib .= '<style>.litespeed-video-facade{position:relative;background-color:#000;background-size:cover;background-position:center;cursor:pointer;width:100%}.litespeed-video-facade .litespeed-video-play{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:68px;height:48px;border:0;background:rgba(0,0,0,.6);border-radius:14%;cursor:pointer;padding:0}.litespeed-video-facade .litespeed-video-play::before{content:"";position:absolute;top:50%;left:55%;transform:translate(-50%,-50%);border-style:solid;border-width:12px 0 12px 20px;border-color:transparent transparent transparent #fff}.litespeed-video-facade:hover .litespeed-video-play{background:#f00}</style>';
908+
$lazy_lib .= '<script data-no-optimize="1">' . File::read( LSCWP_DIR . self::LIB_FILE_VIDEO_FACADE ) . '</script>';
909+
}
893910
if ( $cfg_js_delay ) {
894911
// Load JS delay lib.
895912
if ( ! defined( 'LITESPEED_JS_DELAY_LIB_LOADED' ) ) {
@@ -1167,6 +1184,94 @@ private function _detect_dimensions( $src ) {
11671184
return false;
11681185
}
11691186

1187+
/**
1188+
* Replace known video iframes (YouTube/Vimeo/Wistia/Dailymotion) with click-to-play
1189+
* image facades. The original <iframe> is removed entirely; a <div> placeholder
1190+
* carries the iframe's attributes (base64-encoded) so the JS can rebuild the
1191+
* iframe on click. Runs BEFORE the generic iframe lazy pass so the resulting <div>
1192+
* facades are invisible to its <iframe> regex.
1193+
*
1194+
* @since 7.10
1195+
* @return bool True if at least one iframe was replaced.
1196+
*/
1197+
private function _replace_video_iframes() {
1198+
$cls_excludes = apply_filters( 'litespeed_media_iframe_lazy_cls_excludes', $this->conf( Base::O_MEDIA_IFRAME_LAZY_CLS_EXC ) );
1199+
$cls_excludes[] = 'skip-lazy';
1200+
$parent_cls_excludes = apply_filters( 'litespeed_media_iframe_lazy_parent_cls_excludes', $this->conf( Base::O_MEDIA_IFRAME_LAZY_PARENT_CLS_EXC ) );
1201+
1202+
$content = preg_replace( '#<!--.*-->#sU', '', $this->content );
1203+
if ( $parent_cls_excludes ) {
1204+
foreach ( $parent_cls_excludes as $v ) {
1205+
$content = preg_replace( '#<(\w+) [^>]*class=(\'|")[^\'"]*' . preg_quote( $v, '#' ) . '[^\'"]*\2[^>]*>.*</\1>#sU', '', $content );
1206+
}
1207+
}
1208+
1209+
if ( ! preg_match_all( '#<iframe \s*([^>]+)></iframe>#isU', $content, $matches, PREG_SET_ORDER ) ) {
1210+
return false;
1211+
}
1212+
1213+
$search = array();
1214+
$replace = array();
1215+
$replaced = false;
1216+
1217+
foreach ( $matches as $match ) {
1218+
$attrs = Utility::parse_attr( $match[1] );
1219+
if ( empty( $attrs['src'] ) ) {
1220+
continue;
1221+
}
1222+
if ( ! empty( $attrs['data-no-lazy'] ) || ! empty( $attrs['data-skip-lazy'] ) || ! empty( $attrs['data-lazyloaded'] ) || ! empty( $attrs['data-src'] ) ) {
1223+
continue;
1224+
}
1225+
if ( ! empty( $attrs['class'] ) && Utility::str_hit_array( $attrs['class'], $cls_excludes ) ) {
1226+
continue;
1227+
}
1228+
1229+
// Decode HTML-entity ampersands so `&amp;` / `&#38;` / raw `&` all extract the same way.
1230+
$src_decoded = html_entity_decode( $attrs['src'], ENT_QUOTES | ENT_HTML5, 'UTF-8' );
1231+
$attrs['src'] = $src_decoded;
1232+
1233+
// Skip iframes already configured to autoplay — replacing them would require an
1234+
// extra click and break the author's intent. Matches autoplay=1 / autoplay=true
1235+
// (case-insensitive) after either `?` or `&`.
1236+
if ( preg_match( '#[?&]autoplay=(?:1|true)\b#i', $src_decoded ) ) {
1237+
continue;
1238+
}
1239+
1240+
// Substring-match URL skip via filter (e.g. add_filter('litespeed_video_lazy_skip_urls', fn($u)=>array_merge($u,['?keep_native=1'])))
1241+
if ( Video::url_skipped( $src_decoded ) ) {
1242+
continue;
1243+
}
1244+
// Per-call skip filter for code that needs richer logic.
1245+
if ( apply_filters( 'litespeed_video_lazy_skip', false, $src_decoded ) ) {
1246+
continue;
1247+
}
1248+
1249+
$matched = Video::extract_provider( $src_decoded );
1250+
if ( ! $matched ) {
1251+
continue; // Not a known video provider; let generic iframe lazy handle it.
1252+
}
1253+
1254+
// Avoid double-processing identical iframe markup.
1255+
if ( in_array( $match[0], $search, true ) ) {
1256+
continue;
1257+
}
1258+
1259+
$load_src = Video::ensure_autoplay( $matched['provider'], $src_decoded );
1260+
$thumb_url = Video::get_thumbnail( $matched['provider'], $matched['id'], $src_decoded );
1261+
$facade_html = Video::build_facade( $load_src, $attrs, $thumb_url );
1262+
1263+
$search[] = $match[0];
1264+
$replace[] = $facade_html;
1265+
$replaced = true;
1266+
}
1267+
1268+
if ( $search ) {
1269+
$this->content = str_replace( $search, $replace, $this->content );
1270+
}
1271+
1272+
return $replaced;
1273+
}
1274+
11701275
/**
11711276
* Parse iframe src.
11721277
*

0 commit comments

Comments
 (0)