ffmpeg で動画の頭とかお尻にパディング

英語タイトルの方が意図を伝えやすかったりするのだがなぁ、たとえば「ffmpeg add blank frames」てことね。パディングはパディングなんだけれど、一フレームの絵をパディングする話と紛らわしいのよね。

ffmpeg で動画の頭とかお尻にパディング

「今回のオレ」的動機

これをやりたい動機がわからんてことはないでしょ、普通に頻出のニーズだと思うから。

今「ワタシが」これをしたいのは、ffmpeg で複数動画結合がきっかけ。この複数動画を hstack, vstack で一枚動画にする際に、4つの動画の同期に setpts と adelay を駆使したわけだけれど、これの結果出来上がる動画には不満なわけだ。(同期時刻より前の時間帯に関して、音しか意図通りにならない。)

よって、「ポインタをずらす」というアプローチではなくて、動画に埋め草をしたい、てわけである。

そしてこれが「べっつに書くほどのことでもなかんべな」てくらい簡単だったら良かったんだけどね、やってみたらなんだかちょい鬱陶しかった、てわけだ。

例によって作為的な動画

今までやってきた流れのヤツら、特にffmpeg で動画分割(とか)のままでもそう悪くもないのだけれど、今回のは話の都合上「まともなステレオの動画」である必要があって。

なので MPEG-audio.org からダウンロード出来る動画をちょっと加工したこれを使う:

ffprobe するとこんな:

 1 [me@host: ~]$ ffprobe input1.mp4
 2 ...(snip)...
 3 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'input1.mp4':
 4   Metadata:
 5     major_brand     : isom
 6     minor_version   : 512
 7     compatible_brands: isomiso2avc1mp41
 8     encoder         : Lavf58.17.100
 9   Duration: 00:00:59.02, start: 0.000000, bitrate: 5446 kb/s
10     Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080, 5314 kb/s, 24 fps, 24 tbr, 12288 tbn, 48 tbc (default)
11     Metadata:
12       handler_name    : VideoHandler
13     Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 128 kb/s (default)
14     Metadata:
15       handler_name    : SoundHandler

(というか 1920×1080、h264、aac 44100 Hz, stereo にあえて変換したのだけれども。)

わけあって Waveform な動画 (ffmpeg で)の単なる応用編で、waveform 動画をオーバレイしてるのだが、「ステレオ」らしく左右チャンネル分離した絵にしてある。元動画を input0.mp4 とするとして:

1 [me@host: ~]$ ffmpeg -i input0.mp4 \
2 > -filter_complex \
3 >     "[0:a]showwaves=s=1920x1080:mode=cline:split_channels=1,format=yuv420p[v]" \
4 > -map "[v]" -map 0:a -c:v libx264 -c:a copy input0.waveform.mp4
5 [me@host: ~]$ ffmpeg -y -i input0.mp4 -i input0.waveform.mp4 \
6 > -filter_complex \
7 >     "[0:v][1:v]blend=all_mode='overlay':all_opacity=0.8[v]" \
8 > -map '[v]' -map '0:a' \
9 > -ac 2 input1.mp4

みたいにして作った。なお、本来 10分の動画で長過ぎるので、「ステレオ感」がわかりやすい一部を切り取ったのが上で見せた動画。10分全部のは Youtube にアップロードしといた

無音の真っ黒を頭に追加、の、今の場合の失敗例

基礎的なことに関して結構細かく書いてる人がいる。これなどを参考に、例えば今のワタシの例の場合はたとえばこんな具合:

例によってコマンドラインに書ききれないのでスクリプトにしてるだけ
1 #! /bin/sh
2 
3 # 1920x1080、44100 などの値は (今の場合の)input1.mp4 にあわせること。
4 # 例えば ffprobe や ffmpeg に入力ファイルだけ渡すことで調べられる。
5 ffmpeg -y -i input1.mp4 -filter_complex "
6 color=c=black:s=1920x1080:d=3.5 [padvideo];
7 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio];
8 [padvideo][padaudio] [0:0][0:1] concat=n=2:v=1:a=1" \
9 -ac 2 output1.mp4

これであたかも正しいかのように思える。周波数ゼロの正弦波で無音を作ってる、てことね。

これの間違いに気付ける人はよほど再生環境が良いか、耳がかなりいいのだろうと思う。わかる?

見かけ上はバッチリ:

 1 [me@host: ~]$ ffprobe output1.mp4
 2 ...(snip)...
 3 Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'output1.mp4':
 4   Metadata:
 5     major_brand     : isom
 6     minor_version   : 512
 7     compatible_brands: isomiso2avc1mp41
 8     encoder         : Lavf58.17.100
 9   Duration: 00:01:02.55, start: 0.000000, bitrate: 5061 kb/s
