PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-videopress / src / class-inline-player.php

class-inline-player.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-videopress/src/class-inline-player.php

500 lines 19.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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