32blogby Studio Mitsu

FFmpegで動画の最終フレームを高速に切り出す方法

動画全体をデコードせず最終フレームだけを瞬時に抽出する方法を解説。-sseof と -update 1 の内部挙動、バッチ処理スクリプトまで網羅。

by omitsu14 min read
FFmpegShellPowerShellthumbnailframe extractionbatch processing
目次

動画の最終フレームを画像として保存したい。サムネイル生成や品質確認で頻出する処理だけど、素朴にやると動画全体をデコードすることになる。2時間の動画なら17万フレーム以上を処理して、最後の1枚だけ取り出すことになる。答えは ffmpeg -sseof -1 -i input.mp4 -update 1 output.png。末尾にシークして上書きメカニズムで最終フレームだけを取得できる。

サムネイル生成パイプラインでは定番の手法で、大量のファイルをバッチ処理しても一瞬で終わる。この記事では、なぜこのコマンドが動くのか内部挙動まで掘り下げて解説する。

bash
ffmpeg -sseof -1 -i "input.mp4" -update 1 "output.png"
動画ファイル2時間の動画スキップ-sseof -1末尾1秒にシーク数フレームデコード最後の数フレームだけ上書き保存output.png最終フレーム保存

コマンド全文

Windows (PowerShell)

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)

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 muxerframe001.pngframe002.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 で品質を指定する。

bash
# 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 1EOF直前にシーク → 上書きで最終フレーム取得動画の長さに依存しないO(1)に近い速度

30分・30fpsの動画で比較すると、全デコード方式は54,000フレームを処理する。-sseof 方式は多くて数百フレーム。この差は動画が長くなるほど広がる。

バッチ処理スクリプト

Bash:ディレクトリ内の全MP4を処理

大量の動画を処理するなら、もっと高度なアプローチもある。FFmpeg + Python バッチ自動化ガイドでは並列処理やエラーハンドリングを含むパイプライン構築を解説している。

bash
#!/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:エラーハンドリング付きバッチ処理

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でフィルタリングしよう。

bash
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 バッチ自動化ガイドを参考にしてほしい。