10     Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1920x1080 [SAR 1:1 DAR 16:9], 4935 kb/s, 25 fps, 25 tbr, 12800 tbn, 50 tbc (default)
11     Metadata:
12       handler_name    : VideoHandler
13     Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, stereo, fltp, 121 kb/s (default)
14     Metadata:
15       handler_name    : SoundHandler

ちゃんとステレオを維持できている、でしょう? けれど waveform 動画も作ってみると何が間違っているかわかる:

1 [me@host: ~]$ ffmpeg -y -i output1.mp4  \
2 >    -filter_complex \
3 >    "[0:a]showwaves=s=1920x1080:mode=cline:split_channels=1,format=yuv420p[v]" \
4 >    -map "[v]" -map 0:a -c:v libx264 -c:a copy output1.waveform.mp4

そう、「似非ステレオ」になっちゃってるの。2ch に記録されてはいるけれど、右チャンネルと左チャンネルに同じ値が入ってるだけ(一時停止を繰り返して元の input1.mp4 に blend してる波形とよく比べてみて欲しい)。

ゆえ、入力がモノラルであればこれでもいいのだけれど、入力がステレオで、出力も維持したいなら(今の目的を考えれば「維持しなくてもいい」なんて思うことはあるまい)、これではダメだ、というわけだ。

無音の真っ黒を頭に追加、の、ステレオを維持したい場合の正しいやり方

5.1ch だの 7.1ch なんてなってくるとさらに悶絶なことになると思うが、ただの 2ch なステレオであれば、たとえばこれなどを参考に、どうやらこんな感じ:

例によってコマンドラインに書ききれないのでスクリプトにしてるだけ
 1 #! /bin/sh
 2 
 3 # 1920x1080、44100 などの値は (今の場合の)input1.mp4 にあわせること。
 4 # 例えば ffprobe や ffmpeg に入力ファイルだけ渡すことで調べられる。
 5 ffmpeg -y -i input1.mp4 -filter_complex "
 6 color=c=black:s=1920x1080:d=3.5 [padvideo];
 7 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio_left];
 8 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio_right];
 9 [padaudio_left][padaudio_right]amerge=inputs=2[padaudio];
10 [padvideo][padaudio] [0:0][0:1] concat=n=2:v=1:a=1" \
11 -ac 2 output2.mp4

さきほどと同じ要領で waveform な動画:

今度は大丈夫、だよね?

無音の真っ黒をお尻に追加、は、頭に追加の失敗例と同じノリでも失敗にはならない

上の「失敗」と同じノリでまさに「順序を変えただけ」のこれ:

例によってコマンドラインに書ききれないのでスクリプトにしてるだけ
1 #! /bin/sh
2 
3 # 1920x1080、44100 などの値は (今の場合の)input1.mp4 にあわせること。
4 # 例えば ffprobe や ffmpeg に入力ファイルだけ渡すことで調べられる。
5 ffmpeg -y -i input1.mp4 -filter_complex "
6 color=c=black:s=1920x1080:d=3.5 [padvideo];
7 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio];
8 [0:0][0:1] [padvideo][padaudio] concat=n=2:v=1:a=1" \
9 -ac 2 output3.mp4

これは、ffmpeg が空気読んでくれる。(というか最初の失敗もまさに ffmpeg が空気を読んだ結果。)

waveform動画:

確かにこれは問題はないのだけれど、可搬性を考えた場合は、いつでもこちらのノリ:

例によってコマンドラインに書ききれないのでスクリプトにしてるだけ
 1 #! /bin/sh
 2 
 3 # 1920x1080、44100 などの値は (今の場合の)input1.mp4 にあわせること。
 4 # 例えば ffprobe や ffmpeg に入力ファイルだけ渡すことで調べられる。
 5 ffmpeg -y -i input1.mp4 -filter_complex "
 6 color=c=black:s=1920x1080:d=3.5 [padvideo];
 7 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio_left];
 8 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio_right];
 9 [padaudio_left][padaudio_right]amerge=inputs=2[padaudio];
10 [0:0][0:1] [padvideo][padaudio] concat=n=2:v=1:a=1" \
11 -ac 2 output4.mp4

が良いんじゃないかとは思う。

頭とお尻両方に

ただの応用問題だけど一応。ダルいけど:

