, youtube-nocookie, and youtu.be/. if ( preg_match( '#^https?://(?:www\.)?(?:youtube(?:-nocookie)?\.com/embed/|youtu\.be/)([A-Za-z0-9_-]{6,20})#i', (string) $probe, $m ) ) { return array( 'provider' => 'youtube', 'id' => $m[1], ); } // Vimeo: player.vimeo.com/video/. if ( preg_match( '#^https?://player\.vimeo\.com/video/(\d{6,12})#i', (string) $probe, $m ) ) { return array( 'provider' => 'vimeo', 'id' => $m[1], ); } return null; } /** * Poster URL for an embed, or '' when we can't derive one without an * extra API round-trip. * * YouTube exposes deterministic thumbnail URLs, so a poster costs one * image request — far less than the player it replaces. Vimeo requires * an oEmbed lookup per video, which would mean a server-side HTTP call * during page render; we decline and render a neutral facade instead. */ public static function poster_url( string $provider, string $id ): string { if ( 'youtube' === $provider ) { // `/embed/videoseries?list=…` and `/embed/live_stream?channel=…` // put a keyword where a video id normally goes. Both are // id-shaped enough to pass parse_embed, but neither names a // video, so the thumbnail URL built from them 404s — a broken // request on every page view. The facade still renders (a // playlist loads the same heavy player a single video does); // it just renders without a poster. if ( in_array( strtolower( $id ), array( 'videoseries', 'live_stream' ), true ) ) { return ''; } // hqdefault exists for every video; maxres does not. return 'https://i.ytimg.com/vi/' . rawurlencode( $id ) . '/hqdefault.jpg'; } return ''; } /** * The real player URL to swap in on click — autoplay appended so the * click that revealed the player also starts it (one click, not two). */ public static function player_url( string $src ): string { $src = html_entity_decode( $src, ENT_QUOTES ); if ( false !== strpos( $src, 'autoplay=' ) ) { return $src; } return $src . ( false === strpos( $src, '?' ) ? '?' : '&' ) . 'autoplay=1'; } /** * Build the facade markup for one parsed embed. * * Contract: * - a '; $markup .= ''; return $markup; } /** * The one CSS rule the facade can't express as an inline style. * * Gutenberg wraps an embed in * `figure.wp-has-aspect-ratio > div.wp-block-embed__wrapper`, gives the * wrapper a `::before` with `padding-top:56.25%` to reserve the 16:9 * box, and then absolutely positions the iframe on top of it. Our * facade is a `