WordPressでvideoをjsonldで書き出す

WordPressでvideoタグを埋め込む際に、自動でjsonldを書き出す方法を模索した。ここで言うvideoタグはYouTubeなどの外部サービスではなく、自分のサーバー内にアップロードしたオリジナル動画を指す。

プラグインは使わない方向で、新規投稿だけではなく過去記事にも対応させる。

必要なjsonldを確認

VideoObjectに必要なjsonldの項目は以下。詳細はGoogleにも書かれている。

  • name(ビデオタイトル)
  • description(ビデオの説明)
  • thumbnailUrl(サムネイル画像)
  • uploadDate(ビデオのアップロード日をISO 8601形式)
  • contentUrl(実際のビデオファイルのURL)

contentUrl以外は必須項目。サムネイルは1つだけでもいいが、動画のサイズに合わせて複数用意も可能。この場合はGoogleが自動で最適なサムネイルを選択してくれる。

サムネイル

上記で厄介なのがサムネイル。これはvideoタグ内のposter属性で設定ができるが、必須ではない。自分もposter属性は手に余裕があるときしか設定していない。

設定していないということはthumbnailUrlにセットする値がないため、その場合は適当なプリセット画像を用意しておくのが一般的かと思う。

"thumbnailUrl": [
    // プリセット画像を使用(それぞれ1:1, 4:3, 16:9のサムネイル)
    "https://example.com/photos/1x1/photo.jpg",
    "https://example.com/photos/4x3/photo.jpg",
    "https://example.com/photos/16x9/photo.jpg"
   ], 

ただし、この場合は書き出すjsonldにサムネイル値はあるが、本体のHTMLにはサムネイルはセットされていない状態となる。jsonldという手段が目的になってしまっている気がして個人的にあまり好きでない。本来HTMLが実体であり、そこにメタデータが存在するのが正しい形なんじゃないかなと。

そこでjsonld書き出しの前に、videoタグに自動でサムネイルをセットする機能を実装する。プラグインやアイキャッチを利用する手もあるが、今回は独自フィルタを作成して実現した。以降、jsonld部分も含め以下の流れ組みます。

  1. videoタグにposterが設定されていれば、それをjsonldのthumbnailUrlで使う
  2. posterが設定されていなければ動画のアスペクト比を計算する
  3. 計算したアスペクト比が用意したプリセット画像(16:9, 4:3, 1:1)のいずれかにマッチすれば、そのURLをvideoのposter属性としてHTMLに埋め込む
  4. プリセットにない場合はposter属性は空とする

4は仕方がないのでサムネイルはjsonldの中でのみセットする。適当なサイズの画像を割り当ててもいいがが、アスペクト比の合わない画像をセットすると見栄えが良くない。

余白ができてしまう

このためプリセット画像に合わないものは、HTML側にサムネイルをセットしないようにした。以下がサムネイルをセットするフィルタ部分となる。

// 最大公約数を求める関数
function gcd($x, $y) {
    if ($y === 0) { return $x; }
	    return gcd($y, $x % $y);
}

// ショートコードフィルタ
function responsive_wp_video_shortcode($output, $atts, $video, $post_id, $library) {
    if ( empty($atts['poster']) ) {
        $w = intval($atts['width']);
        $h = intval($atts['height']);
        $g = gcd($w, $h);
        // アスペクト比を計算
        $x = $w/$g;
        $y = $h/$g;
        $ratio = array($x, $y);
        $poster = '';
        switch ($ratio) {
            case array (16, 9):
                $poster = get_template_directory_uri() . '/img/poster_16-9.png';
            break;
            case array (4, 3):
                $poster = get_template_directory_uri() . '/img/poster_4-3.png';
            break;
            case array (1, 1):
                $poster = get_template_directory_uri() . '/img/poster_1-1.png';
            break;
            default: // セットしない
            break;
        }
        if ( !empty($poster) ) {
            $output = str_replace( '<video', "<video poster=\"${poster}\"", $output);
        }
    }
    
    // その他のコード整形などの処理(略)
    
    return $output;
}
add_filter( 'wp_video_shortcode', 'responsive_wp_video_shortcode', 10, 5 );

これでvideoタグに可能な限りサムネイル画像がセットされることとなる。尚、上記でアスペクト比を求めるために最大公約数を求める関数gcd()を定義しているが、PHPにはGMPライブラリにgmp_gcd()という関数も用意されている。もし利用できる環境ならば、そちらを使用した方がいいと思う。

ビデオメタタグの生成

videoのjsonldを作成するためには、videoのメタデータ(投稿日やタイトル)が必要となる。videoのメタデータを取得には、get_attached_media()を使用した。これは第1引数にメディアの種類、第2引数に投稿IDを指定すると、その投稿内に添付されたメディアタイプを返してくれる。

$video_list = get_attached_media( 'video', $post->ID );

例えば、以下のようなデータが得られる。

{
"2661": {
	"ID": 2661, 
        "comment_count": "0", 
        "comment_status": "open", 
        "filter": "raw", 
        "guid": "https://xxx.com/wp-content/uploads/myvideo.webm", 
        "menu_order": 0, 
        "ping_status": "closed", 
        "pinged": "", 
        "post_author": "1", 
        "post_content": "", 
        "post_content_filtered": "", 
        "post_date": "2018-03-10 17:53:45", 
        "post_date_gmt": "2018-03-10 08:53:45", 
        "post_excerpt": "",
        "post_mime_type": "video/webm", 
        "post_modified": "2018-03-10 17:53:45", 
        "post_modified_gmt": "2018-03-10 08:53:45", 
        "post_name": "My Video", 
        "post_parent": 2646, 
        "post_password": "", 
        "post_status": "inherit", 
        "post_title": "Peek 2018-03-10 17-53", 
        "post_type": "attachment", 
        "to_ping": ""
    },
// (略)
}

