| 1 |
<?php |
| 2 |
/** |
| 3 |
* Inline (non-iframe) VideoPress player rendering. |
| 4 |
* |
| 5 |
* @package automattic/jetpack-videopress |
| 6 |
*/ |
| 7 |
|
| 8 |
namespace Automattic\Jetpack\VideoPress; |
| 9 |
|
| 10 |
/** |
| 11 |
* Renders VideoPress players in the page from one shared player script. |
| 12 |
* |
| 13 |
* Every `videopress.com/embed/` iframe downloads and runs its own copy of the |
| 14 |
* player, so a page pays for it once per video. In inline mode the player |
| 15 |
* bundle and stylesheet load once from v0.wordpress.com and a small boot |
| 16 |
* script mounts a player on each placeholder. |
| 17 |
*/ |
| 18 |
class Inline_Player { |
| 19 |
|
| 20 |
const PLAYER_SCRIPT_URL = 'https://v0.wordpress.com/js/videojs/videopress.js'; |
| 21 |
const PLAYER_STYLE_URL = 'https://v0.wordpress.com/js/videojs/videopress.css'; |
| 22 |
|
| 23 |
// The Jetpack plugin's legacy in-page embed used this handle; keep it so existing dequeue hooks still apply. |
| 24 |
const PLAYER_HANDLE = 'videopress-videojs'; |
| 25 |
const BOOT_HANDLE = 'videopress-inline-player'; |
| 26 |
|
| 27 |
const PLACEHOLDER_CLASS = 'jetpack-videopress-player__inline'; |
| 28 |
const FACADE_CLASS = 'jetpack-videopress-player__facade'; |
| 29 |
|
| 30 |
/** |
| 31 |
* How many placeholders this request has rendered; the first poster stays eager, later ones lazy-load. |
| 32 |
* |
| 33 |
* @var int |
| 34 |
*/ |
| 35 |
private static $rendered = 0; |
| 36 |
|
| 37 |
/** |
| 38 |
* Whether the boot script's config has been printed for this request. |
| 39 |
* |
| 40 |
* @var bool |
| 41 |
*/ |
| 42 |
private static $config_printed = false; |
| 43 |
|
| 44 |
/** |
| 45 |
* Whether embeds rendered by this site should mount an inline player instead of an iframe. |
| 46 |
* |
| 47 |
* @return bool |
| 48 |
*/ |
| 49 |
public static function is_enabled() { |
| 50 |
/** |
| 51 |
* Filter whether VideoPress videos are embedded with an iframe. |
| 52 |
* |
| 53 |
* Return false to render the player directly in the page from one shared |
| 54 |
* player script. Defaults to the inverse of the site's inline player setting. |
| 55 |
* |
| 56 |
* @module videopress |
| 57 |
* |
| 58 |
* @since 3.7.0 |
| 59 |
* |
| 60 |
* @param bool $use_iframe Whether to embed with an iframe. |
| 61 |
*/ |
| 62 |
return ! apply_filters( 'jetpack_videopress_player_use_iframe', ! Data::get_videopress_inline_player_enabled() ); |
| 63 |
} |
| 64 |
|
| 65 |
/** |
| 66 |
* Map block or shortcode attributes to player options. |
| 67 |
* |
| 68 |
* Accepts the video block's attribute names (`autoplay`, `preload`, ...); |
| 69 |
* anything missing falls back to the player's defaults. |
| 70 |
* |
| 71 |
* @param array $attributes Attributes to map. |
| 72 |
* @return array Player options, ready to pass to `videopress()`. |
| 73 |
*/ |
| 74 |
public static function get_player_options( array $attributes = array() ) { |
| 75 |
$attributes = wp_parse_args( |
| 76 |
$attributes, |
| 77 |
array( |
| 78 |
'autoplay' => false, |
| 79 |
'controls' => true, |
| 80 |
'loop' => false, |
| 81 |
'muted' => false, |
| 82 |
'playsinline' => false, |
| 83 |
'poster' => '', |
| 84 |
'preload' => 'metadata', |
| 85 |
'seekbarColor' => '', |
| 86 |
'seekbarPlayedColor' => '', |
| 87 |
'seekbarLoadingColor' => '', |
| 88 |
'useAverageColor' => true, |
| 89 |
'cover' => true, |
| 90 |
'hd' => false, |
| 91 |
'at' => 0, |
| 92 |
'defaultLangCode' => '', |
| 93 |
) |
| 94 |
); |
| 95 |
|
| 96 |
$preload = is_string( $attributes['preload'] ) ? strtolower( $attributes['preload'] ) : 'metadata'; |
| 97 |
if ( ! in_array( $preload, array( 'auto', 'metadata', 'none' ), true ) ) { |
| 98 |
$preload = 'metadata'; |
| 99 |
} |
| 100 |
// The site-wide opt-out wins over the embed's own preload attribute. |
| 101 |
if ( Data::get_videopress_player_preload_disabled() ) { |
| 102 |
$preload = 'none'; |
| 103 |
} |
| 104 |
|
| 105 |
$options = array( |
| 106 |
'autoPlay' => self::to_bool( $attributes['autoplay'] ), |
| 107 |
'controls' => self::to_bool( $attributes['controls'] ), |
| 108 |
'loop' => self::to_bool( $attributes['loop'] ), |
| 109 |
'muted' => self::to_bool( $attributes['muted'] ), |
| 110 |
'persistVolume' => ! self::to_bool( $attributes['muted'] ), |
| 111 |
'playsinline' => self::to_bool( $attributes['playsinline'] ), |
| 112 |
'cover' => self::to_bool( $attributes['cover'] ), |
| 113 |
'hd' => self::to_bool( $attributes['hd'] ), |
| 114 |
'useAverageColor' => self::to_bool( $attributes['useAverageColor'] ), |
| 115 |
'preloadContent' => $preload, |
| 116 |
// Embed pages default to the current player skin; match them. |
| 117 |
'chrome' => 'v2', |
| 118 |
); |
| 119 |
|
| 120 |
if ( (int) $attributes['at'] > 0 ) { |
| 121 |
$options['at'] = (int) $attributes['at']; |
| 122 |
} |
| 123 |
|
| 124 |
if ( ! empty( $attributes['poster'] ) && is_string( $attributes['poster'] ) ) { |
| 125 |
$options['poster'] = esc_url_raw( $attributes['poster'] ); |
| 126 |
} |
| 127 |
|
| 128 |
if ( ! empty( $attributes['defaultLangCode'] ) && is_string( $attributes['defaultLangCode'] ) ) { |
| 129 |
$options['defaultLangCode'] = $attributes['defaultLangCode']; |
| 130 |
} |
| 131 |
|
| 132 |
$colors = array( |
| 133 |
'seekbarColor' => 'seekbarColor', |
| 134 |
'seekbarPlayedColor' => 'seekbarPlayedColor', |
| 135 |
'seekbarLoadingColor' => 'seekbarLoadedColor', |
| 136 |
); |
| 137 |
foreach ( $colors as $attribute => $option ) { |
| 138 |
if ( ! empty( $attributes[ $attribute ] ) && is_string( $attributes[ $attribute ] ) ) { |
| 139 |
$options[ $option ] = $attributes[ $attribute ]; |
| 140 |
} |
| 141 |
} |
| 142 |
|
| 143 |
/** |
| 144 |
* Filter the options passed to an inline VideoPress player. |
| 145 |
* |
| 146 |
* @since 0.50.2 |
| 147 |
* |
| 148 |
* @param array $options Player options. |
| 149 |
* @param array $attributes The block or shortcode attributes they were built from. |
| 150 |
*/ |
| 151 |
return apply_filters( 'jetpack_videopress_inline_player_options', $options, $attributes ); |
| 152 |
} |
| 153 |
|
| 154 |
/** |
| 155 |
* Read the player attributes an embed URL carries in its query string. |
| 156 |
* |
| 157 |
* Used to render inline players for videopress.com URLs that reach the |
| 158 |
* page through oEmbed. Only parameters present in the URL are returned. |
| 159 |
* |
| 160 |
* @param string $url A videopress.com/v or /embed URL. |
| 161 |
* @return array Attributes in the shape `get_player_options()` accepts. |
| 162 |
*/ |
| 163 |
public static function get_attributes_from_embed_url( $url ) { |
| 164 |
$query = wp_parse_url( $url, PHP_URL_QUERY ); |
| 165 |
if ( ! is_string( $query ) || '' === $query ) { |
| 166 |
return array(); |
| 167 |
} |
| 168 |
|
| 169 |
$params = array(); |
| 170 |
parse_str( html_entity_decode( $query, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ), $params ); |
| 171 |
|
| 172 |
$booleans = array( |
| 173 |
'autoPlay' => 'autoplay', |
| 174 |
'autoplay' => 'autoplay', |
| 175 |
'controls' => 'controls', |
| 176 |
'loop' => 'loop', |
| 177 |
'muted' => 'muted', |
| 178 |
'playsinline' => 'playsinline', |
| 179 |
'useAverageColor' => 'useAverageColor', |
| 180 |
'cover' => 'cover', |
| 181 |
'hd' => 'hd', |
| 182 |
); |
| 183 |
$strings = array( |
| 184 |
'posterUrl' => 'poster', |
| 185 |
'preloadContent' => 'preload', |
| 186 |
'sbc' => 'seekbarColor', |
| 187 |
'sbpc' => 'seekbarPlayedColor', |
| 188 |
'sblc' => 'seekbarLoadingColor', |
| 189 |
'defaultLangCode' => 'defaultLangCode', |
| 190 |
); |
| 191 |
|
| 192 |
$attributes = array(); |
| 193 |
foreach ( $booleans as $param => $attribute ) { |
| 194 |
if ( isset( $params[ $param ] ) && is_string( $params[ $param ] ) ) { |
| 195 |
$attributes[ $attribute ] = self::to_bool( $params[ $param ] ); |
| 196 |
} |
| 197 |
} |
| 198 |
foreach ( $strings as $param => $attribute ) { |
| 199 |
if ( ! empty( $params[ $param ] ) && is_string( $params[ $param ] ) ) { |
| 200 |
$attributes[ $attribute ] = $params[ $param ]; |
| 201 |
} |
| 202 |
} |
| 203 |
if ( isset( $params['at'] ) && (int) $params['at'] > 0 ) { |
| 204 |
$attributes['at'] = (int) $params['at']; |
| 205 |
} |
| 206 |
|
| 207 |
return $attributes; |
| 208 |
} |
| 209 |
|
| 210 |
/** |
| 211 |
* Versioned URLs of the player bundle and its stylesheet, for anything that loads the player itself. |
| 212 |
* |
| 213 |
* @return array{script: string, style: string} |
| 214 |
*/ |
| 215 |
public static function get_asset_config() { |
| 216 |
return array( |
| 217 |
'script' => add_query_arg( 'ver', Package_Version::PACKAGE_VERSION, self::PLAYER_SCRIPT_URL ), |
| 218 |
'style' => add_query_arg( 'ver', Package_Version::PACKAGE_VERSION, self::PLAYER_STYLE_URL ), |
| 219 |
); |
| 220 |
} |
| 221 |
|
| 222 |
/** |
| 223 |
* Enqueue the boot script and, unless every player on the page sits behind a facade, the shared player assets. |
| 224 |
* |
| 225 |
* The boot script never depends on the player handle: behind a facade it fetches the |
| 226 |
* bundle itself on the first click, from the URLs printed in its config. |
| 227 |
* |
| 228 |
* @param bool $defer_player True to leave the player bundle for the boot script to load on demand. |
| 229 |
*/ |
| 230 |
public static function enqueue_assets( $defer_player = false ) { |
| 231 |
wp_enqueue_script( |
| 232 |
self::BOOT_HANDLE, |
| 233 |
plugins_url( '../build/lib/inline-player.js', __FILE__ ), |
| 234 |
array(), |
| 235 |
Package_Version::PACKAGE_VERSION, |
| 236 |
true |
| 237 |
); |
| 238 |
|
| 239 |
if ( ! self::$config_printed ) { |
| 240 |
self::$config_printed = true; |
| 241 |
wp_add_inline_script( |
| 242 |
self::BOOT_HANDLE, |
| 243 |
'window.jetpackVideoPressInlinePlayer = ' . wp_json_encode( self::get_asset_config(), JSON_UNESCAPED_SLASHES ) . ';', |
| 244 |
'before' |
| 245 |
); |
| 246 |
} |
| 247 |
|
| 248 |
if ( ! $defer_player ) { |
| 249 |
wp_enqueue_style( self::PLAYER_HANDLE, self::PLAYER_STYLE_URL, array(), Package_Version::PACKAGE_VERSION ); |
| 250 |
wp_enqueue_script( self::PLAYER_HANDLE, self::PLAYER_SCRIPT_URL, array(), Package_Version::PACKAGE_VERSION, true ); |
| 251 |
} |
| 252 |
|
| 253 |
// Private videos ask the page for a playback token, same as iframes do. |
| 254 |
Jwt_Token_Bridge::enqueue_jwt_token_bridge(); |
| 255 |
} |
| 256 |
|
| 257 |
/** |
| 258 |
* Whether a placeholder should start as a poster facade instead of a mounted player. |
| 259 |
* |
| 260 |
* Autoplaying videos need the player at once; everything else can wait for a click. |
| 261 |
* |
| 262 |
* @param array $options Player options, see `get_player_options()`. |
| 263 |
* @return bool |
| 264 |
*/ |
| 265 |
public static function should_use_facade( array $options = array() ) { |
| 266 |
$use_facade = empty( $options['autoPlay'] ); |
| 267 |
|
| 268 |
/** |
| 269 |
* Filter whether inline VideoPress players start as a poster facade and load the player on click. |
| 270 |
* |
| 271 |
* @since 0.51.0 |
| 272 |
* |
| 273 |
* @param bool $use_facade Whether to render the facade. |
| 274 |
* @param array $options The player options for this video. |
| 275 |
*/ |
| 276 |
return (bool) apply_filters( 'jetpack_videopress_inline_player_facade', $use_facade, $options ); |
| 277 |
} |
| 278 |
|
| 279 |
/** |
| 280 |
* Resolve the poster to show in a facade, without ever exposing a private video's frame. |
| 281 |
* |
| 282 |
* Order: the block's own poster attribute, the attachment's VideoPress metadata |
| 283 |
* ( by `id`, else by GUID ), then the transient-cached video details lookup. |
| 284 |
* |
| 285 |
* @param string $guid Video GUID. |
| 286 |
* @param array $attributes Block or shortcode attributes ( `poster`, `id`, `isPrivate`, `privacySetting` ). |
| 287 |
* @return string|null Poster URL, or null when none is usable. |
| 288 |
*/ |
| 289 |
public static function get_poster_url( $guid, array $attributes = array() ) { |
| 290 |
$poster = null; |
| 291 |
|
| 292 |
if ( ! empty( $attributes['poster'] ) && is_string( $attributes['poster'] ) ) { |
| 293 |
$poster = esc_url_raw( $attributes['poster'] ); |
| 294 |
} elseif ( ! self::is_private( $attributes ) ) { |
| 295 |
$poster = self::get_poster_from_attachment( $guid, $attributes['id'] ?? 0 ); |
| 296 |
|
| 297 |
if ( null === $poster && function_exists( 'videopress_get_video_details' ) ) { |
| 298 |
$details = videopress_get_video_details( $guid ); |
| 299 |
if ( is_object( $details ) && empty( $details->is_private ) && ! empty( $details->poster ) && is_string( $details->poster ) ) { |
| 300 |
$poster = esc_url_raw( $details->poster ); |
| 301 |
} |
| 302 |
} |
| 303 |
} |
| 304 |
|
| 305 |
/** |
| 306 |
* Filter the poster shown by an inline VideoPress player's facade. |
| 307 |
* |
| 308 |
* @since 0.51.0 |
| 309 |
* |
| 310 |
* @param string|null $poster Poster URL, or null for a plain dark facade. |
| 311 |
* @param string $guid Video GUID. |
| 312 |
* @param array $attributes The block or shortcode attributes. |
| 313 |
*/ |
| 314 |
$poster = apply_filters( 'jetpack_videopress_inline_player_poster', $poster, $guid, $attributes ); |
| 315 |
|
| 316 |
return is_string( $poster ) && '' !== $poster ? $poster : null; |
| 317 |
} |
| 318 |
|
| 319 |
/** |
| 320 |
* Whether the attributes describe a private video, treating "site default" as the site's own setting. |
| 321 |
* |
| 322 |
* @param array $attributes Block attributes. |
| 323 |
* @return bool |
| 324 |
*/ |
| 325 |
private static function is_private( array $attributes ) { |
| 326 |
if ( ! empty( $attributes['isPrivate'] ) ) { |
| 327 |
return true; |
| 328 |
} |
| 329 |
|
| 330 |
// A privacy setting of one is private and two follows the site default; anything else is public. |
| 331 |
$privacy = isset( $attributes['privacySetting'] ) ? (int) $attributes['privacySetting'] : 2; |
| 332 |
if ( 1 === $privacy ) { |
| 333 |
return true; |
| 334 |
} |
| 335 |
|
| 336 |
return 2 === $privacy && Data::get_videopress_videos_private_for_site(); |
| 337 |
} |
| 338 |
|
| 339 |
/** |
| 340 |
* Poster from the local attachment's VideoPress metadata, when the attachment belongs to this GUID. |
| 341 |
* |
| 342 |
* @param string $guid Video GUID. |
| 343 |
* @param int $attachment_id Attachment ID from the block, 0 to look the post up by GUID. |
| 344 |
* @return string|null |
| 345 |
*/ |
| 346 |
private static function get_poster_from_attachment( $guid, $attachment_id = 0 ) { |
| 347 |
$attachment_id = (int) $attachment_id; |
| 348 |
|
| 349 |
if ( $attachment_id <= 0 && function_exists( 'videopress_get_post_by_guid' ) ) { |
| 350 |
$post = videopress_get_post_by_guid( $guid ); |
| 351 |
$attachment_id = ( $post instanceof \WP_Post ) ? $post->ID : 0; |
| 352 |
} |
| 353 |
|
| 354 |
if ( $attachment_id <= 0 ) { |
| 355 |
return null; |
| 356 |
} |
| 357 |
|
| 358 |
$meta = wp_get_attachment_metadata( $attachment_id ); |
| 359 |
$videopress = is_array( $meta ) && isset( $meta['videopress'] ) && is_array( $meta['videopress'] ) ? $meta['videopress'] : array(); |
| 360 |
$poster = $videopress['poster'] ?? ''; |
| 361 |
$meta_guid = $videopress['guid'] ?? ''; |
| 362 |
|
| 363 |
if ( ! is_string( $poster ) || '' === $poster ) { |
| 364 |
return null; |
| 365 |
} |
| 366 |
|
| 367 |
// A block can point at another video than its attachment does; trust the GUID. |
| 368 |
if ( is_string( $meta_guid ) && '' !== $meta_guid && $meta_guid !== $guid ) { |
| 369 |
return null; |
| 370 |
} |
| 371 |
|
| 372 |
return esc_url_raw( $poster ); |
| 373 |
} |
| 374 |
|
| 375 |
/** |
| 376 |
* Stylesheet for the facade, printed inline so no extra request stands between the HTML and the poster. |
| 377 |
* |
| 378 |
* The play button, pre-play scrim and loading spinner copy the player's own chrome, so |
| 379 |
* nothing visibly changes when the player takes the facade's place. |
| 380 |
* |
| 381 |
* @return string CSS. |
| 382 |
*/ |
| 383 |
private static function facade_css() { |
| 384 |
$p = '.' . self::PLACEHOLDER_CLASS; |
| 385 |
$f = '.' . self::FACADE_CLASS; |
| 386 |
return $p . '.is-facade{background:#000;container-type:inline-size;--jetpack-videopress-play-size:min(max(calc(100vw / 8),60px),90px)}' |
| 387 |
. $f . '{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;background:transparent;cursor:pointer;display:block;line-height:0}' |
| 388 |
. $f . '-poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}' |
| 389 |
. $f . '-scrim{position:absolute;inset:0;pointer-events:none;background:radial-gradient(50% 50% at 50% 50%,rgba(0,0,0,.14) 0%,rgba(0,0,0,.3) 100%)}' |
| 390 |
. $f . '-play{position:absolute;top:50%;left:50%;width:var(--jetpack-videopress-play-size);height:var(--jetpack-videopress-play-size);transform:translate(-50%,-50%);display:block;transition:transform .25s cubic-bezier(.4,0,.6,1) .04s;will-change:transform}' |
| 391 |
. $f . '-play svg{display:block;width:100%;height:100%;fill:#fff}' |
| 392 |
. $p . '.is-facade:hover ' . $f . '-play{transform:translate(-50%,-50%) scale(1.08)}' |
| 393 |
. $f . ':focus-visible{outline:2px solid #fff;outline-offset:-4px}' |
| 394 |
. $f . '-spinner{position:absolute;inset:0;z-index:2;display:none;align-items:center;justify-content:center;pointer-events:none}' |
| 395 |
. $f . '-spinner span{display:flex;align-items:center;justify-content:center;width:48px;height:48px;border-radius:999px;background:rgba(18,18,18,.55);-webkit-backdrop-filter:blur(12px) saturate(1.2);backdrop-filter:blur(12px) saturate(1.2);box-shadow:0 4px 16px rgba(0,0,0,.35)}' |
| 396 |
. $f . '-spinner span::after{content:"";box-sizing:border-box;width:24px;height:24px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:jetpack-videopress-spin 750ms linear infinite}' |
| 397 |
. $p . '.is-loading ' . $f . '-play{display:none}' |
| 398 |
. $p . '.is-loading ' . $f . '-spinner{display:flex}' |
| 399 |
. '@keyframes jetpack-videopress-spin{to{transform:rotate(360deg)}}' |
| 400 |
. '@container (max-width:250px){' . $f . '-spinner span{width:40px;height:40px}}' |
| 401 |
. '@media screen and (max-width:200px){' . $p . '.is-facade{--jetpack-videopress-play-size:40px}' . $f . '-play{opacity:.75}}' |
| 402 |
. '@media (prefers-reduced-motion:reduce){' . $f . '-play{transition:none}}'; |
| 403 |
} |
| 404 |
|
| 405 |
/** |
| 406 |
* Render the placeholder the boot script mounts a player on. |
| 407 |
* |
| 408 |
* @param string $guid Video GUID. |
| 409 |
* @param array $options Player options, see `get_player_options()`. |
| 410 |
* @param float|null $ratio Height as a percentage of width (the block's `videoRatio`); 16:9 when unknown. |
| 411 |
* @param array $args Optional `poster` ( URL ), `title` ( for the play button's label ) and `facade` ( bool, default `should_use_facade()` ). |
| 412 |
* @return string Placeholder markup, or an empty string for an invalid GUID. |
| 413 |
*/ |
| 414 |
public static function render( $guid, array $options = array(), $ratio = null, array $args = array() ) { |
| 415 |
if ( ! is_string( $guid ) || ! ctype_alnum( $guid ) ) { |
| 416 |
return ''; |
| 417 |
} |
| 418 |
|
| 419 |
$facade = isset( $args['facade'] ) ? (bool) $args['facade'] : self::should_use_facade( $options ); |
| 420 |
|
| 421 |
self::enqueue_assets( $facade ); |
| 422 |
|
| 423 |
$ratio = is_numeric( $ratio ) && (float) $ratio > 0 ? (float) $ratio : 56.25; |
| 424 |
$style = sprintf( |
| 425 |
'position:relative;width:100%%;aspect-ratio:100 / %s;', |
| 426 |
rtrim( rtrim( number_format( $ratio, 4, '.', '' ), '0' ), '.' ) |
| 427 |
); |
| 428 |
|
| 429 |
$inner = ''; |
| 430 |
if ( $facade ) { |
| 431 |
wp_register_style( self::BOOT_HANDLE, false, array(), Package_Version::PACKAGE_VERSION ); |
| 432 |
wp_enqueue_style( self::BOOT_HANDLE ); |
| 433 |
if ( 0 === self::$rendered ) { |
| 434 |
wp_add_inline_style( self::BOOT_HANDLE, self::facade_css() ); |
| 435 |
} |
| 436 |
|
| 437 |
$title = isset( $args['title'] ) && is_string( $args['title'] ) ? trim( $args['title'] ) : ''; |
| 438 |
$label = '' !== $title |
| 439 |
/* translators: %s is the video title */ |
| 440 |
? sprintf( __( 'Play video: %s', 'jetpack-videopress-pkg' ), $title ) |
| 441 |
: __( 'Play video', 'jetpack-videopress-pkg' ); |
| 442 |
|
| 443 |
$poster = ''; |
| 444 |
if ( ! empty( $args['poster'] ) && is_string( $args['poster'] ) ) { |
| 445 |
// The first poster on the page is a likely LCP candidate; later ones can wait for the viewport. |
| 446 |
$poster = sprintf( |
| 447 |
'<img class="%1$s-poster" src="%2$s" alt="" decoding="async"%3$s>', |
| 448 |
esc_attr( self::FACADE_CLASS ), |
| 449 |
esc_url( $args['poster'] ), |
| 450 |
self::$rendered > 0 ? ' loading="lazy"' : ' fetchpriority="high"' |
| 451 |
); |
| 452 |
} |
| 453 |
|
| 454 |
// The glyph is the player's own large play icon. |
| 455 |
$inner = sprintf( |
| 456 |
'<button type="button" class="%1$s" aria-label="%2$s">%3$s<span class="%1$s-scrim" aria-hidden="true"></span><span class="%1$s-play" aria-hidden="true"><svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M6.25725 1.075C6.10279 0.977449 5.91041 0.974889 5.75365 1.06832C5.59689 1.16174 5.5 1.3367 5.5 1.52632V20.4737C5.5 20.6633 5.59689 20.8383 5.75365 20.9317C5.91041 21.0251 6.10279 21.0226 6.25725 20.925L21.2573 11.4513C21.4079 11.3562 21.5 11.1849 21.5 11C21.5 10.8151 21.4079 10.6438 21.2573 10.5487L6.25725 1.075Z"/></svg></span></button>' |
| 457 |
. '<span class="%1$s-spinner" role="status" aria-live="polite" aria-label="%4$s"><span></span></span>', |
| 458 |
esc_attr( self::FACADE_CLASS ), |
| 459 |
esc_attr( $label ), |
| 460 |
$poster, |
| 461 |
esc_attr__( 'Loading…', 'jetpack-videopress-pkg' ) |
| 462 |
); |
| 463 |
} |
| 464 |
|
| 465 |
++self::$rendered; |
| 466 |
|
| 467 |
return sprintf( |
| 468 |
'<div class="%1$s%5$s" data-videopress-guid="%2$s" data-videopress-options="%3$s"%6$s style="%4$s">%7$s</div>', |
| 469 |
esc_attr( self::PLACEHOLDER_CLASS ), |
| 470 |
esc_attr( $guid ), |
| 471 |
esc_attr( wp_json_encode( (object) $options, JSON_UNESCAPED_SLASHES ) ), |
| 472 |
esc_attr( $style ), |
| 473 |
$facade ? ' is-facade' : '', |
| 474 |
$facade ? ' data-videopress-facade="1"' : '', |
| 475 |
$inner |
| 476 |
); |
| 477 |
} |
| 478 |
|
| 479 |
/** |
| 480 |
* Forget per-request state ( tests ). |
| 481 |
*/ |
| 482 |
public static function reset() { |
| 483 |
self::$rendered = 0; |
| 484 |
self::$config_printed = false; |
| 485 |
} |
| 486 |
|
| 487 |
/** |
| 488 |
* Interpret the boolean spellings that reach us from attributes and query strings. |
| 489 |
* |
| 490 |
* @param mixed $value Raw value. |
| 491 |
* @return bool |
| 492 |
*/ |
| 493 |
private static function to_bool( $value ) { |
| 494 |
if ( is_string( $value ) ) { |
| 495 |
return ! in_array( strtolower( $value ), array( '', '0', 'false', 'no', 'off' ), true ); |
| 496 |
} |
| 497 |
return (bool) $value; |
| 498 |
} |
| 499 |
} |
| 500 |
|