Automattic / jetpack

Security, performance, marketing, and design tools — Jetpack is made by WordPress experts to make WP sites safer and faster, and help you grow your traffic.
https://jetpack.com/
Other
1.59k stars 798 forks source link

VideoPress: allow block settings support for changing play button size. #22810

Open dexter-adams opened 2 years ago

dexter-adams commented 2 years ago

There isn't a way for a user to change the enormous play button. It would be most ideal to provide hooks to override CSS attributes but at the very least an option for play button (icon) size.

kangzj commented 2 years ago

I have a user requesting for play button resizing too: 5330958-zen, as the video is embedded by a remote iframe, and user couldn't change the styling via CSS or JS.

bobmatyas commented 2 years ago

Request for the ability to use CSS to customize this came up in: https://wordpress.org/support/topic/how-to-customize-jetpack-videopress-player-ui/?view=all

github-actions[bot] commented 2 years ago

Support References

This comment is automatically generated. Please do not edit it.

BenceSzalai commented 2 years ago

The best would be to allow the theme to inject css into the iframe, so all players on a given site could accommodate the rest of the appearance. Also it seems to be the lowest effort solution, as with CSS everyone pretty much gets the freedom they need instead of implementing individual configuration options like play button size, seek bar height, volume icon etc. In a professional site all of these would be customised anyway, so CSS is the most versatile solution imho.

BenceSzalai commented 2 years ago

Digging a bit in the code I've found the jetpack_videopress_player_use_iframe filter here. So I assume it can be used to circumvent the IFRAME embed. I'll try to use that to see if that could be a way to apply the page CSS to the player.

Will come back to update it later.

Meanwhile I think the issue of "how to apply custom css" to the player is related, but in approach it is different than this one about only controlling the play button.

BenceSzalai commented 2 years ago

Findings