例によってコマンドラインに書ききれないのでスクリプトにしてるだけ
 1 #! /bin/sh
 2 
 3 # 1920x1080、44100 などの値は (今の場合の)input1.mp4 にあわせること。
 4 # 例えば ffprobe や ffmpeg に入力ファイルだけ渡すことで調べられる。
 5 ffmpeg -y -i input1.mp4 -filter_complex "
 6 color=c=black:s=1920x1080:d=3.5 [padvideo1];
 7 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio_left1];
 8 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio_right1];
 9 [padaudio_left1][padaudio_right1]amerge=inputs=2[padaudio1];
10 color=c=black:s=1920x1080:d=3.5 [padvideo2];
11 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio_left2];
12 sine=frequency=0:sample_rate=44100:d=3.5 [padaudio_right2];
13 [padaudio_left2][padaudio_right2]amerge=inputs=2[padaudio2];
14 [padvideo1][padaudio1] [0:0][0:1] [padvideo2][padaudio2] concat=n=3:v=1:a=1" \
15 -ac 2 output5.mp4

とか。

オマケ: 無音の黒すけじゃのーて

このブログのネタがまさにそうで、カラーバーとかね、そんなのも出来ると言っておる。

無論ここまでワタシがあげてきた例の「frequency」を変えれば、ショボい音付きの真っ黒くろすけを作れる、よね。こんなんをやってみるよろし:

例によってコマンドラインに書ききれないのでスクリプトにしてるだけ
 1 #! /bin/sh
 2 
 3 # 1920x1080、44100 などの値は (今の場合の)input1.mp4 にあわせること。
 4 # 例えば ffprobe や ffmpeg に入力ファイルだけ渡すことで調べられる。
 5 ffmpeg -y -i input1.mp4 -filter_complex "
 6 color=c=black:s=1920x1080:d=3.5 [padvideo];
 7 sine=frequency=440:sample_rate=44100:d=3.5 [padaudio_left];
 8 sine=frequency=880:sample_rate=44100:d=3.5 [padaudio_right];
 9 [padaudio_left][padaudio_right]amerge=inputs=2[padaudio];
10 [padvideo][padaudio] [0:0][0:1] concat=n=2:v=1:a=1" \
11 -ac 2 output6.mp4
例によってコマンドラインに書ききれないのでスクリプトにしてるだけ
 1 #! /bin/sh
 2 
 3 # 1920x1080、44100 などの値は (今の場合の)input1.mp4 にあわせること。
 4 # 例えば ffprobe や ffmpeg に入力ファイルだけ渡すことで調べられる。
 5 ffmpeg -y -i input1.mp4 -filter_complex "
 6 testsrc=s=1920x1080:d=3.5 [padvideo];
 7 sine=frequency=440:sample_rate=44100:d=3.5 [padaudio_left];
 8 sine=frequency=880:sample_rate=44100:d=3.5 [padaudio_right];
 9 [padaudio_left][padaudio_right]amerge=inputs=2[padaudio];
10 [padvideo][padaudio] [0:0][0:1] concat=n=2:v=1:a=1" \
11 -ac 2 output7.mp4

オレ式本題: 4つの動画を4つ同時再生、の、setpts、adelay でない解

ffmpeg で複数動画結合で気に食わないと言った以下:

 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

その気に食わない結果:

無論今回のパディングを使おうぜっ、て話だが、「パディングした結果ファイルを入力として」というのではなく、一気に済ませたい、てこと。

ちょっと10分は頭掻き毟ったが、まぁこういう凄まじいので意図通り:

 1 #! /bin/sh
 2 
 3 # 1: 1.9
 4 # 2: 1.0
 5 # 3: 1.0
 6 # 4: 0
 7 
 8 ffmpeg -y -i i1.mp4 -i i2.mp4 -i i3.mp4 -i i4.mp4 -filter_complex "
 9 color=c=black:s=960x540:d=1.9 [padv0];
