ffmpeg で複数動画結合

あえての曖昧な言い回しの見出し。

ffmpeg で複数動画結合

動機 + 「ついで」

ここで hstack, vstack 使ったついでに Merge Conference Video and Audio call output using hstack ffmpeg 的な、かなり起こりうるニーズのことをやってみよう、つーことである。一つ前で予告した通り、『たとえばともだちと共同で同じ対象を撮影したとしてだな、未編集ならばそれらは時刻あわせさえちゃんと出来るなら、対象までの距離差さえなければ簡単に同期するはずだ、みたいなことね。』であるとか、あるいは「似ているが違う」ものを比較目的で同時再生したい、なんてのも良くあることだろうし。後者のニーズはサイエンスな分野でありがちな気がするわね。(というか正直そういう複数動画同時再生可能なプレイヤーがあればいいのにと思う。)

が、「動画結合」とあえてタイトルを外しておいて、ついでなので How to concatenate (join, merge) media files についても(改めて)メモっとこうかなと。結構何度もやるんだけど毎度忘れるもんで。

ついでといえばもうひとつ、一つ目が hstack, vstack のテリトリーで、二つ目が concat なわけだけれど、三つ目として overlay もメモっときたい。これの用途は無論広いが、「画像結合」という言葉で括る場合は「同じ対象を別アングルで撮影した複数画像を同時に見せる」ための hstack/vstack でない別解、俗に言うところの「ワイプ」だよね。ワタシ個人の用途ではそんなに頻繁に使う想像は出来ないけれど、一応やっておきたい。やりたくなってから調べるの億劫だからさ。

入力動画たち

著作権、youtube の利用規約、個人情報保護等々ナイーブでシビアな問題が多いし、どのみち「処理結果の意味が伝えやすい作為的な動画が必要」なので自作動画を使うしかない、てことでWaveform な動画を自作した。再掲:

1つ目と3つ目は頭で「スラッシュスラッシュ」が入ってるかどうかだけが違う同じ音声、2つ目と4つ目も同じ関係、スラッシュの数が違う。これは「同期する時刻が動画ごとに違う」ことを模擬するためにそうしてる。

これらは便宜的に i1.mp4i2.mp4i3.mp4i4.mp4 という名前にしてあるとする。

試行錯誤のために先に知っておくべきこと

特に同期ポイントが動画開始に揃っていない動画について、どこで同期するかわからず試行錯誤するしかないとするならば、試行錯誤の一回あたりの時間も浪費しがちなので、「毎度動画全体を処理するのを待つ」ことは避けたいというわけだ。(当然そうなりにくいように、予め「およそこのくらい」というアタリをつけておくことも必要。)

ffmpeg の終了方法を知っとくのが先決。ffmpeg は処理中キー入力を受け付けるが、「q」で「処理終了」出来る。つまり、適当な時間処理して「q」で止めると、その時間までの動画が出来上がる。Ctrl-C で強制終了すると終了処理が行われないので、壊れた動画になり、どのプレイヤーも再生出来ない。

もう一つは -t で duration を指定できること、-ss で開始時間を指定できること。後者はフレームスキップが一瞬で終わるわけではないので時短用途には今ひとつだが、前者は特に何度も試行錯誤をする必要がある場合は重要。今 bash のヒストリをあさったらこんなのを使ってた:

bash の計算機能も駆使してる
1 me@host: ~$ for i in *.mp4 ; do \
2 > ffmpeg -y -i "$i" \
3 > -ss 00:00:05 \
4 > -t $((60*88 + 36 - 5)) \
5 > -vf \
6 >   "scale=iw/2:-1,pad=1280:720:(ow-iw)/2:(oh-ih)/2:color=black" \
7 > ../"`basename \"$i\"`" ; done

この例の場合、動画開始5秒から 5311 秒を処理している。ほかにもフレーム数で指定することも出来るので、詳しくは --helpドキュメント参照のこと。

本題ではない concat

本題ではないし、一度書いてるし、てことなんだけれど、youtube の利用規約違反に気付いてなかったときに書いたヤツなのでよろしくないので改めて、つーことだ。

といっても、基本は FFMpeg な wiki に書いてあるしね。さらっとだけ。

形式が統一されていない場合、最低でもサイズを統一する必要がある。たとえば リサイズはこんな感じ:

いつもの通りワタシは MSYS ユーザなので、MSYS bash のコマンドラインね
1 [me@host: HnK]$ mkdir cnv
2 [me@host: HnK]$ for i in *.mp4 ; do ffmpeg -i "$i" -vf scale=1920:1080 cnv/"$i" ; done

ほか色々変換が必要な可能性があるが、どこまで ffmpeg が空気読んでくれるかはやってみないとわからんところ。少なくともサイズに関しては全く ffmpeg の concat は我関せずでぶっ壊れた動画を作ってしまうが、ダメなら色々駆使してリエンコードする必要があるだろう(典型的には -r でフレームレート、-ar でオーディオサンプリングレート、-c:v でビデオコーデック、-c:a でオーディオコーデック、ここいらだろうとは思う)。

