2019年4月28日日曜日

ffplay+ffmpegでその場で二つの動画の右半分と左半分を同時に再生する方法

この記事では、エンコード前の動画とエンコード済みの動画の画質の差や、同一の動画を違う形式でエンコードしたときの画質の差を、それぞれの動画を単純に左右に並べるのではなく、動画を右半分と左半分に分割して比較したい場合を想定しています。
ほら、テレビショッピングとかでよくあるじゃないですか。
同じ人の顔を画面上で半分に割って、左半分はナントカクリームを塗らなかった場合、右半分はナントカクリームでお肌ツヤツヤってやつを見せるアレ。
ああいうイメージです。

さて、画質の差は結局のところ数値化などは意味がなくて、見ている本人が納得できるかできないか、という点に尽きると思います。

まず、最も簡単な比較方法は動画プレイヤーを二つ並べて再生することです。
が、同一シーンを同時比較するためには、神業のような再生ボタン押下処理が必要になります。神ならぬ身の上、凡人にはとてもできません。

また、もしできたとしても、元画像が1440x1080だったりFHDだったりした場合、素直に横に並べると大きすぎて位置合わせが難しかったりします。だからと言って普段見るはずのサイズでチェックしないと検査になってないので、縮小等リサイズして比較するのは避けたいところです。

ということで、表題の件のように、二つの動画の左半分と右半分を同時に再生して、すぐに官能検査してみようというのが今回の目論見です。

これを行うのに必要なプログラムはffmpegとffplayだけです。
ffplayはvlc playerでも代替できます。

まず結論として、コマンドライン一式をWindowsのバッチファイルとして作成すると以下になります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
@echo off
setlocal
set SRC1=%1
set SRC2=%2
 
if not defined SRC1 goto invalidargument
 
REM remove doublequotation
set SRC1=%~1
 
if "%SRC1%"=="/?" goto help
if /i "%SRC1%" equ "/h" goto help
if /i "%SRC1%" equ "/help" goto help
if "%SRC1%"=="-?" goto help
if /i "%SRC1%" equ "-h" goto help
if /i "%SRC1%" equ "-help" goto help
 
if not defined SRC2 goto invalidargument
 
REM remove doublequotation
set SRC2=%~2
 
if not exist "%SRC1%" (
  echo エラー: %SRC1%がありません。
  goto help
)
if not exist "%SRC2%" (
  echo エラー: %SRC2%がありません。
  goto help
)
 
REM オーバーレイ文字列用ファイル名.拡張子抽出
set SRC1FNAME=%~nx1
set SRC2FNAME=%~nx2
REM 念のためアポストロフィを除去
set SRC1FNAME=%SRC1FNAME:'=%
set SRC2FNAME=%SRC2FNAME:'=%
 
REM 時間指定パラメタの記述場所判定用拡張子抽出
set SRC1EXT=%~x1
set SRC2EXT=%~x2
 
set STARTTIME=%~3
set ENDTIME=%~4
if defined STARTTIME ( set STARTTIME= -ss %STARTTIME% )
if defined ENDTIME ( set ENDTIME= -to %ENDTIME% )
 
set FFMPEG=ffmpeg.exe
set FFPLAY=ffplay.exe
set FFPROBE=ffprobe.exe
 
REM ぢでじ用パラメータ
set ASPECT=16:9
set MOVIE_WIDTH=1440
set MOVIE_HEIGHT=1080
set FPS=29.97
set CENTERLINE_WIDTH=2
 
if exist "%FFPROBE%" call :trygetparams
 
