動画の最終フレームを画像として保存したい。サムネイル生成や品質確認で頻出する処理だけど、素朴にやると動画全体をデコードすることになる。2時間の動画なら17万フレーム以上を処理して、最後の1枚だけ取り出すことになる。答えは ffmpeg -sseof -1 -i input.mp4 -update 1 output.png。末尾にシークして上書きメカニズムで最終フレームだけを取得できる。
サムネイル生成パイプラインでは定番の手法で、大量のファイルをバッチ処理しても一瞬で終わる。この記事では、なぜこのコマンドが動くのか内部挙動まで掘り下げて解説する。
ffmpeg -sseof -1 -i "input.mp4" -update 1 "output.png"
コマンド全文
Windows (PowerShell)
# パス定義
$InputVideo = "input.mp4"
$OutputImage = "output.png"
# -sseof -1 : ファイル末尾から1秒前にシーク
# -update 1 : フレームごとに同一ファイルを上書き(最後の1枚が残る)
ffmpeg -y -sseof -1 -i "$InputVideo" -update 1 "$OutputImage"
macOS / Linux (Bash)
#!/bin/bash
INPUT="input.mp4"
OUTPUT="output.png"
# -sseof -1 : ファイル末尾から1秒前にシーク(Input Seeking)
# -update 1 : image2 muxerに対し、単一ファイルへの上書きを指示
ffmpeg -y -sseof -1 -i "$INPUT" -update 1 "$OUTPUT"
内部挙動の解説
見た目はシンプルだけど、FFmpegのパイプライン挙動を巧みに利用している。各フラグが何をやっているのか、一つずつ見ていこう。
1. -sseof -1:末尾からの入力シーク
FFmpegのシークオプションは、コマンド内での配置位置によって挙動が変わる。
-iの前に配置(入力シーク): FFmpegはデコードを開始する前に、ファイル内の指定位置にジャンプする。デマクサーレベルの操作なので、指定位置より前のデータはデコーダーに渡されない-sseof -1: ファイル末尾(EOF)からの相対位置でシーク先を指定する。-1は「末尾から1秒前」という意味
結果として、2時間の動画でも読み込むのは最後の約1秒だけ。処理時間は動画の長さに依存せず、数ミリ秒〜数百ミリ秒で完了する。
2. -update 1:上書きによる最終フレーム取得
通常、FFmpegで動画を静止画に出力すると、image2 muxerは frame001.png、frame002.png のような連番を期待する。固定ファイル名を指定するとエラーになるか、最初の1枚で止まる。
-update 1 はこの挙動を変える:
- muxerに「新しいフレームが来るたびに既存ファイルを上書きせよ」と指示
- FFmpegは末尾約1秒のデータをデコードし、フレームを順次生成する
- フレームが生成されるたびに
output.pngが書き換わる - ストリームが終了(EOF)した時点で最後に書き込まれた画像、つまり 時系列的に最も遅いフレーム がファイルとして残る
これが「最終フレーム」を確実に取得できる仕組みだ。
3. キーフレームスナップの挙動
-sseof による入力シークは、指定時間ぴったりには着地しない。指定位置の直前にあるキーフレーム(Iフレーム)にスナップする。動画のGOP(Group of Pictures)構造によっては、1秒ではなく2〜3秒前から読み込みが始まることもある。
ただし、出力結果には影響しない。-update 1 が全フレームを上書きしながらEOFまで走るので、最終フレームは確実に取得できる。キーフレーム間隔やGOP構造に興味があるなら、コーデック比較の記事で詳しく解説している。
4. 出力形式:PNG vs JPG vs WebP
PNGは可逆圧縮なので画質オプション不要。JPGの場合は -q:v で品質を指定する。
# JPG出力(-q:v 2が最高品質、31が最低)
ffmpeg -y -sseof -1 -i "$INPUT" -update 1 -q:v 2 "output.jpg"
# WebP出力(Web用途なら軽量でおすすめ)
ffmpeg -y -sseof -1 -i "$INPUT" -update 1 -quality 80 "output.webp"
従来手法との比較
| 手法 | 処理内容 | 問題点 |
|---|---|---|
| 全フレームデコード(非推奨) | 先頭から全デコードして最終フレーム出力 | 動画の長さに比例して処理時間が増大 |
| ffprobeでフレーム数計算 → select | フレーム数を数えてからシーク | 二度手間。ffprobe自体が重い |
-ss + 再生時間から計算 | メタデータから再生時間を読んでシーク | メタデータ読み込みが必要。VFR動画で不安定 |
-sseof -1 -update 1 | EOF直前にシーク → 上書きで最終フレーム取得 | 動画の長さに依存しないO(1)に近い速度 |
30分・30fpsの動画で比較すると、全デコード方式は54,000フレームを処理する。-sseof 方式は多くて数百フレーム。この差は動画が長くなるほど広がる。
バッチ処理スクリプト
Bash:ディレクトリ内の全MP4を処理
大量の動画を処理するなら、もっと高度なアプローチもある。FFmpeg + Python バッチ自動化ガイドでは並列処理やエラーハンドリングを含むパイプライン構築を解説している。
#!/bin/bash
INPUT_DIR="./videos"
OUTPUT_DIR="./thumbnails"
mkdir -p "$OUTPUT_DIR"
for video in "$INPUT_DIR"/*.mp4; do
filename=$(basename "$video" .mp4)
output="$OUTPUT_DIR/${filename}_last_frame.png"
ffmpeg -y -sseof -1 -i "$video" -update 1 "$output"
echo "処理完了: $filename"
done
echo "全件完了。出力先: $OUTPUT_DIR"
PowerShell:エラーハンドリング付きバッチ処理
$InputDir = ".\videos"
$OutputDir = ".\thumbnails"
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
$videos = Get-ChildItem -Path $InputDir -Filter "*.mp4"
$total = $videos.Count
$count = 0
foreach ($video in $videos) {
$count++
$filename = $video.BaseName
$output = Join-Path $OutputDir "${filename}_last_frame.png"
Write-Progress -Activity "最終フレーム抽出中" `
-Status "$filename ($count/$total)" `
-PercentComplete (($count / $total) * 100)
ffmpeg -y -sseof -1 -i $video.FullName -update 1 $output 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host "OK: $filename"
} else {
Write-Host "失敗: $filename" -ForegroundColor Red
}
}
Write-Host "完了。$count 件処理しました。"
実務で役立つTips
エッジケースの扱い
1秒未満の動画の場合: -sseof -1 は問題なく動く。オフセットが動画の長さを超えると先頭にクランプされるため、全フレームをデコードして -update 1 で最終フレームが残る。
動画ストリームがないファイル(音声のみ): FFmpegがエラーを返す。混在ディレクトリを処理するなら、先にffprobeでフィルタリングしよう。
ffprobe -v error -select_streams v:0 -show_entries stream=codec_type -of csv=p=0 "input.mp4"
透過フレーム(WebM/VP9のアルファチャンネル): PNG出力を使えばアルファチャンネルが保持される。JPGは透過をサポートしていない。
他のFFmpeg操作との組み合わせ
最終フレーム抽出に加えて、末尾の数秒を無劣化カットしたり、動画を圧縮したりすることもあるだろう。それぞれ別のFFmpegコマンドとして実行するのが確実だ。
リモートサーバーでバッチ処理を回すなら、FFmpeg VPSエンコードサーバー構築ガイドも参考にしてほしい。
FAQ
-sseofはすべての動画形式で使える?
シークに対応している形式なら使える。MP4、MKV、MOV、WebM、AVIなど一般的なコンテナ形式はすべてOK。シークインデックスを持たない形式(生のH.264ストリームなど)では、FFmpegが先頭からスキャンするため高速化の恩恵がなくなる。生ストリームはまずコンテナに格納してから使おう。
1秒未満の動画ではどうなる?
エラーにはならない。FFmpegがシーク位置をファイル先頭にクランプするため、全フレームがデコードされ、-update 1 のメカニズムで最終フレームが正しく出力される。
WebP形式で出力できる?
できる。出力ファイルの拡張子を変えるだけ:ffmpeg -y -sseof -1 -i input.mp4 -update 1 -quality 80 output.webp。WebPはPNGより軽量で、Webサムネイル用途に最適だ。
最後から2番目のフレームを取得するには?
直接指定するフラグはない。実用的には末尾の数フレームを連番で出力し、目的のフレームを取る方法がある:ffmpeg -y -sseof -1 -i input.mp4 -frames:v 2 "frame_%02d.png" — frame_01.png がおおよそ最後から2番目のフレームになる。
VFR(可変フレームレート)の動画でも動く?
動く。-sseof はフレーム数ではなくタイムスタンプベースでデマクサーレベルのシークを行う。VFR動画はフレーム間隔が不規則だが、EOF前1秒にシークしてフォワードデコードする挙動に影響はない。
GPU加速は効果ある?
ほぼない。このコマンドのボトルネックはI/O(シークと少量のデータ読み込み)であり、デコード処理ではない。NVENC/QSVのようなGPUデコーダーは長時間のデコードで威力を発揮するが、数フレームだけならGPU初期化のオーバーヘッドの方が大きい。
毎日数千本の動画から最終フレームを抽出するには?
1本あたりの処理コストがミリ秒単位なので、シンプルなbashループで十分対応できる。ログ出力やリトライ、並列処理が必要なら、Python バッチ自動化ガイドを参照してほしい。
-sseofと-ssの違いは?
-ss はファイル先頭からのオフセットで正の値のみ受け付ける。-sseof はファイル末尾からの相対位置で指定する。FFmpeg 2.8以降で追加された機能で、内部的には同じシークメカニズムを使うが、基準点が異なる。
まとめ
動画の最終フレームを高速に取得するポイントは、FFmpegの2つのオプションの組み合わせにある:
-sseof -1はファイル末尾付近に読み込みポインタを配置し、それ以前のデータはデコードしない-update 1はFFmpegが末尾のフレームをデコードする際、各フレームで前のものを上書きし続ける。結果として、最後に書き込まれた真の最終フレームが出力ファイルとして残る
この手法は動画の長さに依存しない定時間で処理が完了する。数千本のバッチ処理では、全デコード方式との差が数時間単位になることもある。
FFmpegの基本操作からおさらいしたい場合はFFmpegの使い方ガイドを、本格的な動画処理パイプラインの構築にはPython バッチ自動化ガイドを参考にしてほしい。