10 sine=frequency=0:sample_rate=44100:d=1.9 [pada_l0];
11 sine=frequency=0:sample_rate=44100:d=1.9 [pada_r0];
12 [pada_l0][pada_r0]amerge=inputs=2[pada0];
13 [0:v]scale=960:540[v0];
14 [padv0][v0] concat=n=2:v=1:a=0 [vc0];
15 [pada0][0:a] concat=n=2:v=0:a=1 [ac0];
16 
17 color=c=black:s=960x540:d=1.0 [padv1];
18 sine=frequency=0:sample_rate=44100:d=1.0 [pada_l1];
19 sine=frequency=0:sample_rate=44100:d=1.0 [pada_r1];
20 [pada_l1][pada_r1]amerge=inputs=2[pada1];
21 [1:v]scale=960:540[v1];
22 [padv1][v1] concat=n=2:v=1:a=0 [vc1];
23 [pada1][1:a] concat=n=2:v=0:a=1 [ac1];
24 
25 color=c=black:s=960x540:d=1.0 [padv2];
26 sine=frequency=0:sample_rate=44100:d=1.0 [pada_l2];
27 sine=frequency=0:sample_rate=44100:d=1.0 [pada_r2];
28 [pada_l2][pada_r2]amerge=inputs=2[pada2];
29 [2:v]scale=960:540[v2];
30 [padv2][v2] concat=n=2:v=1:a=0 [vc2];
31 [pada2][2:a] concat=n=2:v=0:a=1 [ac2];
32 
33 [3:v]scale=960:540[v3];
34 
35 [vc0][vc1]hstack[1v];
36 [vc2][v3]hstack[2v];
37 [1v][2v]vstack[v];
38 
39 [ac0][ac1][ac2][3:a]amix=inputs=4[a]
40 " -map '[v]' -map '[a]' -ac 2 \
41   merged.mp4

なかなかこれを元に自分の場合用に書き直すのも大変だろうが、ラベルの対応関係を地道に追いかければわからんこともないかなとは思う。今の場合 i4.mp4 (右下に配置してるヤツ)が scale 以外は未加工で、残り3つにパディングしているが、i3.mp4 (右上に配置してるヤツ)基準ならどうなるか、考えてみるといい。


2018-06-20追記:
最後の「[ac0][ac1][ac2][3:a]amix=inputs=4[a]」部分、「[ac0][ac1][ac2][3:a]amerge=inputs=4[a]」の方が適切。(ここまでの流れと同じノリで波形をみてわかった。)

2018-06-20さらに追記:
amerge のほうが左右チャンネルの分離がよりそれらしく、amix はモノラルに近いものになる、というのはそうだったのだが、ほかので適用してみて、「amerge しときゃオッケー」とばかりも言ってられないことに気付いた。

例えばこういう二つの動画があると思って欲しい:

1 movie1 +-----|-----o-----|-----+-----|-----+-----|-----+-----|-----+-----|-----+
2                    ^
3                    |sync point
4                    V
5 movie2             o-----|-----+-----|-----+-----|-----+-----|-----+-----|

この場合、上に挙げた処理をすると、movie2 の頭にはパディングされるが、お尻には追加しておらず、shortest の指定もしていないので、末尾は movie1 だけが動き続けることになる。この時間帯、amix の場合は movie1 の音がそのまま採用されるかたちになるのだが、amerge だと無音になってしまう。

おそらく末尾にもパディングをするしかないと思うけれど、終端処理を指示出来るみたいなので、そっちでもイケたりするかな? まぁ末尾にもパディングするのが一番素直だろうけどね。

