アルアカ - Arcadia Academia

Arcadia Academiaは「エンジニアリングを楽しむ」を合言葉に日本のデジタル競争力を高めることをミッションとするテックコミュニティです。

ffmpegの多区間カットがエラーなく止まる原因と、selectで書き直す方法

Featured image of the post

「50分近い画面収録から不要な区間を削って書き出したいのに、毎回同じところで止まる」「ffmpeg のログにはエラーが1行も出ていないのに、プロセスだけ消えている」「短い動画なら同じコマンドで通るので、書き方自体は間違っていないはず」——長い動画を多数の区間に切って詰める処理では、こういう原因の見えない停止が起きます。

この記事では、trim と concat を区間の数だけ並べる書き方がなぜ長尺で破綻するのか、無言で死んだときにどこを見れば原因が確定するのか、そして select / aselect を使った1パスの書き方へ置き換える手順をまとめます。

💡
30秒で要点
- trim を区間の数だけ並べて concat でつなぐ書き方は、長尺 × 多区間でメモリを使い切って落ちる
- 落ち方は「ログに何も出ないまま消える」。ffmpeg 側のログを何度読んでも原因は書いていない
- 原因の確定は journalctl -k | grep -i oom。カーネル側に kill の記録が残っている
- 置き換えは select='between(t\,a\,b)+…' と aselect の1パス。実測で最大 RSS 21GB → 470MB、約10倍速で完走
- 同じ位置で2回止まったら、プロセスの起動方法ではなく資源の枯渇を疑う

目次を開く

症状:エラーを出さずに ffmpeg が消える

47分50秒・1280×720・60fps・2.0GB の収録から、無音区間23箇所の短縮と前後の除去をまとめて行う処理で、2回とも26分46秒付近で停止しました。標準出力にも標準エラーにもエラーは1行もなく、進捗表示が途中で終わっているだけです。

このとき最初に疑いたくなるのが「バックグラウンドで走らせていた親プロセスが終了して巻き添えになった」という筋です。実際、1回目はそう誤診しました。しかし同じ位置で2回止まる時点でその線は薄いです。親が死ぬタイミングは処理の中身と無関係なので、再現位置が揃うことは普通ありません。

まずカーネルログを見る

ffmpeg のログに書かれないのは、ffmpeg が自分で終了していないからです。記録は OS 側に残ります。

# 直近の kill 記録を探す(時刻は止まった時間に合わせる)
journalctl -k --since "19:00" | grep -i oom

# journal が使えない環境では
sudo dmesg -T | grep -i -e 'out of memory' -e 'oom-kill'

oom-kill の行と、Killed process ... (ffmpeg) total-vm:... rss:... の行が出れば確定です。この例では RSS が約 21GB でした。搭載メモリを超えて確保しにいって、カーネルに止められていたわけです。

なぜ trim と concat の組み合わせが破綻するのか

多区間カットの解説でよく見るのは、区間ごとに trim / atrim を作り、最後に concat でつなぐ書き方です。

ffmpeg -i in.mp4 -filter_complex "\
[0:v]trim=12:48,setpts=PTS-STARTPTS[v0];[0:a]atrim=12:48,asetpts=PTS-STARTPTS[a0];\
[0:v]trim=71:130,setpts=PTS-STARTPTS[v1];[0:a]atrim=71:130,asetpts=PTS-STARTPTS[a1];\
[v0][a0][v1][a1]concat=n=2:v=1:a=1[v][a]" \
  -map "[v]" -map "[a]" -c:v libx264 -crf 20 -c:a aac out.mp4

区間が2つならこれで問題ありません。壊れるのは区間が増えたときです。concat は入力を順に同期して読むため、まだ出番の来ていない枝が受け取ったフレームは、そのままバッファに積まれます。区間の数だけ枝が並ぶと、入力を読み進めるほど保持されるフレームが増え、尺と区間数の積でメモリが伸びていきます。

書き方 区間が増えると 長尺での挙動
trim を並べて concat 枝もバッファも区間数ぶん増える メモリが積み上がり、無言で kill される
select の式1本 式の文字列が長くなるだけ フレームは通過するだけで溜まらない

置き換え:select と aselect で1パスにする

select は「条件に合うフレームだけ通す」フィルタです。区間を足し算でつなげば、枝を作らずに多区間カットが書けます。

ffmpeg -i in.mp4 \
  -vf "select='between(t\,12\,48)+between(t\,71\,130)',setpts=N/60/TB,fps=30,format=yuv420p" \
  -af "aselect='between(t\,12\,48)+between(t\,71\,130)',asetpts=N/SR/TB" \
  -c:v libx264 -crf 20 -c:a aac out.mp4

書き方のポイントは3つです。

  • between の引数区切りは \, とバックスラッシュ付きにする。フィルタ全体の区切り文字と衝突するため
  • setpts=N/60/TB の 60 は元動画のフレームレート。ここを取り違えると再生速度が変わる
  • 音声側の asetpts=N/SR/TB を省かない。映像だけ詰めると音がずれる

同じ素材で、この書き方は 最大 RSS 470MB・約10倍速で完走 しました。21GB との差は、そのままバッファの有無です。

