fqmpeg の C12 クラスタは 動画 ↔ 個別フレーム の往復だ。単発サムネ、定期的なスナップショット、コンタクトシート、フィルムストリップ、シーン検出による自動分割、静止画からのスライドショー、横並び比較、ffprobe でのフレーム数カウント — 全部で 12 動詞。1 つだけ ffprobe で、残りは全部 ffmpeg が動くが、生のフィルタチェーンよりはるかに単純な引数を露出している。
この記事では fqmpeg 3.0.3 の src/commands/ を実読しながら、各動詞の元 FFmpeg フィルタ、デフォルト値、出力ファイル名、--help だけでは見えない罠(thumbnail-grid と tile はサンプリングは同じだがデフォルトファイル名が違う、snapshot と video-to-frames は両方とも定期スチル抽出だが出力ディレクトリのデフォルトが違う、frames-to-video と slideshow は両方とも画像から動画を作るが内部機構は全然違う)を全部書く。
この記事で得られるもの
- 12 動詞をタスクで選ぶ早見表(単発スチル / 複数スチル / コンタクトシート / 動画再構築 / 解析)
- 各動詞が出す FFmpeg 起動行(
--dry-run検証済み) - デフォルト・単位・出力ファイル名 — そして似た動詞同士の違い(
thumbnail-gridvstile、snapshotvsvideo-to-frames) - 実例 3 本: YouTube サムネ作成、防犯カメラからのタイムラプス、フィルタ前後比較動画
12 動詞の全体図
クラスタは 4 タスクグループに分かれる。グループを選んで、動詞を選ぶ。
| グループ | 動詞 | やること |
|---|---|---|
| 単発・定期スチル | thumbnail, snapshot, video-to-frames, count-frames | 任意時点の 1 枚、定期間隔の連番、全フレーム抽出、フレーム数カウント |
| コンタクトシート・フィルムストリップ | thumbnail-grid, thumbnail-strip, tile | 複数フレームを 1 枚の画像に合成(グリッド or 横一列) |
| フレーム ↔ 動画再構築 | frames-to-video, slideshow | 連番パターンから動画、画像リストからスライドショー |
| 解析・合成 | scenes, preview, compare | シーン切れ検出、ハイライト動画、横並び比較 |
読み始める前に押さえておきたい 5 つのポイント:
thumbnail-gridとtileはどちらもコンタクトシートで、サンプリングは同じ時間ベース。 両方ともselect='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)'で 1 秒に 1 フレーム選ぶ。短い動画も長い動画も予測通り。2 つの動詞は発見性のために残してある別名関係 — 「grid」で検索しても「tile」で検索してもここに辿り着くようにしている。違うのはデフォルト出力ファイル名だけ(-grid.jpgvs-tile<C>x<R>.jpg)。snapshotは入力と同じディレクトリに出力、video-to-framesはカレントディレクトリに出力。 どちらも定期スチル抽出だが、デフォルトの出力パスが違う —snapshotは<入力dir>/<stem>-snap-%04d.jpg、video-to-framesは cwd の./frame_%04d.png。予測可能なパスにしたいなら必ず-oを渡すこと。count-framesは C12 で唯一ffmpegではなくffprobeを使う動詞。ffprobe -count_frames -select_streams v:0を実行して整数を 1 つ標準出力に書く。ファイルは作らない。frames-to-videoは-r(出力レート)ではなく-framerate(入力レート)を使う。 これは重要で、-framerateは静止画列を「秒間何枚として解釈するか」を FFmpeg に教える指定で、-rは出力ストリームを再タイミングする指定。一定レートで画像→動画を作るなら-framerateが正解で、fqmpeg もこれを使っている。scenesはシーン切れで自動分割する。 各シーンが独立ファイルになる(<stem>-scene000.mp4、<stem>-scene001.mp4、...)— 長い録画から自動でクリップを切り出すのに便利。閾値は0.0–1.0、低いほど敏感(カット数が増える)。
単発・定期スチル
thumbnail — 任意時点の 1 フレームを 1 枚の画像に
クラスタで一番シンプルな動詞: 指定タイムスタンプにシークして JPEG を 1 枚書く。動画サムネ(YouTube アップロード、ギャラリーカバー、OG 画像タグ)に使う。
- ソース:
src/commands/thumbnail.js - フラグ:
-ss <sec> -i <input> -frames:v 1 -q:v 2 - 出力:
<入力stem>-thumb.jpg
| 引数 / オプション | デフォルト | 備考 |
|---|---|---|
<input> | 必須 | 入力動画 |
-s, --start <sec> | 1 | 秒数(小数可) |
-o, --output <path> | <入力stem>-thumb.jpg | 出力上書き |
$ npx fqmpeg thumbnail input.mp4 --dry-run
ffmpeg -ss 1 -i input.mp4 -frames:v 1 -q:v 2 input-thumb.jpg
$ npx fqmpeg thumbnail input.mp4 -s 45.5 -o cover.jpg --dry-run
ffmpeg -ss 45.5 -i input.mp4 -frames:v 1 -q:v 2 cover.jpg
-ss が -i の前に来るのは高速シーク形式。 -ss を -i より前に置くと FFmpeg はキーフレーム位置にスナップしてからデコードする。長尺ファイルでは劇的に速いが、指定タイムスタンプぴったりではなく最寄りのキーフレームに着地する。コンテンツサムネの用途なら問題ない — キーフレームは普通 0.5 秒間隔くらい。フレーム精度が必要なら -ss を -i の後に置く必要があるが、その場合は先頭から全部デコードするのでずっと遅い(fqmpeg はそのモードを露出していない)。
-q:v 2: JPEG クオリティスケールで 2 はほぼロスレス(スケールは 1–31、低いほど高画質)。サムネとしては合理的な最高画質 — ファイルは大きいが鮮明。ギャラリータイル用に小さいサムネが欲しいなら、ここで最高画質を作って resize で別途縮小する。
snapshot — 一定間隔で連番フレーム抽出
動画全体から N 秒ごとに 1 フレーム抜き出す。出力は連番(<stem>-snap-0001.jpg、0002.jpg、...)で、入力ファイルと同じディレクトリに保存される。
- ソース:
src/commands/snapshot.js - フィルタ:
fps=<1/interval> - 出力:
<入力dir>/<stem>-snap-%04d.<format>
| 引数 / オプション | デフォルト | 許可値 | 備考 |
|---|---|---|---|
<input> | 必須 | — | 入力動画 |
--interval <seconds> | 1 | 正の数 | キャプチャ間隔(--interval 5 = 5 秒に 1 枚) |
--format <fmt> | jpg | jpg, png | 画像フォーマット |
-o, --output <pattern> | <入力dir>/<stem>-snap-%04d.<format> | printf パターン | 上書き。%d か %0Nd を含むこと |
$ npx fqmpeg snapshot lecture.mp4 --dry-run
ffmpeg -i lecture.mp4 -vf fps=1 -q:v 2 lecture-snap-%04d.jpg
$ npx fqmpeg snapshot lecture.mp4 --interval 30 --format png --dry-run
ffmpeg -i lecture.mp4 -vf fps=0.03333333333333333 -q:v 2 lecture-snap-%04d.png
select= ではなく fps= を使う理由: fps フィルタは「出力レートを一定に保つ」処理として最もシンプル — FFmpeg がターゲットレートに合わせてフレームをドロップ/重複させる。fps=0.0333... は「30 秒に 1 フレーム」を意味し、FFmpeg は各 30 秒チック直近のフレームを選ぶ。
-q:v 2 は PNG にも付与されるが無視される — PNG の品質は圧縮レベルで決まり、q スケールには反応しない。意味があるのは JPEG だけ。
出力枚数の見積もり: 60 分の動画を --interval 30 で処理すると 120 ファイル(-snap-0001.jpg から -snap-0120.jpg)。同じファイルを --interval 1 で処理すると 3600 ファイル。ディスク容量に注意。
video-to-frames — 全フレーム(または fps 制限付き)を画像に
ソースのフレームレートでデフォルト動作するか、--fps を渡せばそのレートで間引く。フレーム単位レタッチや ML 学習データなど 全フレーム が必要な場合や、既知の毎秒サンプリングレートが必要な場合に使う。
- ソース:
src/commands/video-to-frames.js - フラグ:
-i <input>(+ オプションで-vf fps=<n>) - 出力:
./frame_%04d.<format>(カレントディレクトリ)
| 引数 / オプション | デフォルト | 許可値 | 備考 |
|---|---|---|---|
<input> | 必須 | — | 入力動画 |
--fps <n> | ソースレート(フィルタなし) | 正の数 | 毎秒 n フレームに間引く |
--format <fmt> | png | png, jpg | 画像フォーマット。PNG デフォルト(ロスレス)で ML / 編集向け |
-o, --output <pattern> | ./frame_%04d.<format> | printf パターン | 上書き |
$ npx fqmpeg video-to-frames input.mp4 --dry-run
ffmpeg -i input.mp4 frame_%04d.png
$ npx fqmpeg video-to-frames input.mp4 --fps 5 --format jpg --dry-run
ffmpeg -i input.mp4 -vf fps=5 frame_%04d.jpg
デフォルトは「全フレーム」。 30 fps × 60 秒なら 1800 枚の PNG — 数百 MB に簡単に達する。定期サンプルで足りるなら snapshot(こちらは妥当なデフォルト)か --fps を渡す。
出力は入力ディレクトリではなく cwd。 これは意図的で、フレームダンプは作業用フォルダに置きたいスクラッチデータであることが多く、動画のあるディレクトリを汚したくないという思想。ただし snapshot との振る舞いの違いになるので、入力の隣に置きたいなら -o 入力/dir/frame_%04d.png を渡すこと。
PNG vs JPG: 後段の画像処理(ロスレス、アルファ保持)には PNG が安全なデフォルト。JPG は 5–10 倍小さいがロッシー — プレビュー用途や ML 学習でモデルがダウンサンプリングする場合は十分。フォーマットフラグが拡張子を駆動し、FFmpeg はそれからコーデックを選ぶ。
count-frames — ffprobe でフレーム数を正確にカウント
動画ストリームの正確なフレーム数を返す。C12 で唯一 ffmpeg を使わない動詞 — ffprobe -count_frames を実行して整数 1 つを標準出力に書く。出力ファイルなし。
- ソース:
src/commands/count-frames.js - バイナリ:
ffprobe(ffmpegではない) - 出力: 標準出力に整数
| 引数 / オプション | 備考 |
|---|---|
<input> | 入力動画 |
$ npx fqmpeg count-frames input.mp4 --dry-run
ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of csv=p=0 input.mp4
$ npx fqmpeg count-frames input.mp4
1798
-count_frames はストリーム全体をデコードする。 正確だが遅い — 長尺ファイルでは count-frames が動画の全バイトを読む。ヘッダから duration × frame_rate を引いて推定したほうが速い(デコード不要):
ffprobe -v error -select_streams v:0 -show_entries stream=duration,r_frame_rate -of csv=p=0 input.mp4
これで duration と r_frame_rate が返る(例: 60.5,30000/1001)ので掛け算すれば(60.5 × 29.97 ≈ 1814 フレーム)— 近いが可変フレームレート(VFR)動画では完全一致しない。
正確なフレーム数が必要な場面: N 等分のフレーム分割を自動化する動画編集、フレームごとにインデックスが必要なポーズ推定ワークフロー、フレーム数そのものがフィンガープリントになる不正検知/フォレンジック解析など。普通のニーズならヘッダ推定で十分。
コンタクトシート・フィルムストリップ
thumbnail-grid — コンタクトシート(tile の別名)
複数フレームをグリッド配置したサムネ合成画像。DVD のチャプター選択画面みたいな見た目。サンプリングは tile と同じ時間ベース(1 秒に 1 フレーム)。「grid」で検索したユーザー向けにこの名前で残している。
- ソース:
src/commands/thumbnail-grid.js - フィルタ:
select='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)', scale=<W>:-1, tile=<C>x<R> - 出力:
<入力stem>-grid.jpg
| 引数 / オプション | デフォルト | 備考 |
|---|---|---|
<input> | 必須 | 入力動画 |
--cols <n> | 4 | グリッドの列数 |
--rows <n> | 4 | グリッドの行数 |
--width <n> | 320 | 各タイルの幅(px) |
-o, --output <path> | <入力stem>-grid.jpg | 上書き |
$ npx fqmpeg thumbnail-grid input.mp4 --dry-run
ffmpeg -i input.mp4 -frames:v 1 -vf select='isnan(prev_selected_t)+gte(t-prev_selected_t\,1)',scale=320:-1,tile=4x4 -q:v 2 input-grid.jpg
1 秒に 1 フレーム、上限は cols × rows。 デフォルト 4×4 = 16 タイルなら最初の 16 秒分をカバーする。動画がもっと長い場合は --cols/--rows を増やして cols × rows ≥ 動画秒数 にする、または tile 動詞(同じ挙動)を使って -o でファイル名を制御する。prev_selected_t の詳しい仕組みは下の tile セクション参照。
tile と同じアルゴリズム、違うのはデフォルト出力名だけ。 「grid」という単語が思い浮かんだら thumbnail-grid、「tile」「コンタクトシート」が思い浮かんだら tile を使う。tile のデフォルト名は寸法を含む(-tile4x4.jpg)ので、--cols/--rows を変えながら再実行するときに便利。
thumbnail-strip — フィルムストリップ(時間サンプリング)
thumbnail-grid の 1 列バリアント。同じ時間ベースサンプリングで、rows=1 固定、幅ではなく高さをパラメータに取る。
- ソース:
src/commands/thumbnail-strip.js - フィルタ:
select='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)', scale=-1:<H>, tile=<N>x1 - 出力:
<入力stem>-strip.jpg
| 引数 / オプション | デフォルト | 備考 |
|---|---|---|
<input> | 必須 | 入力動画 |
--frames <n> | 10 | ストリップ内のフレーム数 |
--height <n> | 120 | 各フレームの高さ(px) |
-o, --output <path> | <入力stem>-strip.jpg | 上書き |
$ npx fqmpeg thumbnail-strip input.mp4 --dry-run
ffmpeg -i input.mp4 -frames:v 1 -vf select='isnan(prev_selected_t)+gte(t-prev_selected_t\,1)',scale=-1:120,tile=10x1 -q:v 2 input-strip.jpg
1 秒に 1 フレーム、上限は --frames。 デフォルト --frames 10 なら最初の 10 秒分のストリップになる。もっと長いランタイムを 1 枚でカバーしたいなら --frames を増やして 動画秒数 以上にする、または snapshot の出力を tile/montage(ImageMagick)で比例配分してつなげる。
フィルムストリップの用途: 動画エディタのスクラバプレビュー、動画プレイヤーのホバープレビュー(YouTube の WebVTT サムネイルストリップ)、SNS 風の「タイムラインスクロール」アート。
tile — コンタクトシート(時間サンプリング)
時間ベース(prev_selected_t 経由)で 1 秒に 1 フレーム選ぶ。thumbnail-grid と同じアルゴリズム。違いはデフォルト出力名(-tile<C>x<R>.jpg)に寸法が入る点。
- ソース:
src/commands/tile.js - フィルタ:
select='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)', scale=<W>:-1, tile=<C>x<R> - 出力:
<入力stem>-tile<C>x<R>.jpg
| 引数 / オプション | デフォルト | 備考 |
|---|---|---|
<input> | 必須 | 入力動画 |
--cols <n> | 4 | グリッドの列数 |
--rows <n> | 4 | グリッドの行数 |
--width <n> | 320 | 各タイルの幅(px) |
-o, --output <path> | <入力stem>-tile<C>x<R>.jpg | 上書き(デフォルト名に寸法が入る) |
$ npx fqmpeg tile input.mp4 --dry-run
ffmpeg -i input.mp4 -frames:v 1 -vf select='isnan(prev_selected_t)+gte(t-prev_selected_t\,1)',scale=320:-1,tile=4x4 -q:v 2 input-tile4x4.jpg
prev_selected_t 述語の意味:
isnan(prev_selected_t)は最初の 1 フレームで真(まだ選択履歴がない →prev_selected_tは未定義 → NaN)。gte(t - prev_selected_t, 1)は前回選択から 1 秒以上経った時に真。
この 2 つの和が select フィルタをゲートし、1 秒に 1 回選択を発生させる。デフォルト 4×4 = 16 タイルなら最初の 16 秒をカバーする。動画がもっと長い場合は tile=4x4 の上限で 17 番目以降は捨てられるので、結局最初の 16 秒分。全尺に分散したコンタクトシートが欲しいなら --cols / --rows を増やして cols × rows ≥ 動画秒数 にする、または生の FFmpeg で select='gte(t, X)' を比例配分で書く。
tile と thumbnail-grid が両方存在する理由: 発見性 — 「grid」で検索しても「tile」で検索してもここに辿り着くようにしている。フィルタとサンプリングは同じ、違うのはデフォルト出力ファイル名だけ(tile は -tile4x4.jpg、thumbnail-grid は -grid.jpg)。思い浮かんだ方を使えば良い。実行間でファイル名を固定したい場合は -o で指定。
フレーム ↔ 動画再構築
frames-to-video — 画像列 → 動画
video-to-frames の逆。printf スタイルの連番(またはグロブパターン)の静止画列から 1 本の動画ファイルを作る。
- ソース:
src/commands/frames-to-video.js - フラグ:
-framerate <fps> -i <pattern> -c:v <codec> -pix_fmt yuv420p - 出力:
<pattern-base>-video.mp4
| 引数 / オプション | デフォルト | 備考 |
|---|---|---|
<pattern> | 必須 | printf パターン(frame_%04d.png)or シェルグロブ(img_*.jpg) |
--fps <n> | 30 | 出力フレームレート |
--codec <name> | libx264 | 動画コーデック |
-o, --output <path> | <pattern-base>-video.mp4 | 上書き(デフォルトはパターンから %d と拡張子を除いた残り) |
$ npx fqmpeg frames-to-video frame_%04d.png --dry-run
ffmpeg -framerate 30 -i frame_%04d.png -c:v libx264 -pix_fmt yuv420p frame-video.mp4
$ npx fqmpeg frames-to-video 'img_*.jpg' --fps 60 --dry-run
ffmpeg -framerate 60 -i img_*.jpg -c:v libx264 -pix_fmt yuv420p img_*-video.mp4
-framerate vs -r: -framerate は入力の画像列レート(毎秒何枚消費するか)を設定する、出力フレームレートではない。画像→動画のほぼすべての用途でこれが正解 — デフォルトでは出力レートが入力レートにマッチする。出力レートを変えたい(補間やフレーム複製など)なら -r を入力の後に追加する。
互換性のための yuv420p: 生の画像ファイル(特に PNG)は yuva420p や rgb24 にデコードされることがあり、これを MP4 コンテナに入れると拒否するプレイヤーがある。yuv420p を強制すれば QuickTime、モバイルブラウザ、組み込みプレイヤーで再生できる。アーカイブ用に高品質(10-bit、フル RGB)を狙うなら --codec libx264rgb か生の FFmpeg に降りる。
パターンマッチング: printf パターン(frame_%04d.png)は連番が 00001 始まり(-start_number 0 で 00000 始まり)の整列が必要。グロブパターン(img_*.jpg)も動くが、シェルが展開する前に渡す必要がある — スクリプトではクオートで囲む。混在拡張子のグロブ(.jpg と .png 同居)は image2 デマクサが同一フォーマットを期待するので動かない。
slideshow — 複数画像 → 各画像に表示時間付き動画
静止画のリストから、各画像を設定可能な時間表示する動画を作る。内部で FFmpeg の concat デマクサを使い、自動生成リストファイル(終了時に削除)で結合する。
- ソース:
src/commands/slideshow.js - フラグ:
-f concat -safe 0 -i <listfile> -vf fps=<n>,format=yuv420p -c:v libx264 -pix_fmt yuv420p - 出力:
<入力dir>/slideshow.mp4
| 引数 / オプション | デフォルト | 備考 |
|---|---|---|
<images...> | 必須(≥2) | 順番に 2 枚以上の画像パス |
--duration <sec> | 3 | 1 枚あたりの表示秒数(全画像共通) |
--fps <n> | 30 | 出力フレームレート |
-o, --output <path> | <入力dir>/slideshow.mp4 | 上書き |
$ npx fqmpeg slideshow img1.jpg img2.jpg img3.jpg --dry-run
# Image list (auto-generated):
# file '/abs/path/img1.jpg'
# duration 3
# file '/abs/path/img2.jpg'
# duration 3
# file '/abs/path/img3.jpg'
# duration 3
# file '/abs/path/img3.jpg'
ffmpeg -f concat -safe 0 -i imagelist.txt -vf fps=30,format=yuv420p -c:v libx264 -pix_fmt yuv420p slideshow.mp4
エントリ単位 duration の concat デマクサ: リストファイルの文法 file '<path>' \n duration <sec> で、静止画を正確な時間でつなぐ。クリーンアップは自動(fqmpeg は process.on("exit") でタイムスタンプ付きリストファイルを削除)。
最後の画像が 2 回書かれている理由: concat デマクサの duration 行は「次のファイルへの遷移時刻」を指定するため、そのままだと最後の画像の表示時間がゼロになる。fqmpeg は最後の画像を duration 行なしのトレーリングエントリとしてもう 1 回書くことで、最後の画像も --duration 秒間表示される。総尺は 画像枚数 × duration 秒で option の約束通りになる。
リストファイルは絶対パス: fqmpeg は書く前に各画像を絶対パスに解決するので、どこから起動しても concat が動く。シングルクォートで内部のクォートはエスケープ('\\'')するので、アポストロフィを含むパス(O'Brien.jpg など)も通る。
画像 1 枚のケース: コマンドは ≥ 2 枚を要求し、1 枚だとエラーで終わる。「任意長の動画として 1 枚の静止画を伸ばしたい」場合は生の FFmpeg を使う: ffmpeg -loop 1 -i img.jpg -t 10 -c:v libx264 -pix_fmt yuv420p out.mp4。
ビルトインの遷移効果なし — ハードカットのみ。 slideshow は FFmpeg の concat デマクサで画像を端から端へ繋ぐだけで、クロスフェードは出さない。フェードを入れたいなら、生 FFmpeg の xfade フィルタ(filter_complex で各ペアを xfade=transition=fade:duration=1:offset=… で連鎖)に降りる、または slideshow 実行後に別パスでフェードを足す。xfade 連鎖をビルトイン化すると concat のシンプルな流れを画像ごとの segment chain に置き換えることになり、「quick」の表面から外れるのでサポートしない。
解析・合成
scenes — シーン切れ検出して自動分割
scene メタデータ変数(フレームごとの差分スコア、0–1)でシーン変化を検出し、segmenter で各シーンを別ファイルに書く。
- ソース:
src/commands/scenes.js - フィルタ:
select='gt(scene,<threshold>)',setpts=N/FRAME_RATE/TB+-f segment -reset_timestamps 1 - 出力:
<入力dir>/<stem>-scene%03d<ext>
| 引数 / オプション | デフォルト | 範囲 | 備考 |
|---|---|---|---|
<input> | 必須 | — | 入力動画 |
--threshold <n> | 0.3 | 0.0–1.0 | 低いほど敏感(カット数増) |
-o, --output <pattern> | <入力dir>/<stem>-scene%03d<ext> | printf パターン | 上書き |
$ npx fqmpeg scenes movie.mp4 --dry-run
ffmpeg -i movie.mp4 -filter_complex select='gt(scene,0.3)',setpts=N/FRAME_RATE/TB -f segment -reset_timestamps 1 movie-scene%03d.mp4
scene メタデータ変数: FFmpeg が前フレームとの色ヒストグラム差分から 0–1 のスコアをフレームごとに計算する。閾値超過がカットとみなされる。典型値:
0.1–0.2: 攻撃的(ディゾルブやクロスフェードも拾う、モーション誤検知多い)0.3: バランス(デフォルト — ナラティブ動画のハードカットのほとんどを拾う)0.4–0.5: 保守的(明確な遷移のみ。正当なカットの一部を見落とす)0.7+: 最も激しいカットだけ(脚本もののチャプター境界)
-reset_timestamps 1: 各出力セグメントが PTS 0 から始まる(ソースのタイムラインの続きではない)。これでセグメントが個別に再生可能になる。
ストリームコピー不可。 フィルタが全フレームに触るので出力は再エンコードされる。再エンコなしで高速にカットリストが欲しいなら、生の FFmpeg で検出後に -ss/-to を使う — まず scenes --dry-run で閾値を確認、ffprobe でタイムスタンプを抽出、ffmpeg -ss <t1> -to <t2> -c copy でセグメント分割。
preview — 短いハイライト動画生成
ソース全体に均等分散させた N 個の短いクリップをサンプリングし、それらを連結して短いプレビュー動画を作る — 「SNS 用予告編」スタイル。
- ソース:
src/commands/preview.js - フィルタ: 均等間隔の位置に
<clip-duration>秒のクリップを N 個 select、-t <clips × clip-duration>で連結 - 出力:
<入力stem>-preview.<ext>
| 引数 / オプション | デフォルト | 備考 |
|---|---|---|
<input> | 必須 | 入力動画 |
--clips <n> | 5 | サンプルクリップ数 |
--clip-duration <sec> | 2 | 各クリップの秒数 |
-o, --output <path> | <入力stem>-preview.<ext> | 上書き |
$ npx fqmpeg preview input.mp4 --dry-run
# Note: could not probe input.mp4 for duration. Using placeholder total=60s.
# Run on a real file (or after creating input.mp4) to get exact clip offsets.
ffmpeg -i input.mp4 -vf select='between(t,0,2)+between(t,12,14)+between(t,24,26)+between(t,36,38)+between(t,48,50)',setpts=N/FRAME_RATE/TB -af aselect='between(t,0,2)+between(t,12,14)+between(t,24,26)+between(t,36,38)+between(t,48,50)',asetpts=N/SR/TB -t 10 input-preview.mp4
出力長は決定的: clips × clip-duration 秒、ソース長に関係なく。デフォルト(5 × 2 = 10 秒)は 10 秒のハイライト — Twitter / Instagram に短く、30 分のトークの要旨を伝えるには十分な長さ。
ソース全体に均等分散: preview はまず ffprobe でソースの動画長 T を読み、--clips 個のセグメントを均等間隔で選ぶ — クリップ 1 は 0 × T/clips、クリップ 2 は 1 × T/clips、...、クリップ N は (N−1) × T/clips から始まり、各 clip-duration 秒。60 分動画のデフォルトなら、0 分、12 分、24 分、36 分、48 分の各 2 秒。上の dry-run 出力は input.mp4 が実在しないので placeholder T = 60 で計算しているだけ、実ファイルなら実値の offset が入る。
クリップ間に音声フェードなし。 出力は各サンプリングクリップ間でハードカットなので、音楽中心のコンテンツでは音のポップが出やすい。SNS 投稿レベルの仕上がりが欲しいなら、trim + crossfade + audio-fade を手で組み合わせてクロスフェードと BGM を入れたほうがいい。
compare — 横並び(または縦並び)の前後比較
2 つの動画を横(デフォルト)または縦に並べて 1 本の動画にする。「このフィルタでこれだけ良くなった」を見せる定番のデモ手法。
- ソース:
src/commands/compare.js - フィルタ(horizontal):
[0:v]scale=iw/2:ih[left];[1:v]scale=iw/2:ih[right];[left][right]hstack - フィルタ(vertical):
[0:v]scale=iw:ih/2[top];[1:v]scale=iw:ih/2[bottom];[top][bottom]vstack - 出力:
<入力1stem>-compare.<ext>
| 引数 / オプション | デフォルト | 許可値 | 備考 |
|---|---|---|---|
<input1> | 必須 | — | 左 / 上の動画 |
<input2> | 必須 | — | 右 / 下の動画 |
--direction <dir> | horizontal | horizontal, vertical | レイアウト |
-o, --output <path> | <入力1stem>-compare.<ext> | — | 上書き |
$ npx fqmpeg compare before.mp4 after.mp4 --dry-run
ffmpeg -i before.mp4 -i after.mp4 -filter_complex [0:v]scale=iw/2:ih[left];[1:v]scale=iw/2:ih[right];[left][right]hstack -c:a copy before-compare.mp4
$ npx fqmpeg compare original.mp4 stabilized.mp4 --direction vertical --dry-run
ffmpeg -i original.mp4 -i stabilized.mp4 -filter_complex [0:v]scale=iw:ih/2[top];[1:v]scale=iw:ih/2[bottom];[top][bottom]vstack -c:a copy original-compare.mp4
各入力は連結前に半分にスケール: 出力キャンバスは元の幅(horizontal)または高さ(vertical)を保ち、各入力は半分のサイズにスケールされる。全体のアスペクト比は維持される。2 つの入力が解像度違いだと scale が半サイズに正規化する — 内容に歪みが出る可能性。仕上げ用には 2 入力をあらかじめ同解像度・同尺にしておくのが望ましい。
音声は入力 1 からコピー。 -c:a copy で最初の入力の音声をストリームコピーし、入力 2 の音声は破棄。これは通常の前後比較フレーム(映像を比較しつつ 1 本の音声を残す)に合っている。
尺の不一致: 2 動画の尺が違うと、出力は短い方が終わった時点で終わる(FFmpeg の hstack/vstack のデフォルト動作)。長い方の残りフレームは切り捨てられる。厳密な整列が必要なら事前に両方を同じ尺にトリムすること。
ビルトインのラベル機能なし。 「Left」/「Right」「Before」/「After」のようなテキストオーバーレイが欲しいなら、まず --dry-run でフィルタを出してから生の FFmpeg に降り、各 scale ステップに drawtext を組み込む — 具体テンプレートは下の Recipe 2 参照。--label option を入れると fontfile 依存(drawtext は libfreetype + フォントパスが必要)の表面が増えるので、quick の範囲外。
実用ユースケース
レシピ 1: YouTube サムネ作成ワークフロー
完成した 12 分の動画があり、カスタムサムネを作りたい。目標: ベストなフレームを選ぶ → YouTube 仕様(1280×720)にリサイズ → コンタクトシートで選択を確認。
# ステップ 1: コンタクトシートでベストな瞬間を選ぶ(時間ベース)
npx fqmpeg tile video.mp4 --cols 6 --rows 8 --width 480
# → video-tile6x8.jpg、最初の 48 秒分の 48 フレーム
# 12 分(720 秒)動画の全尺カバーには cols×rows ≥ 720 が必要なので、
# 30×24 グリッド(720 タイル)。幅 200 なら 6000×~3375 px のシート。
npx fqmpeg tile video.mp4 --cols 30 --rows 24 --width 200
# ステップ 2: 選んだフレームをぴったりのタイムスタンプで抽出
npx fqmpeg thumbnail video.mp4 -s 374 -o thumbnail-raw.jpg
# ステップ 3: YouTube 推奨サイズにリサイズ
npx fqmpeg resize thumbnail-raw.jpg 1280x720 -o thumbnail-final.jpg
tile ステップが遅い部分 — ソースを全部デコードする必要がある。タイムスタンプが決まれば、thumbnail はキーフレーム整列 -ss のおかげでほぼ瞬時。
レシピ 2: 防犯カメラのダンプからタイムラプス作成
防犯カメラから 86,400 枚の JPEG(1 秒に 1 枚、24 時間分)が出てきた。これを 60 秒、30 fps のタイムラプス動画にしたい。
# 入力 86400 枚を 30 fps で出すと 86400/30 = 2880 秒 = 48 分。
# 60 秒、30 fps に圧縮するには 1800 フレーム出力が必要。
# 入力フレームから 48 枚に 1 枚サンプリング: 86400 / 1800 = 48。
# ステップ 1: 48 枚ごとに 1 枚をシンボリックリンクで連番化
i=1
ls *.jpg | awk 'NR%48==1' | while read f; do
printf -v new "frame_%04d.jpg" "$i"
ln -s "$(realpath "$f")" "$new"
((i++))
done
# ステップ 2: 30 fps タイムラプスとして結合
npx fqmpeg frames-to-video frame_%04d.jpg --fps 30
# → frame-video.mp4
シンボリックリンクを使わない別解: slideshow の --duration 0.0333(1/30 秒/枚)を使う — ただし slideshow は concat デマクサで再エンコードするので frames-to-video の直接 -i pattern より遅い。この規模のデータセットなら frames-to-video が正解、slideshow は数十枚程度で各画像表示時間を指定したい用途。
レシピ 3: ポートフォリオ向けのフィルタ前後比較
stabilize の効果を手ぶれドローン映像で示したい。ポートフォリオ用に横並び比較動画をラベル付きで作る:
# ステップ 1: 元映像を手ぶれ補正
npx fqmpeg stabilize drone-raw.mp4 -o drone-stable.mp4
# ステップ 2: 横並び比較
npx fqmpeg compare drone-raw.mp4 drone-stable.mp4 \
-o drone-comparison.mp4
# ステップ 3: ケーススタディの表紙用にサムネを 1 枚抽出
npx fqmpeg thumbnail drone-comparison.mp4 -s 3 -o drone-cover.jpg
compare 自体にラベル機能はない — しっかりしたケーススタディなら、--dry-run でフィルタを出して生の FFmpeg で再レンダリングし、各 scale ステップに drawtext を組み込む:
ffmpeg -i drone-raw.mp4 -i drone-stable.mp4 -filter_complex \
"[0:v]scale=iw/2:ih,drawtext=text='Original':x=20:y=20:fontsize=36:fontcolor=white:box=1:boxcolor=black@0.5[left];[1:v]scale=iw/2:ih,drawtext=text='Stabilized':x=20:y=20:fontsize=36:fontcolor=white:box=1:boxcolor=black@0.5[right];[left][right]hstack" \
-c:a copy drone-portfolio.mp4
よくある質問
thumbnail-grid と tile のどちらを使うべき?
どちらでも — 別名関係。両方とも同じ時間ベースサンプリング(select='isnan(prev_selected_t)+gte(t-prev_selected_t, 1)')で、同じ --cols/--rows/--width なら同じコンタクトシートを生成する。違いはデフォルト出力名だけ:thumbnail-grid は <stem>-grid.jpg、tile は <stem>-tile<C>x<R>.jpg。グリッドサイズをファイル名に残したいなら tile、固定の -grid.jpg のほうが扱いやすいなら thumbnail-grid。どちらにせよ -o で上書き可能。
snapshot と video-to-frames の違いは?
両方とも定期的にスチル抽出するが、デフォルトと出力先が違う:
snapshotは 1 秒に 1 フレーム、JPEG 形式、入力と同じディレクトリに出力(<入力dir>/<stem>-snap-%04d.jpg)。video-to-framesはソースレートで全フレーム、PNG 形式、カレントディレクトリに出力(./frame_%04d.png)。
「動画の隣に時々参照用のスチル」なら snapshot が適切。「ML 編集用に全フレーム」なら video-to-frames。どちらも別の場所に書きたければ -o を渡す。
count-frames が長い動画で遅いのは?
-count_frames がストリーム全体をデコードするから。このフラグは ffprobe に「実際にパケットを全部歩いてデコード成功フレームを数えろ」と指示する — 正確性のために必要(特に VFR ファイル)だが、フル デコードパスのコストがかかる。4 時間 4K ソースだと数分かかることもある。推定で良ければヘッダから duration と r_frame_rate を取って掛け算するほうが速い(デコード不要):
ffprobe -v error -select_streams v:0 -show_entries stream=duration,r_frame_rate -of csv=p=0 input.mp4
frames-to-video で img_*.jpg のようなグロブは使える?
使えるが、クオートが必要。FFmpeg の image2 デマクサは printf パターン(frame_%04d.png)とシェルグロブ('img_*.jpg')の両方を受ける。クオートなしのグロブはシェルが先に展開してしまうので普通はコマンドが壊れる — シングルクォートで囲んでリテラルパターンを FFmpeg に渡す。グロブモードは全ファイルが同じ拡張子である必要(.jpg と .png 混在は不可)。
tile で長い動画の最初の 16 秒しかカバーされないのはなぜ?
デフォルトの 4×4 = 16 タイル × 1 秒間隔の時間サンプリングが、ちょうど 16 秒分を生成するから。長い動画全体をカバーしたいなら、--cols と --rows を増やして cols × rows ≥ 動画秒数 にする。5 分(300 秒)動画を均等にサンプリングするには約 17×18 グリッド(306 タイル)が必要。長尺動画でもっと荒くサンプリングしたいなら生の FFmpeg に降りて select 述語を調整する — 例: select='gte(t - prev_selected_t, 60)' で 1 分に 1 フレーム。
frames-to-video と slideshow の違いは?
frames-to-video は連番(またはグロブ)の画像列(frame_%04d.png)を消費し、単一のフレームレートで動画にする — 入力画像 1 枚 = 出力 1 フレーム、均一。slideshow は明示的な画像リスト(img1.jpg img2.jpg img3.jpg)を消費し、画像ごとの表示時間(各画像 N 秒)を設定できる、画像間はハードカット(ビルトインのクロスフェードはなし — フェードは生 FFmpeg の xfade で追加、slideshow セクション参照)。タイムラプス、ML 出力の再構築、レンダリングからの画像列には frames-to-video。写真プレゼンなど 1 枚を数秒ずつ見せたい用途には slideshow。
典型的なナラティブ動画で scenes の閾値はいくつにすべき?
デフォルト 0.3 から始める。カット検出が少なすぎる(ハード遷移を見落とす)なら 0.2 か 0.15 に下げる。多すぎる(カメラ動きやディゾルブで誤検知)なら 0.4 か 0.5 に上げる。閾値の感度はコンテンツ次第 — モーションが速い MV はショット内動きを無視するのに高めの閾値(0.5+)が要る。トーキングヘッドのインタビューは変化が本当のカットだけなので低め(0.2)でも使える。
compare で両方の音声を同時に出せる?
出せない。実装は -c:a copy で入力 1 の音声ストリームしか取らない。入力 2 の音声は破棄される。通常のワークフロー(前後映像比較で 1 本の音声)に合っている。デュアル音声比較(例: 2 つの音声ミックスを並べて比較)には生の FFmpeg で amerge か amix:
ffmpeg -i a.mp4 -i b.mp4 -filter_complex \
"[0:v]scale=iw/2:ih[L];[1:v]scale=iw/2:ih[R];[L][R]hstack;[0:a][1:a]amerge=inputs=2[a]" \
-map "[a]" -ac 2 compare-dual-audio.mp4
まとめ
C12 の 12 動詞は動画 ↔ 個別フレームの往復をカバーする:
thumbnail,snapshot,video-to-frames,count-framesで単発・定期スチル(-q:v 2が JPEG クオリティのデフォルト、count-framesだけがクラスタ内で唯一のffprobe動詞)thumbnail-grid,thumbnail-strip,tileでコンタクトシート・フィルムストリップ(3 つとも時間ベースサンプリング — 1 秒に 1 フレーム — で、--cols × --rows(grid/tile)または--frames(strip)が上限)frames-to-video,slideshowで画像→動画再構築(frames-to-videoは連番を一定レートで結合、slideshowは画像ごとの表示時間でハードカット連結 — フェードが必要なら生 FFmpeg のxfadeで追加)scenes,preview,compareで解析・合成(scenesは検出カットで分割、previewは均等サンプリングのハイライト動画、compareは前後比較のスタック)
各動詞は --dry-run で内部の FFmpeg 起動行を出力するので、簡略化された CLI で足りない場合(独自のコンタクトシートサンプリング、デュアル音声比較、キーフレーム整列ではないフレーム精度シーク)は、フィルタをコピーして編集し、生の FFmpeg を直接呼ぶ。fqmpeg の全体マップは fqmpeg complete guide を参照。