Skip to content

Commit 8faa75a

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

8 files changed

Lines changed: 485 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: 106 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
/**
@@ -801,6 +803,7 @@ public function finalize( $content ) {
801803
* Run lazyload replacement for images in buffer.
802804
*
803805
* @since 1.4
806+
* @since 7.9 Added Video LazyLoad image facade support.
804807
* @access private
805808
* @return void
806809
*/
@@ -831,6 +834,11 @@ private function _finalize() {
831834

832835
$cfg_lazy = ( defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_LAZY ) ) && ! $this->cls( 'Metabox' )->setting( 'litespeed_no_image_lazy' );
833836
$cfg_iframe_lazy = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_IFRAME_LAZY );
837+
$cfg_video_img_set = $cfg_iframe_lazy && $this->conf( Base::O_MEDIA_IFRAME_LAZY_VIDEO_IMG );
838+
$cfg_video_img = apply_filters( 'litespeed_media_iframe_lazy_video_img', $cfg_video_img_set );
839+
if ( (bool) $cfg_video_img !== (bool) $cfg_video_img_set ) {
840+
Video::debug( 'Video image facade setting overridden by filter.' );
841+
}
834842
$cfg_js_delay = defined( 'LITESPEED_GUEST_OPTM' ) || 2 === $this->conf( Base::O_OPTM_JS_DEFER );
835843
$cfg_trim_noscript = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_OPTM_NOSCRIPT_RM );
836844
$cfg_vpi = defined( 'LITESPEED_GUEST_OPTM' ) || $this->conf( Base::O_MEDIA_VPI );
@@ -866,6 +874,12 @@ private function _finalize() {
866874
$this->content = str_replace( $html_list_ori, $html_list, $this->content );
867875
}
868876

877+
// Replace known video iframes with image facades (must run before generic iframe lazy).
878+
$video_facade_used = false;
879+
if ( $cfg_video_img ) {
880+
$video_facade_used = $this->_replace_video_iframes();
881+
}
882+
869883
// iframe lazy load.
870884
if ( $cfg_iframe_lazy ) {
871885
$html_list = $this->_parse_iframe();
@@ -890,6 +904,10 @@ private function _finalize() {
890904
// Include lazyload lib js and init lazyload.
891905
if ( $cfg_lazy || $cfg_iframe_lazy ) {
892906
$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>';
907+
if ( $video_facade_used ) {
908+
$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>';
909+
$lazy_lib .= '<script data-no-optimize="1">' . File::read( LSCWP_DIR . self::LIB_FILE_VIDEO_FACADE ) . '</script>';
910+
}
893911
if ( $cfg_js_delay ) {
894912
// Load JS delay lib.
895913
if ( ! defined( 'LITESPEED_JS_DELAY_LIB_LOADED' ) ) {
@@ -1167,6 +1185,94 @@ private function _detect_dimensions( $src ) {
11671185
return false;
11681186
}
11691187

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

0 commit comments

Comments
 (0)