@@ -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 `&` / `&` / 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