💪

mp4等の動画ファイルからwebmでプレビューを作成する方法

2024/02/25に公開

TL;DR

プレビューが未作成の場合のみ実行:

やりたいこと

よく動画サイトとかであるホバーするとプレビューが表示されるのを作りたい。

検討した方法

  1. spriteイメージにて実現
  2. webpアニメーションにて実現
  3. webmにて実現

検討の結果3のwebmがメンテナビリティも高く、コードも複雑にならなさそうなので採用。

評価詳細

読みたい人用

# 実現方法 実装イメージ いいところ わるいところ
1 sprite css
backgroundbackground-positionでごにょごにょする
・枯れてる
・単純
・フレームが入れ替わる度にJSを発火させる必要があるが、処理を書くのがめんどくさそう
(特に複数ある場合に同時に動かすと重くなりそうなので、同期処理機構がほしくなりそう)
2 webp html
・webpを作成し、<img>タグで埋め込む
・実装はシンプル 一時停止不可
・作成方法は単純明快(連番で画像を作って、それをアニメーションにするだけ)
3 webm html
・webmを作成し、<video>タグで埋め込む
・実装はシンプル
・一時停止もDOMから直接触れるメソッドを呼ぶだけで簡単
・動画ファイルを作るのに秘伝のタレ的な技術が必要

とりあえず、2と3を検討。結果は3を採用。

webmの作成方法

基本的にはffmpegから呼ぶだけ。brewで入るffmpegではデフォルトでVP9が使える(libxvid)ため、特に何も考えずにbrew install ffmpegしとけば環境はできる。

作成の流れ

デコード設定

プレビュー作成のためにどうせフレームを間引くことが決定しているため、追加のデコード処理はしたくない。
そのため、-skip_frame nokey-fps_mode passthroughでキーフレームのみを渡すように指示する。
-skip_frameは動画の読込時、-fps_modeはStreamが渡される際のオプションのため、-iとの前後関係に注意する。(-skip_frame-i-fps_modeの順番で設定するのが正しい)
参考: キーフレームとは?Iフレーム・Pフレーム・Bフレームの違い

vfフィルター

vfフィルターは3つ指定する

  1. setpts (上記#1: 倍速加工に相当)
  2. fps (上記#2: フレーム間引きに相当)
  3. scale (上記#3: 縮小に相当)

setpts

setpts={VAR}で該当フレームにタイムスタンプを付与する。
今回はプレビューを作りたいので、適当に元の150倍ぐらいになるように、setpts=PTS/150で現在のタイムスタンプを圧縮する。
参考: ffmpeg documentation setpts

fps

fps={VAR}で出力する動画のfpsを指定する。
プレビューでは24fpsで動かす必要もないので、ある程度紙芝居的に動くfps=6を指定する。
こうすることで、6fpsに近いタイムスタンプに該当するフレームが抽出され、その他のフレームが間引かれる。
参考: ffmpeg documentation fps

scale

scale={VAR}で出力する動画のサイズを指定する。
プレビューでは小さくても問題ないので、scale=320:180(16:9動画の場合)ぐらいで指定しておく。

エンコード設定

webmを使うため、 VP9でのエンコード設定を探す。
過去にVP9のエンコードはやったことがないため、先人たちの知恵を拝借する。
基本的にはニコラボさんの解説を元に設定を行う。

品質設定

プレビュー動画の場合はファイルサイズ最適化の余地も少ないため、1passにてエンコードを行うべく、crfでの設定を採用。
上記サイトにある推奨設定よりも出力サイズが小さいため、上限値の-crf 37をとりあえず採用。
上記サイト冒頭にもあるように忘れずに-b:vも設定。

CPU設定

こちらも上記サイトの推奨設定より設定。
一番小さいサイズの設定を参考に、-cpu-used 0 -row-mt 1 -thread 1を設定。
(-threadの設定値が2の指数であることに注意。今回は2 threadsで動作させたいため、2^1となる1を設定)
参考: タイリングとスレッド化に関する推奨事項

最終的なコマンド

local file="hoge.mp4"
ffmpeg -hide_banner -nostats -skip_frame nokey -i "$file" -fps_mode passthrough -an -vf setpts=PTS/150,fps=6,scale=320:180 -vcodec libvpx-vp9 -crf 37 -b:v 0 -threads 1 -row-mt 1 -cpu-used 0 "${file:t:r}.webm"

webpの作成方法

採用しなかったが一応実装したのでメモ書き程度に。
ffmpegに加え、webpmuxというアニメーションwebpを使えるユーティリティツールを利用する。
(brew install ffmpegしていれば、依存関係で既に入っているため、特に追加の環境設定は不要)

作成の流れ

webmに比べると多少複雑に見えるが、webmを出力して、それをpackするだけの実装。

実装

特にwebmuxがフレームごとにオプションの指定が必要なため、簡単なshellscriptを書いて対応した。

function create_preview () {
  local file=$1
  local TMP_DIR=$(mktemp -d)

  mkdir -p "${TMP_DIR}/${file:t:r}"
  ffmpeg -hide_banner -nostats -skip_frame nokey -i "$file" -fps_mode passthrough -an -vf "fps=fps=1/90,scale=320\:180" -vcodec libwebp -preset picture "${TMP_DIR}/${file:t:r}/"%04d.webp
  local -a frames
  (){
    emulate -L zsh -o extended_glob
    for f in ${TMP_DIR}/${file:t:r}/*.webp; do
      frames+="-frame ${f} +250"
    done
  }
  eval "webpmux $frames -o ${file:t:r}.webp"
  rm -r "${TMP_DIR}/${file:t:r}"
}

問題点

<img>タグを利用する場合、DOMがHTMLMediaElementではなく、HTMLImageElementであるため、再生を止めたり・再開したりするAPIがnativeに存在しない。
gifアニメーションの一時停止を可能とするfreezframe.js等のライブラリの実装も確認したが、自らCanvasに描画する等の方法をとっており、今回のユースケースだと<img>タグ単体での実装は難しそうであることがわかった。
逆に、loopが前提となるアイコンやローディング画像であったりすると適している方法と思われるため、備忘録として記録を残しておく。

Discussion