So it looks like jetpack_videopress_player_use_iframe is kinda broken, because it only works for videos added using a shortcode, e.g. [videopress WrC5GDcm]. However if someone adds the video into the block editor as a video block, the resulting <!-- wp:video {"guid":["WrC5GDcm"],... block code is not processed by the same code, but rather it is processed by WP_oEmbed which itself has a pattern as #https?://videopress\.com/v/.*# but also VideoPress_Shortcode::__construct() registers an oEmbed handler for the same. And oEmbed simply defaults to IFRAME based rendering.

So on one hand full CSS customisation is possible using a shortcode, however it is rather inconvenient for the editors.

Workaround

I've just made a content filter that simply replaces the block code with a shortcode, so it is rendered correctly by VideoPress, respecting jetpack_videopress_player_use_iframe.

add_filter( 'the_content', 'content_check',0);
function content_check($content) {
    $content = preg_replace('/<!--\s*wp:video\s*{[^}]*"guid":"([^\"]+)"[^}]*}\s*-->.*<!-- \/wp:video -->/s', '<!-- wp:shortcode -->[videopress $1]<!-- /wp:shortcode -->', $content);
    return $content;
}

(Indeed it is very rudimental, but it is only a PoC and with some improvement it could also process the properties and pass them as shortcode attributes.)

Once it is in place the play button size can be limited easily, which is in fact an issue on mobiles and small screens.

.video-js.vjs-videopress .vjs-big-play-button .vjs-icon-placeholder {
  max-width: 100px;
  max-height: 100px;
  background-size: contain;
}

However this is only a workaround

The proper behaviour imho would be for VideoPress to register a pre_render_block filter, and simply short circuit rendering of wp:video blocks containing a "guid" and render them properly instead of leaving it to WP_oEmbed. Probably I'll do that myself as well instead of the above workaround as filtering all content using regexp may cost some cpu... I'll post my solution once done for reference.

Another method could be to create another oEmbed provider such as https://public-api.wordpress.com/oembed/?for=develop.wordpress.com&url=https://videopress.com/v/{guid} which instead of an iframe returns inline code. Then based on the jetpack_videopress_player_use_iframe filter VideoPress_Shortcode::__construct() could register either the current or the non-iframe one.

BenceSzalai commented 2 years ago

A better Workaround

As mentioned above, a better workaround can be implemented using the pre_render_block filter.

// This ensures that VideoPress shortcodes are rendered inline the DOM instead of as an IFRAME.
add_filter( 'jetpack_videopress_player_use_iframe', '__return_false', 1000);

/**
 * We short circuit the rendering of the video blocks for VideoPress videos.
 *
 * @param string|null   $pre_render   The pre-rendered content. Default null.
 * @param array      $parsed_block The block being rendered.
 * @param WP_Block|null $parent_block If this is a nested block, a reference to the parent block.
 */
function divert_videopress_render( $pre_render, $parsed_block, $parent_block ) {

    if ( $parsed_block['blockName'] !== 'core/video' ) {
        return null;
    }

    $guid = $parsed_block['attrs']['guid'] ?? false;
    if ( !$guid ) {
        return null;
    }

    if ( !class_exists('VideoPress_Shortcode') ) {
        return null;
    }

    // We can pass the block attributes to the shortcode in lowercase
    $block_attrs = array_change_key_case($parsed_block['attrs'], CASE_LOWER);

    // useAverageColor can only be checked from the URL, it is not part of the block attributes.
    $useAverageColor = strpos( $parsed_block['innerHTML'], 'useAverageColor=true') > 0;
    if ( $useAverageColor ) {
        $block_attrs['useaveragecolor'] = 'true';
    }
    // guid must be added as a first non-associative value for the shortcode to work.
    array_unshift( $block_attrs, $guid );

    // Render the shortcode
    $result = VideoPress_Shortcode::initialize()->shortcode_callback($block_attrs);

    // Unfortunately these do not work with the shortcode: seekbarPlayedColor, seekbarLoadingColor, seekbarColor
    // but we can replicate custom color behaviour using inline styles targeting the player by unique id.
    $styles = [];
    if (!$useAverageColor) {
        $preg_matches = [];
        preg_match( '/<div id="(v-[\w-]+)"/m', $result, $preg_matches);
        $video_container_id = $preg_matches[1] ?? null;

        if ($video_container_id) {
            if (!empty( $parsed_block['attrs']['seekbarColor'] )) {
                $color = preg_replace('/[[:cntrl:]]/', '', str_replace([';','}'], '', $parsed_block['attrs']['seekbarColor']));
                $styles[] = "#$video_container_id .video-js.vjs-videopress .vjs-slider { background: $color; }";
            }
            if (!empty( $parsed_block['attrs']['seekbarPlayedColor'] )) {
                $color = preg_replace('/[[:cntrl:]]/', '', str_replace([';','}'], '', $parsed_block['attrs']['seekbarPlayedColor']));
                $styles[] = "#$video_container_id .video-js.vjs-videopress .vjs-play-progress { background: $color; }";
            }
            if (!empty( $parsed_block['attrs']['seekbarLoadingColor'] )) {
                $color = preg_replace('/[[:cntrl:]]/', '', str_replace([';','}'], '', $parsed_block['attrs']['seekbarLoadingColor']));
                $styles[] = "#$video_container_id .video-js.vjs-videopress .vjs-load-progress { background: $color; }";
                $styles[] = "#$video_container_id .video-js.vjs-videopress .vjs-load-progress div { background: $color; }";
            }
        }
    }

    // Add the custom styles if any
    if ( $styles !== [] ) {
        $result .= "<style>\n" . implode("\n", $styles) . "\n</style>";
    }

    // We replace the same part that the oEmbed would have replaced.
    if ( !empty($result) ) {
        return preg_replace(
            [ '#^https?://videopress.com/v/.*#im', '|^https?://v\.wordpress\.com/([a-zA-Z\d]{8})(.+)?$|im' ],
            [ $result, $result ],
            $parsed_block['innerHTML']
        );
    }
}
add_filter( 'pre_render_block', 'divert_videopress_render', 99, 3);

/**
 * We prevent oEmbed to render the video blocks.
 *
 * @param string|false $html   The cached HTML result, stored in post meta.
 * @param string       $url     The attempted embed URL.
 * @param array        $attr    An array of shortcode attributes.
 * @param int          $post_ID Post ID.
 */
function prevent_videopress_oembed_override( $html, string $url, array $attr, int $post_ID ) {
    // For the preview to work in the editor we need the oEmbed (queried over wp-json api).
    if (!wp_is_json_request()) {
        $patterns = [
            '#https?://videopress\.com/v/.*#', // Default WP
            '#^https?://videopress.com/v/.*#', // JetPack
            '|^https?://v\.wordpress\.com/([a-zA-Z\d]{8})(.+)?$|i', // JetPack
        ];

        foreach ( $patterns as $pattern ) {
            if ( preg_match( $pattern, $url ) ) {
                return $url; // We skip the embed, will handle when the block is rendered.
            }
        }
    }
    return $html;
}
add_filter( 'embed_oembed_html', 'prevent_videopress_oembed_override', 10, 4);

// Some demonstration of adding global styling for the player
function videopress_global_styles() { ?>
    <style>
        /* Limit play button size */
        .video-js.vjs-videopress .vjs-big-play-button .vjs-icon-placeholder {
            max-width: 100px;
            max-height: 100px;
            background-size: contain !important;
        }

        /* Make the shortcode player responsive */
        .video-js.vjs-videopress .vjs-tech {
            position: static;
            display: block;
        }
        .video-js.vjs-videopress {
            width: 100%;
            height: auto;
        }
        .video-js.vjs-videopress .videopress-overlay {
            top: 0;
        }
        .video-player {
            padding-bottom: 56.25%; /* 16:9 as min height to prevent flashing during load */
            height: 0;
        }
    </style>
<?php }
add_action('wp_head', 'videopress_global_styles');

One shortcoming of this is due to the limitation of the shortcode embed is that the video is not really responsive by default. Few css adjustments can make it responsive, however the video height may flash during load which is an issue too, so I've just set a 16:9 min height on the player by utilising the padding-bottom aspect ratio trick. But in other usecases this may be not enough, for example when a site deploys videos in various aspect ratios...

Conclusion

The issues are not impossible to fix, but it is also a matter of taste which approach is acceptable. I admit this one is a bit hackish, but afaics the oEmbed part only plays nicely as long as it is a fallback for a more controlled experience with higher customisability.

The solution can be turned into production code I think, but good few decisions should be made about how this thing should really work, mainly should 'jetpack_videopress_player_use_iframe' be respected in all cases, not just for shortcodes.

Also the shortcode player could use a fix to dynamically calculate the player size based on the known aspect ratio of the video using JS, pretty much the same as the iframe player does. Also it would be nice for the shortcode player to natively support seekbarPlayedColor, seekbarLoadingColor, seekbarColor just for the sake of consistency.

For the time being, this workaround suits my needs and I hope others will find it useful as well.