で、これ(ら)が済んで形式が整ったら今度はこれらを「結合」する。例えばこんな感じ:

カレントにはサイズが統一されたヤツらがいるとして
1 [me@host: HnK]$ ls *.mp4 | sed 's@^\(.*\)$@file '"'"'\1'"'"'@' > flist.txt
2 [me@host: HnK]$ ffmpeg -f concat -safe 0 -i flist.txt concat-result.mp4

リストを予め作るのが面倒くさいというやり方で、他のやり方もないではなさそうなんだけど、まぁ多分これが一番素直。(なお、上の例は「リエンコードを伴う結合」。リエンコードを避けたければ「-c copy」を付けなはれ。)

最初に挙げた入力動画を使った結果をアップロードすると結構デカい(50MB)し、これの結果が想像出来ない人はいないと思うので省略。(concatinate の名の通り「連結」動画になる、それだけ。)

結合部分をブラックアウトにするとかフェードアウト・フェードインにするとかについては、たとえばこんな検索で見つかると思う。

本題の hstack, vstack を使った結合

まずは音なしかつ同期に無頓着に単純に

1×2、2×1、4×4 と順に…、なんてやってるとダルいので、一気に 4×4 で:

(bash)シェルスクリプトの態だが、再利用可能性のためではなく単にコマンドラインで書き切るのが大変だから
 1 #! /bin/sh
 2 
 3 ffmpeg -y -i i1.mp4 -i i2.mp4 -i i3.mp4 -i i4.mp4 -filter_complex "
 4 [0:v]scale=iw/2:-1[0v];
 5 [1:v]scale=iw/2:-1[1v];
 6 [2:v]scale=iw/2:-1[2v];
 7 [3:v]scale=iw/2:-1[3v];
 8 [0v][1v]hstack[v1];
 9 [2v][3v]hstack[v2];
10 [v1][v2]vstack[v]" -map '[v]' merged0.mp4

filter_complex の書き方について熟知しているわけではなく、まだ雰囲気しかわかってないけど、これを読んでくれてるあなたもおそらく「雰囲気だけはすぐにわかる」と思う。0, 1, 2, 3 は当然4つの入力ファイルに対応してて、0:v は一個目の入力ファイルのビデオストリーム、なので一行目は「入力1のビデオストリームをリサイズしてこれを 0v とする」てこと。hstack, vstack 部分もこの要領で読めるでしょう(なのでscaleせずにそのまま結合するやり方も想像通りのものなはずよ)。本気であれこれやる必要が出たらもうドキュメントを熟読するしかない。まだワタシも全然出来てないけど。

これの処理結果例は、続く音声あり版から想像つくはずなので省略。

音ありだが同期にはまだ無頓着に

(bash)シェルスクリプトの態だが、再利用可能性のためではなく単にコマンドラインで書き切るのが大変だから
 1 #! /bin/sh
 2 
 3 ffmpeg -y -i i1.mp4 -i i2.mp4 -i i3.mp4 -i i4.mp4 -filter_complex "
 4 [0:v]scale=iw/2:-1[0v];
 5 [1:v]scale=iw/2:-1[1v];
 6 [2:v]scale=iw/2:-1[2v];
 7 [3:v]scale=iw/2:-1[3v];
 8 [0v][1v]hstack[v1];
 9 [2v][3v]hstack[v2];
10 [v1][v2]vstack[v];
11 [0:a][1:a][2:a][3:a]amix=inputs=4[a]" -map '[v]' -map '[a]' -ac 2 merged1.mp4

amix と amerge の違いはよくわかってないし、音の混ぜ方を懲りたければ、色々もっと複雑なことが出来るみたいで、当然「左動画の音は左チャンネルに、右動画の音は右チャンネルに」なんてことをしたいなんてことは考えられるけど、これはワタシは今のところ用事ないので、今回はやめとく。(こういうのって「悔しいからやりたい」気分はないではないので、気力があって気が向いたら追記の形で書くかもしれないし書かないかもしれない。)

想像通りの結果になる:

「短い動画にお尻を揃えたい」なんてのはあると思うが、これはドキュメントを読めばわかると思う。

音ありで同期時刻合わせあり

たとえば StackOverflow のこの質問と回答を参考にすればこんな具合:

(bash)シェルスクリプトの態だが、再利用可能性のためではなく単にコマンドラインで書き切るのが大変だから
 1 #! /bin/sh
 2 
 3 ffmpeg -y -i i1.mp4 -i i2.mp4 -i i3.mp4 -i i4.mp4 -filter_complex "
 4 [0:v]scale=iw/2:-1,setpts=PTS-STARTPTS+1.9/TB[0v];
 5 [1:v]scale=iw/2:-1,setpts=PTS-STARTPTS+1.0/TB[1v];
 6 [2:v]scale=iw/2:-1,setpts=PTS-STARTPTS+1.0/TB[2v];
 7 [3:v]scale=iw/2:-1,setpts=PTS-STARTPTS+0.0/TB[3v];
 8 [0v][1v]hstack[v1];
 9 [2v][3v]hstack[v2];