ここにある値を使えば簡単にjsonldを構築できる。

  • guidをcontentUrl
  • post_dateをuploadDate
  • descriptionをdescription
  • post_titleをname

descriptionはメディアの追加時に入力していない場合もあると思うので、もし空ならばpost_titleをdescriptionにも設定すればいいと思う。

$description = $video->post_content ? $video->post_content : $video->post_title

問題は上を見てわかるようにメタの中にposterは含まれていないという点。このため、posterを別途取得する仕組みを作る必要がある。これがかなり面倒。

まず、posterを取得する方法がget_media_embedded_in_content()ぐらいしかない。この関数は第1引数にメディアデータを含む文字列(投稿のコンテンツ内容)、第2引数にメディアタイプ(今回はvideo)を指定すると投稿内に存在するvideoタグの配列を取得することができる。

見やすいように一部IDやclassを取り除いているが、概ね以下のような内容となる。

<video poster='posterのURL1' preload="metadata" controls="controls">
<source type="video/webm" src="xxx.webm?_=1" />
<a href="https://xxx.com/wp-content/uploads/myvideo.webm">
http://xxx.com/wp-content/uploads/myvideo.webm</a>
</video>

<video preload="metadata" controls="controls">
<source type="video/webm" src="yyy.webm?_=1" />
<a href="xxx.com/movie.webm">http://xxx.com/wp-content/uploads/movie.webm</a>
</video>

これは実際に埋め込まれるHTMLなので、この解説の最初でフィルタを設定している場合、アスペクト比が対応していない場合を除きposterがセットされています。勿論、手動でposterをセットした場合も含め。

ただし、2つ目のvideoタグのようにposter属性が存在しない。つまりフィルタ処理でどのアスペクト比の画像ともマッチしなかった場合もある。その際は別途jsonldに先に用意した3つのプリセット画像を配列形式でセットする。この場合、Googleは動画のアスペクト比に1番近い画像を自動的に選択してくれる。

メタデータとvideoタグの一致

次はHTMLタグ内のvideoとメタデータオブジェクトをどうマッチさせるか。上の出力HTMLを見てわかるように各動画ファイルがどのメタデータと等しいのかは、HTMLから直接取得できない。ただし、アンカータグのhref属性とメタデータのguidが等しいものは同じ動画を指していることがわかります。

// メタデータの方のプロパティ
"guid": "https://xxx.com/wp-content/uploads/myvideo.webm", 

// videoタグ内のaタグ
<a href="https://xxx.com/wp-content/uploads/myvideo.webm">

これを条件式にすればメタデータに該当するposterは取り出せる。あとはその仕組みを作ればOK。自分の場合はvideoタグを含むHTMLを各要素単位で分割し配列に格納。メタデータの反復の中でguidと等しいかのチェックを行った。パスすればvideoタグのposterの値を取り出してthubnailUrlにセットする。条件に一致しない場合やその他の例外でthubnailUrlが空の場合、先に記述したようにプリセット画像を配列形式でセットする。


// video(HTML)の配列を取得
$embeds = get_media_embedded_in_content( $content , 'video');
$split_tags_ary = [];

// video要素単位でループさせ、各要素を分割する
foreach ( $embeds as $elem ) {
    preg_match_all("/\<\w[^<>]*?\>([^<>]+?\<\/\w+?\>)?|\<\/\w+?\>/i", $elem, $matches);
    $split_tags_ary[] = $matches[0];
}

// videoメタデータを反復させてjsonldを作成
foreach ( $video_list as $video ) {
    $thumbnailUrl = '';
    $poster_pattern = '@poster="([^"]+)"@';

    if ( !empty($split_tags_ary) ) {
        foreach ( $split_tags_ary as $split_tag ) {
             foreach ( $split_tag as $tag ) {
                if ( (strpos($tag, $video->guid) !== false) && 
                   (preg_match($poster_pattern, $split_tag[0], $matches)) ) {
                    $thumbnailUrl = $matches[1];
                    break 2;
                 }
             }
        }
    }

    if ( empty($thumbnailUrl) ) {
        $thumbnailUrl =  array(
        get_template_directory_uri() . '/img/poster_16-9.png',
        get_template_directory_uri() . '/img/poster_4-3.png',
        get_template_directory_uri() . '/img/poster_1-1.png'
    );
}

以上でVideoObjectに必要な値はすべて用意できました。

$schemaVideo = array (
	"@context" => "http://schema.org",
    "@type" => "VideoObject",
    "name" => $video->post_title,
    "description" => $description,
    "thumbnailUrl" => $thumbnailUrl,
    "uploadDate" => $video->post_date,
    "contentUrl" => $video->guid
);

あとはjson_encode()で書き出すだけです。

問題点

問題点があるとすれば、get_attached_media()で取得できるオブジェクトは過去に添付した動画の場合は、再び取得できないという点です。例えばAという記事にvideo1という動画を添付し、その後にBという記事にvideo1という動画ファイルを添付した場合、get_attached_media()はこの動画のメタデータを取得できない。勿論、B記事に新しくvideo2という動画を添付した場合は、その記事内で取得可能。

これはバグなのか仕様なのか自分のテーマの組み方によるミスなのかわからないが、重複も回避できるのでとりあえず現状はこのまま利用している。