REM ファイル名表示用フォントファイル(要エスケープ: コロン及び¥)
set FONT=C\\:/WINDOWS/fonts/meiryob.ttc
REM 長いファイル名対策のため左右にテキストを流す
set DRAWTEXT_FNAME_OPT=fontfile=%FONT%:fontsize=24:fontcolor=darkgreen:shadowx=1:shadowy=1:x=w-mod(n*4\,w+tw):y=h-th-24:
REM 再生中動画のPTSとフレーム番号
set DRAWTEXT_PTS_OPT=fontfile=%FONT%:fontsize=12:fontcolor=white:shadowx=1:shadowy=1:text= pts\\:%%{pts\\:hms} %%{pts\\:flt} frame\\:%%{frame_num}
 
 
REM 再生開始及び終了時間指定
set DURATION1= %STARTTIME% %ENDTIME%
set DURATION2= %DURATION1%
REM (ffmpegではmpeg2tsは前置指定での時間指定不可なので引数の位置を後置する)
set DURATION_POSITION=PRE
if /i "%SRC1EXT%" equ ".ts" set DURATION_POSITION=POST
if /i "%SRC2EXT%" equ ".ts" set DURATION_POSITION=POST
if %DURATION_POSITION%==POST (
  set DURATION_POST=%DURATION1%
  set DURATION1=
  set DURATION2=
  echo  *警告: mpeg2tsのため指定時刻への頭出しに馬鹿みたいに時間がかかります*
  pause
)
 
set /A HALF_WIDTH=%MOVIE_WIDTH%/2
set /A RIGHT_HALF_X=%HALF_WIDTH%+%CENTERLINE_WIDTH%
 
set FILTER_OPT=^
    nullsrc=size=%MOVIE_WIDTH%x%MOVIE_HEIGHT%,fps=%FPS% [canvas]; ^
    [0:v] crop=%HALF_WIDTH%:%MOVIE_HEIGHT%:0:0,fps=%FPS%, ^
          drawtext=%DRAWTEXT_PTS_OPT% , ^
          drawtext=%DRAWTEXT_FNAME_OPT%:text=%SRC1FNAME% [left]; ^
    [1:v] crop=%HALF_WIDTH%:%MOVIE_HEIGHT%:%HALF_WIDTH%:0,fps=%FPS%,^
          drawtext=%DRAWTEXT_PTS_OPT% , ^
          drawtext=%DRAWTEXT_FNAME_OPT%:text=%SRC2FNAME% [right]; ^
    [canvas][left] overlay=x=0:shortest=1 [lefthalf]; ^
    [lefthalf][right] overlay=x=%RIGHT_HALF_X%:shortest=1
 
%FFMPEG% %DURATION1% -i "%SRC1%" %DURATION2% -i "%SRC2%" ^
 -filter_complex "%FILTER_OPT%" -f nut ^
 -aspect %ASPECT% -c:a copy -c:v rawvideo %DURATION_POST% pipe:1 ^
 | %FFPLAY% -i pipe:0 -fflags nobuffer
REM | %FFPLAY% -i pipe:0 -fflags nobuffer
REM VLCの場合は、「 | vlc.exe -」
goto end
 
:trygetparams
REM ffprobeの出力からfilter_complexの各値の取得を試みる
set FFPROBEOPT=%FFPROBE% -i "%SRC1%" -of csv -show_streams -hide_banner
REM ffprobeの結果をcsv形式で受け取り、カンマで分割した10,11,16,28の要素を取得
REM ※ここで直接リダイレクト先とパイプを指定しないとうまくいかないようだ
REM ※おまけにexeのパス名にスペースが入っていてもまともに動かないと来た。
for /f "usebackq tokens=10,11,16,28 delims=," %%a in (`%FFPROBEOPT% 2^>NUL ^| findstr video`) do (
  set MOVIE_WIDTH=%%a
  set MOVIE_HEIGHT=%%b
  set ASPECT=%%c
  set FPS=%%d
)
 
echo   ffprobe結果;
echo     size=%MOVIE_WIDTH%x%MOVIE_HEIGHT%
echo     aspect=%ASPECT%
echo     fps=%FPS%
pause
exit /b
 
:invalidargument
echo エラー: 引数が不足しています
:help
echo 使い方: %0 左半分を表示する動画 右半分を表示する動画 [ 再生開始時刻 [ 再生終了時刻 ] ]
echo         再生開始・終了時刻書式 HH:MM:SS または 秒
:end
endlocal
上記の内容を"左右2分割.bat"とでも名前を付けて保存して実行してください。使い方が表示されます。
ffmpegの特徴は複雑なことができる代償として呪文のような意味不明なコマンドラインを構築しなければならないところですが、今回も例にもれず一見意味不明な呪文が並びます。
おまけに当方の環境の都合で(サーバにはAV機能が皆無なので)Windows用のバッチとしたため、シェルスクリプトに比べて極端にひねくれています。
この両者の強力な組み合わせによって、上記のバッチは心の底からうんざりさせてくれるに十二分の威力を備えています。