区間が多いときは式をファイルに逃がす

区間が20を超えると式が数百文字になり、シェルの引用符と相性が悪くなります。生成をスクリプトに任せ、ファイル経由で渡すと事故が減ります。

# make_sel.py — 区間リストからフィルタ式を書き出す
segs = [(12.0, 48.0), (71.5, 130.2)]       # (開始秒, 終了秒)
FPS = 60                                    # 元動画のフレームレート
expr = "+".join(f"between(t\\,{a}\\,{b})" for a, b in segs)
open("vsel.txt", "w").write(f"select='{expr}',setpts=N/{FPS}/TB,fps=30,format=yuv420p")
open("asel.txt", "w").write(f"aselect='{expr}',asetpts=N/SR/TB")
python3 make_sel.py
ffmpeg -i in.mp4 -vf "$(cat vsel.txt)" -af "$(cat asel.txt)" \
  -c:v libx264 -crf 20 -c:a aac body.mp4

区間そのものは、無音の検出結果から機械的に作れます。

ffmpeg -i in.mp4 -af silencedetect=noise=-35dB:d=2.5 -f null - 2> silence.log
grep -e silence_start -e silence_end silence.log

長いレンダは切り離して待つ

数十分かかる処理は端末に貼り付けたままにせず、切り離して走らせ、終了を待ちます。

setsid nohup ffmpeg -i in.mp4 -vf "$(cat vsel.txt)" -af "$(cat asel.txt)" \
  -c:v libx264 -crf 20 -c:a aac body.mp4 > render.log 2>&1 &
until ! pgrep -x ffmpeg; do sleep 5; done   # 終わるまで待つ
tail -n 5 render.log

ついでに1パスで済ませられること

再エンコードは1回で済ませたいので、カットと一緒に掛けておくと後の工程が減ります。

  • 字幕の焼き込み: -vf の末尾に ass=subs.ass を足す
  • 音量の正規化: -af の末尾に loudnorm=I=-16:TP=-1.5:LRA=11。掛ける前の値は ebur128 で測れる(この収録は -25.8 LUFS だった)
  • 画面下部の黒帯: cropdetect で幅を出してから、字幕をその内側に収める

字幕の時刻は「結合前のファイル」を基準に書く

本編を単独で書き出し、あとからオープニングを -f concat -c copy で前置する構成にする場合、字幕の時刻にオープニングの尺を足してはいけません。前置は結合であって、本編側のタイムラインは動かないからです。オープニング12秒ぶんを足して書いたときは、全字幕が12秒遅れました。

継ぎ目の確認は、特定のフレーム番号だけ抜き出して並べると速いです。

ffmpeg -i out.mp4 -vf "select='eq(n\,150)+eq(n\,330)'" -vsync 0 frame_%03d.png

まとめ

  • 長尺 × 多区間のカットで ffmpeg が無言で消えたら、まず journalctl -k | grep -i oom を見る。ffmpeg のログには出ない
  • 原因は trim を並べて concat する構造がフレームを溜め込むこと。区間が増えるほどメモリが伸びる
  • select / aselect の1パスに書き換えると、同じ素材で最大 RSS 21GB → 470MB・約10倍速になる
  • 式は Python で生成してファイルに逃がし、$(cat …) で渡すと引用符の事故が減る
  • 同じ位置で2回止まったら、プロセスの起動方法ではなく資源の枯渇を疑う

よくある質問(FAQ)

Q. カットした境界で映像と音がずれます

setpts と asetpts の片方しか書いていない場合に起きます。select はフレームを間引くだけでタイムスタンプを詰めないので、映像は setpts=N/<fps>/TB、音声は asetpts=N/SR/TB を必ず対で書きます。

Q. -c copy のまま多区間カットはできますか

できません。select はデコード後のフレームに働くフィルタなので、再エンコードが前提です。ストリームコピーで切りたい場合は、区間ごとに -ss / -to で別ファイルへ切り出してから -f concat -c copy でつなぐ形になり、境界はキーフレームに丸められます。

Q. 何区間くらいから危ないですか

一律の閾値はありません。手元で破綻したのは47分・24区間の組み合わせでした。フレームサイズと尺にも左右されるので、区間数が2桁になったら select 側で書く、と決めてしまうのが早いです。

Q. メモリを増やせば trim と concat のままでいけますか

一時的には通りますが、区間を増やすたびに同じ壁に当たります。select 側は区間が増えても式が長くなるだけでメモリはほぼ変わらないので、書き換えたほうが安定します。

プログラミング学習でお悩みですか?

現役エンジニアがあなたの学習をマンツーマンでサポートします。

  • 学習の進め方がわからない
  • ポートフォリオの作り方を知りたい
  • 現場で使える技術を学びたい
まずは30分の無料相談

相談は完全無料・オンラインで気軽に

あなたを爆速で成長させるメンタリングプログラムはこちら

メンタープログラムバナー

学習・開発のお悩みは現役エンジニアに相談

メンタープログラムの詳細を見る

エンジニアの基礎学習ゲーム

プログラミング学習支援

無料相談はこちら