Chienomi

plyrを使ってビデオプレイヤーを導入

雑感::site

Chienomi組み込み向けにビデオプレイヤーシステムを作った。 地味に先日のkeychronの記事で実践投入されている。

取り組んだ経緯としては、脱YouTubeでサーバー負荷の少ない動画配信プラットフォーム構築を考えていて、MediaCMSを検討したけれどサーバー負荷が高いのでなんか作るかぁとなっていた。

Chienomiでは従来、単純にVP9をリンクしていたのだけど、SafariだとVP9は困るらしいし、解像度選択できたほうがいいよなぁと思っていたので今回Chienomiで使うものもその流れで作った。

実際のところ、動画配信プラットフォームとChienomi埋め込みでは求められるものが違うので流用はできないのだけど、お試しと知見の集積という意味ではまあまあ意義がある。

で、そのシステムはPlyrを採用した。 Vanillaで使えるよって言っているところが気に入った。

埋め込みの判定

Blessでメタデータに組み込み、テンプレートでロードする方式。

    if content =~ /^:::\{\.videoplayer/
      frontmatter["enable_video"] = true
    end

videoplayerクラスを持つdiv要素がビデオ埋め込みの対象で、data-mediaidがURLのパートになっている。 PandocのMarkdownで表現しやすいのも良い感じ。

$if(enable_video)$
    <script src="/resources/js/3p/plyr.js"></script>
    <script defer="defer" type="module" src="/resources/js/videoplayer.js"></script>
$endif$

Plyr用データに展開

一応こういうスクリプトを作った。

#!/bin/zsh

media_id="$1"
source_file="$2"
title="$3"

usage() {
  print "generate-video.zsh <media_id> <source_file> <title>"
  exit 1
}

if [[ ! -e "$source_file" ]]
then
  usage
fi

if [[ -z "$title" ]]
then
  usage
fi

if [[ -e "media/$media_id" ]]
then
  read -q "?$media_id is already exists. overwrite? [yN] " || exit 2
else
  mkdir -pv "media/$media_id"
fi

ffmpeg -nostdin -i "$source_file" -c:v libvpx-vp9 -b:v 1300k -vf scale=-1:720 "media/$media_id/720.webm"
ffmpeg -nostdin -i "$source_file" -c:v libx264 -b:v 700k -profile:v baseline -vf scale=-1:576 "media/$media_id/576.mp4"
ffmpeg -nostdin -i "$source_file" -c:v libx264 -b:v 400k -profile:v baseline -vf scale=-1:360 "media/$media_id/360.mp4" 

cat > "media/$media_id/sources.json" << EOF
{
  "type": "video",
  "title": "$title",
  "sources": [
    {
      "src": "/media/$media_id/720.webm",
      "type": "video/webm",
      "size": 720
    },
    {
      "src": "/media/$media_id/576.mp4",
      "type": "video/mp4",
      "size": 576
    },
    {
      "src": "/media/$media_id/360.mp4",
      "type": "video/mp4",
      "size": 360
    }
  ]
}
EOF

けどまぁ、まだ運用してないからこれでいけるかは分からない。 まぁ、/media/$media_id/$res.$extの動画ファイルと、sources.jsonがあれば良い。

埋め込み

videoplayer.jsがこう

const videos = document.getElementsByClassName("videoplayer")

for (const i of videos) {
  try {
    const media_id = i.dataset.mediaid
    const source_meta = await fetch(`/media/${media_id}/sources.json`)
    if (source_meta.status === 200 ) {
      const plyr_container = document.createElement("video")
      plyr_container.playsInline = true
      plyr_container.controls = true
      const plyr_id = "chienomi-video-" + crypto.randomUUID()
      plyr_container.id = plyr_id
      i.appendChild(plyr_container)
      
      const player = new Plyr(("#" + plyr_id), {
        iconUrl: "/media/plyr.svg",
        blankVideo: "/media/blank.mp4"
      })
      player.source = await source_meta.json()
    } else {
      console.error("Failed to load video " + media_id)
    }
  } catch(e) {
    console.error("Failed to load video")
    continue
  }
}

new Plyr()Elementを渡すことができないので、video要素を作ってランダムなIDを付与、コンテナdivappendChild()した上でIDを指定してPlyrを作るという流れ。

crypto.randomUUID()とかfetch()使ってるあたりが若干スパイシーかもしれない?

ちなみに、Plyrのロード自体はこのコードの中でscript要素を作ってもできるのだけど、追加されたscript要素が実行されるのはそれを作ったコードの実行が終了してからなので、ライブラリのロードみたいには使えない。 このため、テンプレートでロードする方式をとっている。

オリジン

Chienomiは結構厳しくCSP書いてるので、CDN経由でロードできない。

Plyrはセルフホストしやすいようにしてあって、CDNの配信ファイルをダウンロードして配置すればいいよということになっている。

ただ、plyr.jsplyr.svgblank.mp4をCDNからロードしようとする。 CSPで制限しているのでこれが失敗するため、これもローカルに必要。

こちらはCDNからダウンロードした上で

{
  iconUrl: "/media/plyr.svg",
  blankVideo: "/media/blank.mp4"
}

というオプションをコンストラクタに渡すことでCDNからダウンロードしないようにできる。

あと、data:image/xml+svgで作っているところがあり、これはCSPを緩和するしかない。

img-src 'self' data:;

感想

ドキュメントが若干不親切。機能は限定的。

でも軽くて使いやすいので、今回の目的にはドンピシャだったと思う。 ちゃんとCDN使わない場合が考慮されているのも素晴らしい。

単純にvideo要素使うよりはいいよねって感じ。