さて、バッチの書式とか細かいことは放っておいて、重要なのは画像をそれぞれ左半分だけ、右半分だけ、それぞれ切り出して一枚の画像にすることですが、それをffmpegの -filter_complexオプションで行います。
このパラメータはffmpegの中でも極端に面倒くさいので、冗長になることを恐れずに説明したいと思います。

なお、行末の^(サーカムフレックス)はWindowsのcmd.exeで1行で記述しなければならない行を複数行に分割する記号ですので無視してください。
  1. 88行目のnullsrc=ですが、ffmpegが予め用意している多くの入力ソースの中の一つです。ここではこの上に2枚の動画を重ねてゆくベース動画として利用します。
    パラメータはsizeとfpsを指定しています。
    このバッチではsizeは最終的に出力される動画サイズ(1440x1080)、fpsは29.97が指定されています。これらの値はエンコード元が地デジのtsと仮定しています。ことさらにfpsを指定しているのはffmpegは無指定だと25fpsとして処理するからです。
    そして、このnullsrcから入力して加工(sizeとfps)した動画に対して[canvas]と名付けました。これの名づけルールは自由です。
    ここまででベース動画に対する定義は終わりました。続きがあるためセミコロンをつけます。
  2. 89行目の[0:v]ですが、これは1つ目の入力ソース(動画ファイル)コンテナ中で最初に見つかった動画を表しています。これを名付けたのはffmpegです。これに対してcropとfpsとdrawtextの指定をしています。
    cropは切り抜き範囲を指定します。このパラメータはffmpegのバージョンによって解釈される意味合いが変わるというとんでもない代物なので要注意です。
    バージョン20190426-f857753では切り抜き幅:同高さ:元動画に対する切り抜き開始位置X:同Yの順で解釈されます。
    上記のバッチでは720:1080:0:0として左半分を切り抜いています。言い換えると、1つ目の入力ソースの座標(左から0,上から0)から(幅720,高さ1080)だけ切り抜け、という意味合いになります。
    drawtextパラメータはどちらのファイルを切り抜いたかわかるようにファイル名を動画にオーバーレイするためのものです。
    以上を[left]と名付けました。これも名づけルールは自由です。続きがあるためセミコロンをつけます。
  3. 92行目の[1:v]ですが、これは2つ目の入力ソース(動画ファイル)中で最初に見つかった動画を表しています。これを名付けたのはffmpegです。これに対してもcropとfpsとdrawtextの指定をしています。
    異なるのは切り抜き位置で、上記のバッチでは720:1080:720:0を指定して右半分を切り抜いています。2つ目の入力ソースの座標(左から720,上から0)から(幅720,高さ1080)だけ切り抜けという意味合いになります。
    以上を[right]と名付けました。これも名づけルールは自由です。続きがあるためセミコロンをつけます。
  4. 95行目の[canvas][left]ですが、これは私がつけた名前です。この二つを重ね合わせるため、overlay=x=0:shortest=1というパラメータを指定しています。重ねる順番は左から順に重なります。この例では[canvas]の上に[left]が重なります。
    x=0は[left]を[canvas]上のどこに重ねるのかを指定しています。この場合は重ね合わせのx座標を0、つまり一番左としています。本来はこの記述は不要ですが、次の手順で右側に重ね合わせる際の座標指定との対比を分かりやすくするために付与しています。
    shortest=1はどちらかの動画の再生が終わったら処理を完了するというおまじないです。[canvas]の動画はnullsrc由来でdurationを指定していないので無限に続きますので、[left]の再生が終わったら完了するという意味になります。
    以上を[lefthalf]と名付けました。これも名づけルールは自由です。続きがあるためセミコロンをつけます。
  5. 96行目の[lefthalf][right]ですが、これは私がつけた名前です。この二つを重ね合わせるため、 overlay=x=%RIGHT_HALF_X%:shortest=1というパラメータを指定しています。重ねる順番は左から順に重なります。この例では[lefthalf]の上に[right]が重なります。
    x座標に指定している %RIGHT_HALF_X% の値は722ですので、2ピクセル分(本バッチの環境変数"CENTERLINE_WIDTH=2"として定義しています)だけ右にずれて重ね合わされます。これによって、隙間に[canvas]の地の動画が出ます(緑単色)。これは、左右の動画がどこで分割されているのかを分かりやすくするためにあえて行っています。
    以上でベース動画に右半分と左半分をオーバーレイ出来ましたので続きはありませんのでセミコロンをつけません
    なお、ここでセミコロンをつけますとffmpeg様に激烈に叱られた挙句ABENDしてくださいます。