10 [v1][v2]vstack[v];
11 [0:a]adelay=1900|1900[0a];
12 [1:a]adelay=1000|1000[1a];
13 [2:a]adelay=1000|1000[2a];
14 [3:a]adelay=0|0[3a];
15 [0a][1a][2a][3a]amix=inputs=4[a]" -map '[v]' -map '[a]' -ac 2 merged2.mp4

adelay の値としてパイプ記号で区切って二つの値を書いてるのは右チャンネル・左チャンネル別々に記述出来るから、かなと思う。(2018-06-16追記: 古いバージョンの ffmpeg (少なくとも 3.3.2)では adelay にゼロを与えるとエラーになるので注意。)

ただねぇ、気に喰わんのよ:

パーフェクトに同期出来ないのが気に喰わん! …つーことではなくて。「スラッシュスラッシュ」言ってる時間帯を観察してみてちょ。絵が動いておらんでしょう? 今の場合 i4.mp4 が基準で、i1.mp4 は i4.mp4 がスラッシュスラッシュ言い終わるまで待ってる、ということなわけね。だから「待ってるほう」が動かないのは無論意図通りなのだが、「待たせてるほう」は動かないとおかしいでしょう? どうも絵の方は「先に行って待ってる」らしい。そうじゃないだろ、と思うのだが…。(だって音の方は意図通りなんだもの、なんじゃそりゃ、て思うさそりゃ。)

なんとかしてみようといくつか試してみたけどダメだったので、これは諦めた。これがどうしても気になる場合はもう trim して開始を揃えてしまうのがいいと思う。(各々 -ss やらで切り取ったものを入力としてしまう、てこと。今の例の場合は「スラッシュ…」言ってる部分を切り取ってしまう。)

音声は混ぜなくてええんや

一番音声が理想的なもの一つだけを取ればいいのだ、みたいなこともあると思う。実際の撮影動画で音割れしちゃってて使い物にならないようなものを「混ぜたい」わきゃぁねいのであって。

一つ上のやつより簡単だろう、と思ったがそうでもなかったの。

「正しい」アプローチがあるに違いない、と信じてはいるけれど、「volume 調整しちゃえばいいんじゃね?」という場当たり的な思いつきでも一応うまくいった:

(bash)シェルスクリプトの態だが、再利用可能性のためではなく単にコマンドラインで書き切るのが大変だから
 1 #! /bin/sh
 2 
 3 ffmpeg -y -i i1.mp4 -i i2.mp4 -i i3.mp4 -i i4.mp4 -filter_complex "
 4 [0:v]scale=iw/2:-1,setpts=PTS-STARTPTS+1.9/TB[0v];
 5 [1:v]scale=iw/2:-1,setpts=PTS-STARTPTS+1.0/TB[1v];
 6 [2:v]scale=iw/2:-1,setpts=PTS-STARTPTS+1.0/TB[2v];
 7 [3:v]scale=iw/2:-1,setpts=PTS-STARTPTS+0.0/TB[3v];
 8 [0v][1v]hstack[v1];
 9 [2v][3v]hstack[v2];
10 [v1][v2]vstack[v];
11 [0:a]adelay=1900|1900,volume=0[0a];
12 [1:a]adelay=1000|1000,volume=0[1a];
13 [2:a]adelay=1000|1000,volume=0[2a];
14 [3:a]adelay=0|0[3a];
15 [0a][1a][2a][3a]amix=inputs=4[a]" -map '[v]' -map '[a]' -ac 2 merged3.mp4

少なくとも「不正解」ではないしね、これでいいような気はする。

Overlay でワイプ的な

これの効果が今使ってる入力動画だけだと伝わりにくいので、予め

1 me@host: ~$ ffmpeg -y -i i2.mp4 -vf 'negate' i2n.mp4

なんてことをして、黒背景動画と白背景動画の overlay で遊ぶことにする。つまり入力はこの2つ:

あとはドキュメントを読めばわかる、と言いたいところだが、読み方がわからない状態でこれを読んでわかるわけはなく。たとえばこれなども参考にしつつたとえば:

(bash)シェルスクリプトの態だが、再利用可能性のためではなく単にコマンドラインで書き切るのが大変だから
1 #! /bin/sh
2 
3 ffmpeg -y -i i2n.mp4 -i i4.mp4 -filter_complex "
4 [0:v]setpts=PTS-STARTPTS+1.0/TB[0v];
5 [1:v]scale=iw/4:-1,setpts=PTS-STARTPTS+0.0/TB[1v];
6 [0v][1v]overlay=(W - w - 50):(H - h - 50)[v];
7 [0:a]adelay=1000|1000[0a];
8 [1:a]adelay=0|0[1a];
9 [0a][1a]amix[a]" -map '[v]' -map '[a]' -ac 2 merged4.mp4

開始時刻のズレ補正がいらないならもっと単純、なのはわかるよね。

結果:

これだけならなんてことはないね。

あとは透過させたり、一定時間だけオーバレイ出来たりするけど、特に難しくはないと思うので省略。