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