というようなことをやった挙句、ようやく--filter_complex=オプションにダブルクォーテーションで囲んで指定します。

ここまでで2つの入力ソース(動画ファイル)が合成されているので、これをどこかに吐き出さないと人間様には見えません。
今回はいったんファイル化するのではなく、いきなり再生してしまおうというのが趣旨ですので、ffmpegにもれなくついてくるffplayで直接再生させたいと思います。

そのためのオプションは
-f nut -aspect %ASPECT% -c:a copy -c:v rawvideo pipe:1 | %FFPLAY% -i pipe:0 -fflags nobuffer
となります。

出力フォーマットにnut, ソースのサイズは1440x1080なのでぢでじのアスペクト比16:9を明示的に指定して、音声は再変換せずコピーして動画のコーデックをrawvideoとしてパイプ経由でffplayに出力しています。
vlc playerで再生したい場合は、パイプでつなぐアプリをffplayからvlc -(ハイフン)に置き換えてください。こちらもstdinから読み込んで再生してくれます。
率直に言ってffplayはとても使いづらいので、普段ffplayで動画を見ている人というのも少ないでしょうし、vlcも多くのOSに移植されていますから、vlcで官能試験を行ったほうが普段の視聴環境により近いかもしれません。

出力フォーマットのnutというのは単純にピクセルの羅列を扱えますので、rawvideoな動画を扱う際のフォーマットとして優れています。お望みであればこれをファイルに保存してもいいのですが、大変愉快なファイルサイズになります。

蛇足ですが、上記のバッチの例をお読みになれば上下分割や四分割なんかも簡単にできることはご理解いただけると思いますので、見やすいように改造してみてください。

なお、入力ソース次第では-ssが効かない場合があるとか使用するffmepgのバージョンが違うので挙動が違うとかザラです。
文中にもありますが、本稿は20190426-f857753を対象に記事を書いています。

個人的にはffmpegって強力で頼もしいけど融通は利かないしコロコロ仕様が変わるんで尊敬はするけど近づきたくはないです。

以上、大変くだらない記事で恐縮ですが、ここまでお読みいただいてありがとうございました。

以下、駄文です。
なんでこんなことをしようと思ったのかというと、HandBrakeのバージョンが上がるたびに同じH.265のエンコードパラメータを与えても妙に平均ビットレートが上がってきていて、同じCRF値でも1タモリ倶楽部単位(エンコネタではタモリ倶楽部1話分を当blogの基本単位とさせていただいております)の保存ファイルサイズが100MBほど大きくなってきたのが発端です。

なぜそうなるのかはHanbBrakeのせいなのか、libx265のせいなのかはわかりませんが、同一CRF値を指定してもエンコードされたファイルの平均ビットレートが上がっているのでファイルサイズが大きくなっているのが原因なのはわかったので、そっちが同じパラメータでも挙動を変えようって魂胆なら、こっちはもうCRF値を下げちまえ、もともとCRF値は28という(劣化に厳しい諸兄にはありえない)数値でエンコードしていましたが、今回は大台の30にして比較してみよう、でも画質はどうなるのかな?という素朴な疑問を持ったため、尊敬はするけど近寄りたくないffmpeg様のお力を拝借することにした次第です。

やってみてわかったことは、H.265の低ビットレートにおける再現力の高さです。
確かにわずかに28と30では違うかもしれないが、そもそもいずれもオリジナルのtsとの差ともわずかだ、という官能評価に至りました。
ぶっちゃけ、個人的評価ですがエンコ元先を左右に並べても上下に並べてもいずれも全然気にならないことが確認できました。

HEVC、恐るべし。そんなことをつらつら思う平成最後の日曜日でございます。
駄文までお読みいただき、誠にありがとうございました。

0 件のコメント:

コメントを投稿