Jonouchi Kouyo's Blog

カテゴリ:コマンド

ffmpegを使って動画を切り取る(trim:トリムする)ときに、キーフレーム(keyframe:Iフレーム)を使う方法をメモしておく。

実行環境
MacOSX 10.8.5
ffmpeg ver 2.4.2
bash ver 3.2.53

今回使用した動画の形式
コンテナ mp4
動画コーデック h264
音声コーデック aac

何故キーフレームを選択するかというと、キーフレーム単位で動画を切り取ると動画の再エンコードが必要ないので、元の画像・音声を劣化せずに切り取れるからである(詳細は"動画 GOP"で検索)。またエンコードが無いため処理が一瞬で終わる。

逆にキーフレームを指定しない場合は再エンコードが発生して処理に時間がかかる。キーフレーム以外の任意の位置から切り取りを開始したい場合は、必ず後者の処理を行うことになる。

1.切り取りに使うffmpegのコマンド


ffmpeg -ss "開始時刻" -i input.mp4 -ss 0 -t "切り取り時間" -c:v copy -c:a copy -async 1 -strict -2 output.mp4

この"開始時刻"と"切り取り時間"に、キーフレームの時刻(時間)を設定する。

最初の「-ss "開始時刻"」でffmpegに切り取りの開始位置をシークさせ、2番目の「-ss 0」で開始時刻から切り取りを実行する。

「-i」の前に「-ss "開始時刻"」をつけるだけで十分な気もするが、これだとffmpegが開始時刻を正しくシークしてくれない。結果、切り取った映像の最初の1〜4秒間くらいがグレー画面になったり、フレームが飛ばされたりと酷い映像が出来上がる。なので「-i」の後ろにもう一度「-ss 0」と設定する事により、シークした時刻から正確に切り取り開始するようになる。

どうも無劣化で切り取る場合(copyを使った場合)に発生するffmpegの仕様(バグ?)のようで、上記で述べた再エンコードを行う場合はこの事象は発生しない。

尚、このコマンドで「-t」の代わりに「-to "終了時刻"」を使おうとすると、「-to」が無視されて「-t」に自動的に変換される。どうやら最初に「-ss」を持ってきた場合に限り「-to」が無視されるようだ。

2.キーフレームを調べる


まず対象の動画のキーフレームを調べるために、以下のコマンドを実行する。

ffprobe -show_frames -pretty input.mp4

実行すると、動画と音声のフレーム単位の詳細な情報が出力される。

[FRAME]  ← 1フレームの動画情報
media_type=video
key_frame=0
pkt_pts=864064
pkt_pts_time=0:00:54.004000
pkt_dts=864064
pkt_dts_time=0:00:54.004000
best_effort_timestamp=864064
best_effort_timestamp_time=0:00:54.004000
pkt_duration=533
pkt_duration_time=0:00:00.033313
pkt_pos=3041277
pkt_size=358
width=604
height=340
pix_fmt=yuv420p
sample_aspect_ratio=1360:1359
pict_type=B
coded_picture_number=1617
display_picture_number=0
interlaced_frame=0
top_field_first=0
repeat_pict=0
[/FRAME]
[FRAME]  ← 1フレームの音声情報
media_type=audio
key_frame=1
pkt_pts=1193982
pkt_pts_time=0:00:54.148844
pkt_dts=1193982
pkt_dts_time=0:00:54.148844
best_effort_timestamp=1193982
best_effort_timestamp_time=0:00:54.148844
pkt_duration=1024
pkt_duration_time=0:00:00.046440
pkt_pos=3047553
pkt_size=355
sample_fmt=fltp
nb_samples=1024
channels=2
channel_layout=stereo
[/FRAME]

今回必要なのは動画情報のみ(media_type=video)なので、オプションに「-select_streams -v」を追加して動画のフレーム情報のみ抽出する。

ffprobe -show_frames -select_streams -v -pretty input.mp4

[FRAME]
media_type=video
key_frame=1
pkt_pts=1772176
pkt_pts_time=0:01:50.761000
pkt_dts=1772176
pkt_dts_time=0:01:50.761000
best_effort_timestamp=1772176
best_effort_timestamp_time=0:01:50.761000
pkt_duration=533
pkt_duration_time=0:00:00.033313
pkt_pos=5994965
pkt_size=24411
width=604
height=340
pix_fmt=yuv420p
sample_aspect_ratio=1360:1359
pict_type=I
coded_picture_number=3316
display_picture_number=0
interlaced_frame=0
top_field_first=0
repeat_pict=0
[/FRAME]
[FRAME]
media_type=video
key_frame=0
pkt_pts=1772704
pkt_pts_time=0:01:50.794000
pkt_dts=1772704
pkt_dts_time=0:01:50.794000
best_effort_timestamp=1772704
best_effort_timestamp_time=0:01:50.794000
pkt_duration=533
pkt_duration_time=0:00:00.033313
pkt_pos=6025150
pkt_size=535
width=604
height=340
pix_fmt=yuv420p
sample_aspect_ratio=1360:1359
pict_type=B
coded_picture_number=3319
display_picture_number=0
interlaced_frame=0
top_field_first=0
repeat_pict=0
[/FRAME]

この結果の中で「key_frame=1」となっているのがキーフレームのことである。このフレームの「pict_type」をみると、このフレームが「pict_type=I」(Iフレーム)であることがわかる。

ffmpegで必要なのはこのキーフレームの時刻であるため、ここから「pkt_pts_time」を抽出する。

ffprobe -show_frames -select_streams -v -pretty input.mp4 | grep "key_frame=1" -A 3 | grep -w pkt_pts_time | cut -d= -f2 | sed -e s/000\$// > temp.txt

