序
Chienomi組み込み向けにビデオプレイヤーシステムを作った。 地味に先日のkeychronの記事で実践投入されている。
取り組んだ経緯としては、脱YouTubeでサーバー負荷の少ない動画配信プラットフォーム構築を考えていて、MediaCMSを検討したけれどサーバー負荷が高いのでなんか作るかぁとなっていた。
Chienomiでは従来、単純にVP9をリンクしていたのだけど、SafariだとVP9は困るらしいし、解像度選択できたほうがいいよなぁと思っていたので今回Chienomiで使うものもその流れで作った。
実際のところ、動画配信プラットフォームとChienomi埋め込みでは求められるものが違うので流用はできないのだけど、お試しと知見の集積という意味ではまあまあ意義がある。
で、そのシステムはPlyrを採用した。 Vanillaで使えるよって言っているところが気に入った。
埋め込みの判定
Blessでメタデータに組み込み、テンプレートでロードする方式。
if content =~ /^:::\{\.videoplayer/
frontmatter["enable_video"] = true
endvideoplayerクラスを持つ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を付与、コンテナdivにappendChild()した上でIDを指定してPlyrを作るという流れ。
crypto.randomUUID()とかfetch()使ってるあたりが若干スパイシーかもしれない?
ちなみに、Plyrのロード自体はこのコードの中でscript要素を作ってもできるのだけど、追加されたscript要素が実行されるのはそれを作ったコードの実行が終了してからなので、ライブラリのロードみたいには使えない。
このため、テンプレートでロードする方式をとっている。
オリジン
Chienomiは結構厳しくCSP書いてるので、CDN経由でロードできない。
Plyrはセルフホストしやすいようにしてあって、CDNの配信ファイルをダウンロードして配置すればいいよということになっている。
ただ、plyr.jsがplyr.svgとblank.mp4をCDNからロードしようとする。
CSPで制限しているのでこれが失敗するため、これもローカルに必要。
こちらはCDNからダウンロードした上で
{
iconUrl: "/media/plyr.svg",
blankVideo: "/media/blank.mp4"
}というオプションをコンストラクタに渡すことでCDNからダウンロードしないようにできる。
あと、data:image/xml+svgで作っているところがあり、これはCSPを緩和するしかない。
img-src 'self' data:;
感想
ドキュメントが若干不親切。機能は限定的。
でも軽くて使いやすいので、今回の目的にはドンピシャだったと思う。 ちゃんとCDN使わない場合が考慮されているのも素晴らしい。
単純にvideo要素使うよりはいいよねって感じ。