Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add a VideoPress playlist block that stores a list of video GUIDs and plays them in sequence.
196 changes: 196 additions & 0 deletions projects/packages/videopress/src/class-initializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ public static function video_enqueue_bridge_when_oembed_present( $cache, $url, $
public static function register_videopress_blocks() {
// Register VideoPress Video block.
self::register_videopress_video_block();

// Register VideoPress Playlist block.
self::register_videopress_playlist_block();
}

/**
Expand Down Expand Up @@ -552,6 +555,199 @@ public static function register_videopress_video_block() {
Block_Editor_Extensions::init( $script_handle );
}

/**
* Register the VideoPress playlist block.
*
* @param string|null $metadata_file Path to the block.json metadata file.
* Defaults to the package build output; tests can point at a fixture.
*
* @return void
*/
public static function register_videopress_playlist_block( $metadata_file = null ) {
/*
* Unlike the video block, the playlist block has no "activate the module"
* placeholder, so don't register it at all when VideoPress isn't available.
*/
if (
Status::is_jetpack_plugin_without_videopress_module_active()
&& ! Status::is_standalone_plugin_active()
) {
return;
}

if ( null === $metadata_file ) {
$metadata_file = __DIR__ . '/../build/block-editor/blocks/playlist/block.json';
}

if ( ! file_exists( $metadata_file ) ) {
return;
}

$metadata = json_decode(
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
file_get_contents( $metadata_file )
);

if ( empty( $metadata->name ) ) {
return;
}

// Do not register if the block is already registered.
if ( \WP_Block_Type_Registry::get_instance()->is_registered( $metadata->name ) ) {
return;
}

register_block_type(
$metadata_file,
array(
'render_callback' => array( __CLASS__, 'render_videopress_playlist_block' ),
)
);
}

/**
* Get the freshest available title for a playlist video.
*
* Reads the public VideoPress API so renamed videos show their current
* title, cached in a transient to keep renders cheap. Falls back to the
* title stored in the block attributes when the API has no usable answer
* (e.g. private videos or connectivity issues).
*
* @param string $guid Video GUID.
* @param string $stored_title Title stored in the block attributes.
*
* @return string Title to render.
*/
private static function get_playlist_video_title( $guid, $stored_title ) {
$cache_key = 'videopress_playlist_title_' . $guid;
$cached = get_transient( $cache_key );

if ( false !== $cached ) {
// An empty string is a negative-cache entry from a recent failed lookup.
return is_string( $cached ) && '' !== $cached ? $cached : $stored_title;
}

$response = wp_remote_get(
'https://public-api.wordpress.com/rest/v1.1/videos/' . rawurlencode( $guid ),
array( 'timeout' => 2 )
);

if ( ! is_wp_error( $response ) && 200 === wp_remote_retrieve_response_code( $response ) ) {
$body = json_decode( wp_remote_retrieve_body( $response ), true );

if ( ! empty( $body['title'] ) && is_string( $body['title'] ) ) {
$title = wp_specialchars_decode( $body['title'], ENT_QUOTES );
set_transient( $cache_key, $title, HOUR_IN_SECONDS );

return $title;
}
}

// Negative-cache failures briefly so an unreachable API doesn't slow every render.
set_transient( $cache_key, '', 5 * MINUTE_IN_SECONDS );

return $stored_title;
}

/**
* Build the VideoPress embed URL for a playlist entry.
*
* @param string $guid Video GUID.
* @param bool $autoplay Whether the video should start playing as soon as it loads.
*
* @return string Embed URL.
*/
private static function get_playlist_embed_url( $guid, $autoplay ) {
return add_query_arg(
array(
'cover' => 1,
'autoPlay' => (int) $autoplay,
'preloadContent' => 'metadata',
),
'https://videopress.com/embed/' . rawurlencode( $guid )
);
}

/**
* VideoPress playlist block render method.
*
* @param array $block_attributes Block attributes.
*
* @return string Block markup.
*/
public static function render_videopress_playlist_block( $block_attributes ) {
$videos = array();

if ( isset( $block_attributes['videos'] ) && is_array( $block_attributes['videos'] ) ) {
foreach ( $block_attributes['videos'] as $video ) {
if ( ! is_array( $video ) || empty( $video['guid'] ) || ! is_string( $video['guid'] ) ) {
continue;
}

// VideoPress GUIDs are 8 alphanumeric characters; drop anything else.
if ( ! preg_match( '/^[a-z\d]{8}$/i', $video['guid'] ) ) {
continue;
}

$videos[] = array(
'guid' => $video['guid'],
'title' => isset( $video['title'] ) && is_string( $video['title'] ) ? $video['title'] : '',
);
}
}

if ( ! $videos ) {
return '';
}

$auto_advance = ! isset( $block_attributes['autoAdvance'] ) || $block_attributes['autoAdvance'];
$loop = ! empty( $block_attributes['loop'] );

// The JWT token bridge lets the embed play private videos for authorized viewers.
Jwt_Token_Bridge::enqueue_jwt_token_bridge();

$items_markup = '';
foreach ( $videos as $index => $video ) {
$title = self::get_playlist_video_title( $video['guid'], $video['title'] );

if ( '' === $title ) {
/* translators: %d: position of the video in the playlist. */
$title = sprintf( __( 'Video %d', 'jetpack-videopress-pkg' ), $index + 1 );
}

$items_markup .= sprintf(
'<li><button type="button" class="videopress-playlist__item%1$s" data-guid="%2$s" data-src="%3$s" aria-current="%4$s"><span class="videopress-playlist__item-index">%5$d.</span><span class="videopress-playlist__item-title">%6$s</span></button></li>',
0 === $index ? ' is-current' : '',
esc_attr( $video['guid'] ),
esc_url( self::get_playlist_embed_url( $video['guid'], true ) ),
0 === $index ? 'true' : 'false',
$index + 1,
esc_html( $title )
);
}

$player_markup = sprintf(
'<div class="videopress-playlist__player-wrapper"><iframe class="videopress-playlist__player" title="%1$s" aria-label="%1$s" src="%2$s" data-guid="%3$s" allowfullscreen allow="clipboard-write; presentation"></iframe></div>',
esc_attr__( 'VideoPress Playlist Player', 'jetpack-videopress-pkg' ),
esc_url( self::get_playlist_embed_url( $videos[0]['guid'], false ) ),
esc_attr( $videos[0]['guid'] )
);

$wrapper_attributes = get_block_wrapper_attributes(
array(
'data-auto-advance' => $auto_advance ? '1' : '0',
'data-loop' => $loop ? '1' : '0',
)
);

return sprintf(
'<figure %1$s>%2$s<ol class="videopress-playlist__items">%3$s</ol></figure>',
$wrapper_attributes,
$player_markup,
$items_markup
);
}

/**
* Enqueue the VideoPress Iframe API script
* when the URL of oEmbed HTML is a VideoPress URL.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "videopress/playlist",
"version": "0.1.0",
"title": "VideoPress Playlist",
"category": "media",
"icon": "<svg viewBox='0 0 29 21' width='24' height='24' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M2.79037 0.59375C4.0363 0.59375 5.13102 1.41658 5.47215 2.60947L8.8452 14.4044C8.8486 14.4164 8.85411 14.4273 8.86124 14.4368L12.8572 0.59375H15.0927H21.2721C25.6033 0.59375 28.5066 3.39892 28.5066 7.64565C28.5066 11.9411 25.5272 14.6196 21.0818 14.6196H18.1499H14.3719L13.6379 16.8813C12.9796 18.9095 11.0827 20.2839 8.94152 20.2839C6.80035 20.2839 4.90341 18.9095 4.24517 16.8813L0.137069 4.22276C-0.444671 2.43022 0.898038 0.59375 2.79037 0.59375ZM15.7374 10.4119H20.0156C21.8718 10.4119 22.9856 9.35018 22.9856 7.64565C22.9856 5.93137 21.8718 4.91839 20.0156 4.91839H17.5202L15.7374 10.4119Z'></path></svg>",
"description": "Play a list of VideoPress videos in sequence.",
"keywords": [ "playlist", "video", "videopress" ],
"supports": {
"html": false,
"align": true,
"anchor": true,
"spacing": {
"margin": true,
"padding": true
}
},
"attributes": {
"videos": {
"type": "array",
"items": {
"type": "object"
},
"default": []
},
"autoAdvance": {
"type": "boolean",
"default": true
},
"loop": {
"type": "boolean",
"default": false
}
},
"textdomain": "jetpack-videopress-pkg",
"editorScript": "file:./index.js",
"editorStyle": "file:./index.css",
"viewScript": "file:./view.js",
"viewStyle": "file:./view.css"
}
Loading
Loading