「grep "key_frame=1" -A 3」とは、ffprobeの結果から「key_frame=1」を抽出し、さらにその行を含む3行を抽出する。
次のgrepで「pkt_pts_time」の行だけを抽出し、cutで"="より後ろの時刻を取り出し、最後にsedで時刻の余分なミリ秒(後3桁"000")を削除してtemp.txtに書き出す。

尚、元動画の時間が長いほど、この処理に時間がかかるので暫く待つ(1分程度)

3.切り取り時間を求める


ffmpegでもう一つ必要なのは"切り取り時間"なので、このtemp.txtファイルの中から開始時刻と終了時刻を選択し、その差分(切り取り時間)をbashで求める。
#!/bin/bash

ts_get_msec()
{
	read -r h m s ms <<< $(echo $1 | tr '.:' ' ')
#	echo $(((h*60*60*1000)+(m*60*1000)+(s*1000)+ms))
	echo $(((10#${h}*60*60*1000)+(10#${m}*60*1000)+(10#${s}*1000)+10#${ms}))
}

ts_get_time()
{
	hour=$(($1/(60*60*1000)))
	min=$((($1%(60*60*1000))/(60*1000)))
	sec=$((($1%(60*1000))/1000))
	ms=$((($1%(60*1000))%1000))

	min=0$min
	sec=0$sec
	ms=0$ms

	echo "${hour}:${min:(-2)}:${sec:(-2)}.${ms:(-3)}"
}

start_pts=開始時刻 (例 0:01:24.333)
start_msec=$(ts_get_msec $start_pts)

end_pts=終了時刻 (例 0:08:45.338)
end_msec=$(ts_get_msec $end_pts)

DIFF=$((end_msec-start_msec))
duration_ts=$(ts_get_time $DIFF)

echo $duration_ts


bashで時刻の差分を計算する場合「date -d"時刻" +%s」コマンドを使って一旦ミリ秒に変換するのが簡単であるが、私の実行環境のMacはBSD UNIXをベースに作られているため「date」コマンドの「-d」オプションが使用できない。正直、Unixの〜系はほとんど意識したことがなかったので、今回はドツボにハマってかなり時間を食ってしまった。

そこでgoogle先生から「date」コマンドに頼らない自作の関数を教えてもらい、その内部でミリ秒に変換する処理を行う。(上記 ts_get_msec関数)

尚、ts_get_msec関数で最初のechoをコメントアウトしている理由は、$((計算式))で処理を行う際に文字列「08」が入ると、bashの仕様で「08」を16進数に勝手に変換して計算しようとするため、計算処理で必ずエラーが発生する。これを回避するには「08」という文字列を意図的に数値に変換してやる必要がある。

そこで2番目のecho文のように、「10#${変数}」(10は10進数という意味)としてやれば、この変数に格納された値を数値として認識してくれる。もちろん「08」が数字の「8」として認識されるようになる。

ts_get_msec関数で開始時刻と終了時刻をミリ秒に変換した後、差分を計算し、ミリ秒を時刻表記に戻す(ts_get_time関数)。min, sec, ms の先頭に「0」をつけて0埋めした後、echo出力時に分:秒.ミリ秒を2桁、2桁、3桁に調整して出力する。

上記の例で実行した場合、切り取り時間は「0:07:21.005」が得られる。

開始時刻と切り取り時間が求められたので、最後にffmpegコマンドを実行する。

ffmpeg -ss 0:01:24.333 -i input.mp4 -ss 0 -t 0:07:21.005 -c:v copy -c:a copy -async 1 -strict -2 output.mp4

4.まとめ

はっきり言って、動画を無劣化で切り取るためにffmpegを使って上記のやり方を行うのは非常に面倒である。これなら「avidemux」というフリーソフトを使ってGUI上で操作する方が遥かに簡単で早い。

但し、複数の動画の切り取り時間を事前に決めておき、バッチ処理で一気に行う場合は、上記の方法を自動化すると便利かもしれない。

例えば、複数の動画の最初10分を無劣化で取り出したいという場合は、開始時刻に「0:10:00.000」を設定しておき、その時刻に一番近いキーフレームを検索・抽出して動画の数だけ切り取りを繰り返すということもできる。

筆者の場合は「Aegisub」という字幕ソフトで切り取る動画にある字幕(ラベル)をつけておき、「ass」ファイルで保存した後、その「ass」ファイルから開始時刻と終了時刻を読み取って一気にバッチ処理で切り取るという方法を行っている。

いずれにせよ、ffmpegにこだわる必要がない場合は別ソフトを使う方が良い。
また無劣化にこだわる必要がなければ、時間指定で再エンコードする方が余計な動画を入れずに綺麗な動画が出来上がるので、そちらをお勧めする。

vim Window系コマンドメモ
コマンド説明
splitウインドウを上下に分割
newウインドウを上下に分割し新しいウインドウを作成
onlyアクティブなウインドウだけを表示
closeアクティブなウインドウを閉じる
Ctrl+W Kアクティブなウインドウを上に移動
Ctrl+W Jアクティブなウインドウを下に移動
Ctrl+W +アクティブなウインドウを広げる
Ctrl+W -アクティブなウインドウを狭める
vimdiff a-file b-file2つのファイルの差分を表示
tabedit a-file新規タブを作成して a-file を表示
tab split新規タブを作成して現在開いている内容を表示
tab new新規タブを作成して新規ファイル表示
tab help gt新規タブを作成して gt のヘルプを表示
gt次のタブへ移動
gT前のタブへ移動
{count}gt{count}番目のタブへ移動
tabfir最初のタブへ移動
tabl最後のタブへ移動
tabm [N]アクティブなタブを[N]番目の後ろに移動(0を指定すると最初に移動)
tabm +[N]アクティブなタブを右に移動
tabm -[N]アクティブなタブを左に移動

このページのトップヘ