2018-06-20さらに追記:
さすがに手作業がつらいので、フィルタグラフを構築するスクリプトを取り急ぎ書いてみた:

  1 # -*- coding: utf-8 -*-
  2 class _StreamWithPadBuilder(object):
  3     def __init__(self, idx, w=960, h=540, sample_rate=44100):
  4         self._idx = idx
  5         self._tmpl_padv = (
  6             "color=c=black:s=%dx%d:d={duration:.3f}" % (w, h),
  7             "[{prepost}padv%d]" % idx)
  8         self._tmpl_pada = (
  9             """\
 10 sine=frequency=0:sample_rate={sample_rate}:d={{duration:.3f}} [{{prepost}}pada_l{idx}];
 11 sine=frequency=0:sample_rate={sample_rate}:d={{duration:.3f}} [{{prepost}}pada_r{idx}];
 12 [{{prepost}}pada_l{idx}][{{prepost}}pada_r{idx}]amerge=inputs=2""".format(
 13                 sample_rate=sample_rate, idx=idx),
 14             "[{{prepost}}pada{idx}]".format(
 15                 idx=idx))
 16         self._bodyv = (
 17             "[%d:v]scale=%d:%d" % (idx, w, h),
 18             "[v%d]" % idx)
 19         self._bodya = (
 20             "",
 21             "[%d:a]" % idx)
 22         self._result = []
 23         self._last_out_v = []
 24         self._last_out_a = []
 25 
 26     def _add_prepost(self, duration, prepost):
 27         if duration <= 0:
 28             return
 29         self._result.append(
 30             self._tmpl_padv[0].format(prepost=prepost, duration=duration))
 31         self._result.append(
 32             self._tmpl_padv[1].format(prepost=prepost))
 33         self._result.append(";\n")
 34         self._last_out_v.append(self._tmpl_padv[1].format(prepost=prepost))
 35         #
 36         self._result.append(
 37             self._tmpl_pada[0].format(prepost=prepost, duration=duration))
 38         self._result.append(
 39             self._tmpl_pada[1].format(prepost=prepost))
 40         self._result.append(";\n")
 41         self._last_out_a.append(self._tmpl_pada[1].format(prepost=prepost))
 42 
 43     def add_pre(self, duration):
 44         self._add_prepost(duration, "pre")
 45         return self
 46 
 47     def add_body(self):
 48         self._result.append(
 49             self._bodyv[0])
 50         self._result.append(
 51             self._bodyv[1])
 52         self._result.append(";\n")
 53         self._last_out_v.append(self._bodyv[1])
 54         #
 55         self._last_out_a.append(self._bodya[1])
 56         return self
 57 
 58     def add_post(self, duration):
 59         self._add_prepost(duration, "post")
 60         return self
 61 
 62     def build(self):
 63         if len(self._last_out_v) > 1:
 64             # concat
 65             self._result.append("""\
 66 {iseqs} concat=n={nseqs}:v=1:a=0 {out}""".format(
 67                 iseqs="".join(self._last_out_v),
 68                 nseqs=len(self._last_out_v),
 69                 out="[vc{}]".format(self._idx)))
 70             self._result.append(";\n")
 71             self._last_out_v = ["[vc{}]".format(self._idx)]
 72             #
 73             self._result.append("""\
 74 {iseqs} concat=n={nseqs}:v=0:a=1 {out}""".format(
 75                 iseqs="".join(self._last_out_a),
 76                 nseqs=len(self._last_out_a),
 77                 out="[ac{}]".format(self._idx)))
 78             self._result.append(";\n")
 79             self._last_out_a = ["[ac{}]".format(self._idx)]
 80         #
 81         return (
 82             "".join(self._result),
 83             self._last_out_v[0],
 84             self._last_out_a[0])
 85 
 86 
 87 class _StackFourVideosFilterGraphBuilder(object):
 88     def __init__(self, w=960, h=540, sample_rate=44100):
 89         self._builders = []
 90         for i in range(4):
 91             self._builders.append(
 92                 _StreamWithPadBuilder(i, w, h, sample_rate))
 93 
 94     def set_paddings(self, idx, pre, post):
 95         self._builders[idx].add_pre(pre)
 96         self._builders[idx].add_body()
 97         self._builders[idx].add_post(post)
 98 
 99     def build(self):
100         result = []
101         _r = []
102         for i in range(4):
103             _r.append(self._builders[i].build())
104             result.append(_r[i][0] + "\n")
105 
106         # hstack, hstack
107         #   hstack r[0] and r[1]
108         result.append(
109             "{}{}hstack[1v];\n".format(_r[0][1], _r[1][1]))
110         #   hstack r[2] and r[3]
111         result.append(
112             "{}{}hstack[2v];\n".format(_r[2][1], _r[3][1]))
113         # vstack
114         result.append("[1v][2v]vstack[v];\n")
115 
116         # audio amerge
117         result.append("{}amerge=inputs=4[a]".format(
118                 "".join([_ri[2] for _ri in _r])))
119         #
120         return "".join(result)
121 
122 
123 if __name__ == '__main__':
124     b = _StackFourVideosFilterGraphBuilder()
125     b.set_paddings(0, 1.1, 2.2)
126     b.set_paddings(1, 3.3, 4.4)
127     b.set_paddings(2, 2.3, 0)
128     b.set_paddings(3, 0, 0)
129     print(b.build())

頭に埋めるパディングの duration をこれで取得し、お尻に埋めるパディングの duration を ffprobe を呼び出して自動で計算し…、というところまでやれば全自動も可能だが、ひとまずはこれら情報が全て既知と仮定した場合の filter_complex に書く内容だけを作り出す処理、ね。

2018-06-20さらに追記:
作った、それなりのを。仮で Gist に置いといた

2018-06-29追記: simple_stack_videos_by_sound_track

align-videos-by-sound パッケージのひとつのアプリケーションサンプルとして simple_stack_videos_by_sound_track が出来た。まだドキュメントはないけど使い方は簡単。

というかまずはインストールだけれど、setup.py があるのでわかるよね? インストールしたとして:

1 [me@host: ~]$ simple_stack_videos_by_sound_track 1.mp4 2.mp4 3.mp4 4.mp4 --mode=direct
2    ...
3 [me@host: ~]$ simple_stack_videos_by_sound_track 1.mp4 2.mp4 --shape='[2, 1]' --mode=direct
4    ...

とか。