false,
'controls' => true,
'loop' => false,
'muted' => false,
'playsinline' => false,
'poster' => '',
'preload' => 'metadata',
'seekbarColor' => '',
'seekbarPlayedColor' => '',
'seekbarLoadingColor' => '',
'useAverageColor' => true,
'cover' => true,
'hd' => false,
'at' => 0,
'defaultLangCode' => '',
)
);
$preload = is_string( $attributes['preload'] ) ? strtolower( $attributes['preload'] ) : 'metadata';
if ( ! in_array( $preload, array( 'auto', 'metadata', 'none' ), true ) ) {
$preload = 'metadata';
}
// The site-wide opt-out wins over the embed's own preload attribute.
if ( Data::get_videopress_player_preload_disabled() ) {
$preload = 'none';
}
$options = array(
'autoPlay' => self::to_bool( $attributes['autoplay'] ),
'controls' => self::to_bool( $attributes['controls'] ),
'loop' => self::to_bool( $attributes['loop'] ),
'muted' => self::to_bool( $attributes['muted'] ),
'persistVolume' => ! self::to_bool( $attributes['muted'] ),
'playsinline' => self::to_bool( $attributes['playsinline'] ),
'cover' => self::to_bool( $attributes['cover'] ),
'hd' => self::to_bool( $attributes['hd'] ),
'useAverageColor' => self::to_bool( $attributes['useAverageColor'] ),
'preloadContent' => $preload,
// Embed pages default to the current player skin; match them.
'chrome' => 'v2',
);
if ( (int) $attributes['at'] > 0 ) {
$options['at'] = (int) $attributes['at'];
}
if ( ! empty( $attributes['poster'] ) && is_string( $attributes['poster'] ) ) {
$options['poster'] = esc_url_raw( $attributes['poster'] );
}
if ( ! empty( $attributes['defaultLangCode'] ) && is_string( $attributes['defaultLangCode'] ) ) {
$options['defaultLangCode'] = $attributes['defaultLangCode'];
}
$colors = array(
'seekbarColor' => 'seekbarColor',
'seekbarPlayedColor' => 'seekbarPlayedColor',
'seekbarLoadingColor' => 'seekbarLoadedColor',
);
foreach ( $colors as $attribute => $option ) {
if ( ! empty( $attributes[ $attribute ] ) && is_string( $attributes[ $attribute ] ) ) {
$options[ $option ] = $attributes[ $attribute ];
}
}
/**
* Filter the options passed to an inline VideoPress player.
*
* @since 0.50.2
*
* @param array $options Player options.
* @param array $attributes The block or shortcode attributes they were built from.
*/
return apply_filters( 'jetpack_videopress_inline_player_options', $options, $attributes );
}
/**
* Read the player attributes an embed URL carries in its query string.
*
* Used to render inline players for videopress.com URLs that reach the
* page through oEmbed. Only parameters present in the URL are returned.
*
* @param string $url A videopress.com/v or /embed URL.
* @return array Attributes in the shape `get_player_options()` accepts.
*/
public static function get_attributes_from_embed_url( $url ) {
$query = wp_parse_url( $url, PHP_URL_QUERY );
if ( ! is_string( $query ) || '' === $query ) {
return array();
}
$params = array();
parse_str( html_entity_decode( $query, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 ), $params );
$booleans = array(
'autoPlay' => 'autoplay',
'autoplay' => 'autoplay',
'controls' => 'controls',
'loop' => 'loop',
'muted' => 'muted',
'playsinline' => 'playsinline',
'useAverageColor' => 'useAverageColor',
'cover' => 'cover',
'hd' => 'hd',
);
$strings = array(
'posterUrl' => 'poster',
'preloadContent' => 'preload',
'sbc' => 'seekbarColor',
'sbpc' => 'seekbarPlayedColor',
'sblc' => 'seekbarLoadingColor',
'defaultLangCode' => 'defaultLangCode',
);
$attributes = array();
foreach ( $booleans as $param => $attribute ) {
if ( isset( $params[ $param ] ) && is_string( $params[ $param ] ) ) {
$attributes[ $attribute ] = self::to_bool( $params[ $param ] );
}
}
foreach ( $strings as $param => $attribute ) {
if ( ! empty( $params[ $param ] ) && is_string( $params[ $param ] ) ) {
$attributes[ $attribute ] = $params[ $param ];
}
}
if ( isset( $params['at'] ) && (int) $params['at'] > 0 ) {
$attributes['at'] = (int) $params['at'];
}
return $attributes;
}
/**
* Versioned URLs of the player bundle and its stylesheet, for anything that loads the player itself.
*
* @return array{script: string, style: string}
*/
public static function get_asset_config() {
return array(
'script' => add_query_arg( 'ver', Package_Version::PACKAGE_VERSION, self::PLAYER_SCRIPT_URL ),
'style' => add_query_arg( 'ver', Package_Version::PACKAGE_VERSION, self::PLAYER_STYLE_URL ),
);
}
/**
* Enqueue the boot script and, unless every player on the page sits behind a facade, the shared player assets.
*
* The boot script never depends on the player handle: behind a facade it fetches the
* bundle itself on the first click, from the URLs printed in its config.
*
* @param bool $defer_player True to leave the player bundle for the boot script to load on demand.
*/
public static function enqueue_assets( $defer_player = false ) {
wp_enqueue_script(
self::BOOT_HANDLE,
plugins_url( '../build/lib/inline-player.js', __FILE__ ),
array(),
Package_Version::PACKAGE_VERSION,
true
);
if ( ! self::$config_printed ) {
self::$config_printed = true;
wp_add_inline_script(
self::BOOT_HANDLE,
'window.jetpackVideoPressInlinePlayer = ' . wp_json_encode( self::get_asset_config(), JSON_UNESCAPED_SLASHES ) . ';',
'before'
);
}
if ( ! $defer_player ) {
wp_enqueue_style( self::PLAYER_HANDLE, self::PLAYER_STYLE_URL, array(), Package_Version::PACKAGE_VERSION );
wp_enqueue_script( self::PLAYER_HANDLE, self::PLAYER_SCRIPT_URL, array(), Package_Version::PACKAGE_VERSION, true );
}
// Private videos ask the page for a playback token, same as iframes do.
Jwt_Token_Bridge::enqueue_jwt_token_bridge();
}
/**
* Whether a placeholder should start as a poster facade instead of a mounted player.
*
* Autoplaying videos need the player at once; everything else can wait for a click.
*
* @param array $options Player options, see `get_player_options()`.
* @return bool
*/
public static function should_use_facade( array $options = array() ) {
$use_facade = empty( $options['autoPlay'] );
/**
* Filter whether inline VideoPress players start as a poster facade and load the player on click.
*
* @since 0.51.0
*
* @param bool $use_facade Whether to render the facade.
* @param array $options The player options for this video.
*/
return (bool) apply_filters( 'jetpack_videopress_inline_player_facade', $use_facade, $options );
}
/**
* Resolve the poster to show in a facade, without ever exposing a private video's frame.
*
* Order: the block's own poster attribute, the attachment's VideoPress metadata
* ( by `id`, else by GUID ), then the transient-cached video details lookup.
*
* @param string $guid Video GUID.
* @param array $attributes Block or shortcode attributes ( `poster`, `id`, `isPrivate`, `privacySetting` ).
* @return string|null Poster URL, or null when none is usable.
*/
public static function get_poster_url( $guid, array $attributes = array() ) {
$poster = null;
if ( ! empty( $attributes['poster'] ) && is_string( $attributes['poster'] ) ) {
$poster = esc_url_raw( $attributes['poster'] );
} elseif ( ! self::is_private( $attributes ) ) {
$poster = self::get_poster_from_attachment( $guid, $attributes['id'] ?? 0 );
if ( null === $poster && function_exists( 'videopress_get_video_details' ) ) {
$details = videopress_get_video_details( $guid );
if ( is_object( $details ) && empty( $details->is_private ) && ! empty( $details->poster ) && is_string( $details->poster ) ) {
$poster = esc_url_raw( $details->poster );
}
}
}
/**
* Filter the poster shown by an inline VideoPress player's facade.
*
* @since 0.51.0
*
* @param string|null $poster Poster URL, or null for a plain dark facade.
* @param string $guid Video GUID.
* @param array $attributes The block or shortcode attributes.
*/
$poster = apply_filters( 'jetpack_videopress_inline_player_poster', $poster, $guid, $attributes );
return is_string( $poster ) && '' !== $poster ? $poster : null;
}
/**
* Whether the attributes describe a private video, treating "site default" as the site's own setting.
*
* @param array $attributes Block attributes.
* @return bool
*/
private static function is_private( array $attributes ) {
if ( ! empty( $attributes['isPrivate'] ) ) {
return true;
}
// A privacy setting of one is private and two follows the site default; anything else is public.
$privacy = isset( $attributes['privacySetting'] ) ? (int) $attributes['privacySetting'] : 2;
if ( 1 === $privacy ) {
return true;
}
return 2 === $privacy && Data::get_videopress_videos_private_for_site();
}
/**
* Poster from the local attachment's VideoPress metadata, when the attachment belongs to this GUID.
*
* @param string $guid Video GUID.
* @param int $attachment_id Attachment ID from the block, 0 to look the post up by GUID.
* @return string|null
*/
private static function get_poster_from_attachment( $guid, $attachment_id = 0 ) {
$attachment_id = (int) $attachment_id;
if ( $attachment_id <= 0 && function_exists( 'videopress_get_post_by_guid' ) ) {
$post = videopress_get_post_by_guid( $guid );
$attachment_id = ( $post instanceof \WP_Post ) ? $post->ID : 0;
}
if ( $attachment_id <= 0 ) {
return null;
}
$meta = wp_get_attachment_metadata( $attachment_id );
$videopress = is_array( $meta ) && isset( $meta['videopress'] ) && is_array( $meta['videopress'] ) ? $meta['videopress'] : array();
$poster = $videopress['poster'] ?? '';
$meta_guid = $videopress['guid'] ?? '';
if ( ! is_string( $poster ) || '' === $poster ) {
return null;
}
// A block can point at another video than its attachment does; trust the GUID.
if ( is_string( $meta_guid ) && '' !== $meta_guid && $meta_guid !== $guid ) {
return null;
}
return esc_url_raw( $poster );
}
/**
* Stylesheet for the facade, printed inline so no extra request stands between the HTML and the poster.
*
* The play button, pre-play scrim and loading spinner copy the player's own chrome, so
* nothing visibly changes when the player takes the facade's place.
*
* @return string CSS.
*/
private static function facade_css() {
$p = '.' . self::PLACEHOLDER_CLASS;
$f = '.' . self::FACADE_CLASS;
return $p . '.is-facade{background:#000;container-type:inline-size;--jetpack-videopress-play-size:min(max(calc(100vw / 8),60px),90px)}'
. $f . '{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;background:transparent;cursor:pointer;display:block;line-height:0}'
. $f . '-poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}'
. $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%)}'
. $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}'
. $f . '-play svg{display:block;width:100%;height:100%;fill:#fff}'
. $p . '.is-facade:hover ' . $f . '-play{transform:translate(-50%,-50%) scale(1.08)}'
. $f . ':focus-visible{outline:2px solid #fff;outline-offset:-4px}'
. $f . '-spinner{position:absolute;inset:0;z-index:2;display:none;align-items:center;justify-content:center;pointer-events:none}'
. $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)}'
. $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}'
. $p . '.is-loading ' . $f . '-play{display:none}'
. $p . '.is-loading ' . $f . '-spinner{display:flex}'
. '@keyframes jetpack-videopress-spin{to{transform:rotate(360deg)}}'
. '@container (max-width:250px){' . $f . '-spinner span{width:40px;height:40px}}'
. '@media screen and (max-width:200px){' . $p . '.is-facade{--jetpack-videopress-play-size:40px}' . $f . '-play{opacity:.75}}'
. '@media (prefers-reduced-motion:reduce){' . $f . '-play{transition:none}}';
}
/**
* Render the placeholder the boot script mounts a player on.
*
* @param string $guid Video GUID.
* @param array $options Player options, see `get_player_options()`.
* @param float|null $ratio Height as a percentage of width (the block's `videoRatio`); 16:9 when unknown.
* @param array $args Optional `poster` ( URL ), `title` ( for the play button's label ) and `facade` ( bool, default `should_use_facade()` ).
* @return string Placeholder markup, or an empty string for an invalid GUID.
*/
public static function render( $guid, array $options = array(), $ratio = null, array $args = array() ) {
if ( ! is_string( $guid ) || ! ctype_alnum( $guid ) ) {
return '';
}
$facade = isset( $args['facade'] ) ? (bool) $args['facade'] : self::should_use_facade( $options );
self::enqueue_assets( $facade );
$ratio = is_numeric( $ratio ) && (float) $ratio > 0 ? (float) $ratio : 56.25;
$style = sprintf(
'position:relative;width:100%%;aspect-ratio:100 / %s;',
rtrim( rtrim( number_format( $ratio, 4, '.', '' ), '0' ), '.' )
);
$inner = '';
if ( $facade ) {
wp_register_style( self::BOOT_HANDLE, false, array(), Package_Version::PACKAGE_VERSION );
wp_enqueue_style( self::BOOT_HANDLE );
if ( 0 === self::$rendered ) {
wp_add_inline_style( self::BOOT_HANDLE, self::facade_css() );
}
$title = isset( $args['title'] ) && is_string( $args['title'] ) ? trim( $args['title'] ) : '';
$label = '' !== $title
/* translators: %s is the video title */
? sprintf( __( 'Play video: %s', 'jetpack-videopress-pkg' ), $title )
: __( 'Play video', 'jetpack-videopress-pkg' );
$poster = '';
if ( ! empty( $args['poster'] ) && is_string( $args['poster'] ) ) {
// The first poster on the page is a likely LCP candidate; later ones can wait for the viewport.
$poster = sprintf(
'',
esc_attr( self::FACADE_CLASS ),
esc_url( $args['poster'] ),
self::$rendered > 0 ? ' loading="lazy"' : ' fetchpriority="high"'
);
}
// The glyph is the player's own large play icon.
$inner = sprintf(
''
. '',
esc_attr( self::FACADE_CLASS ),
esc_attr( $label ),
$poster,
esc_attr__( 'Loading…', 'jetpack-videopress-pkg' )
);
}
++self::$rendered;
return sprintf(
'