PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.3
16.3 16.3-beta 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 All 508 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.3, at jetpack_vendor/automattic/jetpack-videopress/src/class-inline-player.php

484 lines 17.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 * @return string CSS.
379 */
380 private static function facade_css() {
381 $f = '.' . self::FACADE_CLASS;
382 return '.' . self::PLACEHOLDER_CLASS . '.is-facade{background:#000}'
383 . $f . '{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;background:transparent;cursor:pointer;display:block;line-height:0}'
384 . $f . '-poster{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}'
385 . $f . '-play{position:absolute;top:50%;left:50%;width:72px;height:72px;transform:translate(-50%,-50%);border-radius:50%;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;transition:background .2s ease}'
386 . $f . ':hover ' . $f . '-play,' . $f . ':focus-visible ' . $f . '-play{background:rgba(0,0,0,.85)}'
387 . $f . '-play svg{width:32px;height:32px;fill:#fff;margin-inline-start:4px}'
388 . $f . ':focus-visible{outline:2px solid #fff;outline-offset:-4px}'
389 . $f . '.is-loading ' . $f . '-play{opacity:.5}';
390 }
391
392 /**
393 * Render the placeholder the boot script mounts a player on.
394 *
395 * @param string $guid Video GUID.
396 * @param array $options Player options, see `get_player_options()`.
397 * @param float|null $ratio Height as a percentage of width (the block's `videoRatio`); 16:9 when unknown.
398 * @param array $args Optional `poster` ( URL ), `title` ( for the play button's label ) and `facade` ( bool, default `should_use_facade()` ).
399 * @return string Placeholder markup, or an empty string for an invalid GUID.
400 */
401 public static function render( $guid, array $options = array(), $ratio = null, array $args = array() ) {
402 if ( ! is_string( $guid ) || ! ctype_alnum( $guid ) ) {
403 return '';
404 }
405
406 $facade = isset( $args['facade'] ) ? (bool) $args['facade'] : self::should_use_facade( $options );
407
408 self::enqueue_assets( $facade );
409
410 $ratio = is_numeric( $ratio ) && (float) $ratio > 0 ? (float) $ratio : 56.25;
411 $style = sprintf(
412 'position:relative;width:100%%;aspect-ratio:100 / %s;',
413 rtrim( rtrim( number_format( $ratio, 4, '.', '' ), '0' ), '.' )
414 );
415
416 $inner = '';
417 if ( $facade ) {
418 wp_register_style( self::BOOT_HANDLE, false, array(), Package_Version::PACKAGE_VERSION );
419 wp_enqueue_style( self::BOOT_HANDLE );
420 if ( 0 === self::$rendered ) {
421 wp_add_inline_style( self::BOOT_HANDLE, self::facade_css() );
422 }
423
424 $title = isset( $args['title'] ) && is_string( $args['title'] ) ? trim( $args['title'] ) : '';
425 $label = '' !== $title
426 /* translators: %s is the video title */
427 ? sprintf( __( 'Play video: %s', 'jetpack-videopress-pkg' ), $title )
428 : __( 'Play video', 'jetpack-videopress-pkg' );
429
430 $poster = '';
431 if ( ! empty( $args['poster'] ) && is_string( $args['poster'] ) ) {
432 // The first poster on the page is a likely LCP candidate; later ones can wait for the viewport.
433 $poster = sprintf(
434 '<img class="%1$s-poster" src="%2$s" alt="" decoding="async"%3$s>',
435 esc_attr( self::FACADE_CLASS ),
436 esc_url( $args['poster'] ),
437 self::$rendered > 0 ? ' loading="lazy"' : ' fetchpriority="high"'
438 );
439 }
440
441 $inner = sprintf(
442 '<button type="button" class="%1$s" aria-label="%2$s">%3$s<span class="%1$s-play" aria-hidden="true"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v14l11-7z"/></svg></span></button>',
443 esc_attr( self::FACADE_CLASS ),
444 esc_attr( $label ),
445 $poster
446 );
447 }
448
449 ++self::$rendered;
450
451 return sprintf(
452 '<div class="%1$s%5$s" data-videopress-guid="%2$s" data-videopress-options="%3$s"%6$s style="%4$s">%7$s</div>',
453 esc_attr( self::PLACEHOLDER_CLASS ),
454 esc_attr( $guid ),
455 esc_attr( wp_json_encode( (object) $options, JSON_UNESCAPED_SLASHES ) ),
456 esc_attr( $style ),
457 $facade ? ' is-facade' : '',
458 $facade ? ' data-videopress-facade="1"' : '',
459 $inner
460 );
461 }
462
463 /**
464 * Forget per-request state ( tests ).
465 */
466 public static function reset() {
467 self::$rendered = 0;
468 self::$config_printed = false;
469 }
470
471 /**
472 * Interpret the boolean spellings that reach us from attributes and query strings.
473 *
474 * @param mixed $value Raw value.
475 * @return bool
476 */
477 private static function to_bool( $value ) {
478 if ( is_string( $value ) ) {
479 return ! in_array( strtolower( $value ), array( '', '0', 'false', 'no', 'off' ), true );
480 }
481 return (bool) $value;
482 }
483 }
484