YouTube Shorts や TikTok に縦型動画をアップロードすると、プラットフォーム側で必ず再エンコードされます。元ファイルの解像度やフレームレート、音声の形式やラウドネスがずれていると、再エンコード後にさらに目立つことがあります。この記事では、ffprobe と ffmpeg の loudnorm フィルタ、jq を組み合わせて、9:16 のショート動画を投稿前にチェックする小さなシェルスクリプトを作ります。

チェック対象のクリップ。AI 生成の例で、人物は AI 生成のポートレートです。
なぜ投稿前にチェックするのか
縦型ショートは、アップロードした時点でプラットフォームごとの設定に変換されます。元のファイルが 720 × 1280 なら拡大され、44.1 kHz の音声なら 48 kHz に変換され、音量が大きすぎればプラットフォーム側で下げられます。どれも自動で行われるので気づきにくいのですが、変換が重なるほど画質や音質は少しずつ落ちていきます。
また、投稿後に「思ったよりぼやけている」「ほかの動画より音が小さい」と気づいても、多くのプラットフォームでは動画の差し替えができません。消して上げ直すと、再生数やコメントもリセットされます。だからこそ、アップロードする前の数秒のチェックに意味があります。
ここで作るスクリプトは、書き出しのたびに実行することを想定しています。手で数値を読むより速く、見落としもありません。
対象のファイル
例として、次の2つのファイルを使います。
-
raw.mp4:AI エフェクトで生成したそのままの 15 秒クリップ(720 × 1280) -
master.mp4:投稿用に 1080 × 1920 へ拡大し、冒頭にタイトル、最後にエンドカードを足した 17.5 秒版
チェックしたい項目
縦型ショートで問題になりやすいのは、だいたい次の6つです。
- 解像度:1080 × 1920(9:16)
- フレームレート:30fps 固定
- ピクセルフォーマット:
yuv420p - 音声:AAC、48 kHz、ステレオ
- ラウドネス:-14 LUFS 前後(ここでは -15〜-13 を合格にします)
- トゥルーピーク:-1 dBTP 以下
1. 映像ストリームを見る
ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate,pix_fmt \
-of csv=p=0 master.mp4
1080,1920,yuv420p,30/1
-of csv=p=0 にすると1行で返ってくるので、シェルで扱いやすくなります。注意点として、列の順番は -show_entries に書いた順ではなく、ffprobe 内部のフィールド順になります。上の例では pix_fmt が r_frame_rate より先に出ています。スクリプトで read するときはこの順番に合わせます。
raw.mp4 の場合は次のとおりです。
720,1280,yuv420p,30/1
比率は同じ 9:16 ですが、720 × 1280 のままだとプラットフォーム側で拡大されて甘く見えやすいので、投稿用には 1080 × 1920 に拡大したファイルを使います。
2. 音声ストリームを見る
ffprobe -v error -select_streams a:0 \
-show_entries stream=codec_name,sample_rate,channels \
-of csv=p=0 master.mp4
aac,48000,2
44.1 kHz やモノラルだった場合は、書き出し直しておくと安心です。
3. ラウドネスを JSON で取る
loudnorm フィルタは、print_format=json を付けると測定結果を JSON で標準エラーに出します。正規化はせずに、測定だけに使います。
ffmpeg -hide_banner -nostats -i master.mp4 \
-af loudnorm=print_format=json -f null - 2>&1 \
| sed -n '/^{/,/^}/p' | jq '{input_i, input_tp, input_lra}'
{
"input_i": "-14.95",
"input_tp": "-3.36",
"input_lra": "3.30"
}
input_i が統合ラウドネス(LUFS)、input_tp がトゥルーピーク(dBTP)です。値が文字列で返ってくる点に注意してください。比較するときは awk などで数値として扱います。
なお、ebur128 フィルタでも測れますが、こちらは結果がテキストなので、スクリプトで使うなら loudnorm の JSON のほうが取り出しやすいです。測定アルゴリズムの違いで、小数点以下が少しずれることがあります(今回の master.mp4 は ebur128 で -14.8 LUFS、loudnorm で -14.95 LUFS)。
4. まとめてチェックするスクリプト
上の3つをまとめて、OK / NG を1行ずつ出すスクリプトにします。
#!/usr/bin/env bash
# usage: ./check_short.sh input.mp4
set -euo pipefail
f="$1"
v=$(ffprobe -v error -select_streams v:0 \
-show_entries stream=width,height,r_frame_rate,pix_fmt \
-of csv=p=0 "$f")
a=$(ffprobe -v error -select_streams a:0 \
-show_entries stream=codec_name,sample_rate,channels \
-of csv=p=0 "$f")
IFS=, read -r w h pix fps <<< "$v"
IFS=, read -r ac sr ch <<< "$a"
lj=$(ffmpeg -hide_banner -nostats -i "$f" \
-af loudnorm=print_format=json -f null - 2>&1 \
| sed -n '/^{/,/^}/p')
il=$(echo "$lj" | jq -r .input_i)
tp=$(echo "$lj" | jq -r .input_tp)
ok() { if [ "$1" = 1 ]; then echo "OK $2"; else echo "NG $2"; fi; }
ok $([ "$w" = 1080 ] && [ "$h" = 1920 ] && echo 1 || echo 0) "size ${w}x${h}"
ok $([ "$fps" = 30/1 ] && echo 1 || echo 0) "fps $fps"
ok $([ "$pix" = yuv420p ] && echo 1 || echo 0) "pix_fmt $pix"
ok $([ "$ac" = aac ] && [ "$sr" = 48000 ] && [ "$ch" = 2 ] \
&& echo 1 || echo 0) "audio $ac $sr Hz ${ch}ch"
ok $(awk -v i="$il" 'BEGIN{print (i>=-15 && i<=-13)?1:0}') \
"loudness $il LUFS"
ok $(awk -v t="$tp" 'BEGIN{print (t<=-1)?1:0}') "true peak $tp dBTP"
実行結果です。まず投稿用の master.mp4:
OK size 1080x1920
OK fps 30/1
OK pix_fmt yuv420p
OK audio aac 48000 Hz 2ch
OK loudness -14.95 LUFS
OK true peak -3.36 dBTP
次に生成直後の raw.mp4:
NG size 720x1280
OK fps 30/1
OK pix_fmt yuv420p
OK audio aac 48000 Hz 2ch
NG loudness -15.65 LUFS
OK true peak -4.45 dBTP
raw.mp4 は解像度とラウドネスで NG になりました。どちらも投稿用の書き出しで直している項目なので、アップロードするのは master.mp4 のほうだと一目でわかります。
NG だったときの直し方
よくある NG と、ffmpeg での直し方をまとめておきます。どれも再エンコードになるので、元ファイルは残しておきます。
解像度が足りないときは、Lanczos で 1080 × 1920 に拡大します。拡大しても細部が増えるわけではありませんが、プラットフォームの拡大より結果が安定します。
ffmpeg -i raw.mp4 -vf scale=1080:1920:flags=lanczos \
-c:a copy master.mp4
ラウドネスがずれているときは、loudnorm で -14 LUFS、トゥルーピーク -1.5 dBTP を目標に正規化します。1 回目の測定値を2回目に渡す2パス方式のほうが正確ですが、まずは1パスでも十分です。
ffmpeg -i in.mp4 -c:v copy \
-af loudnorm=I=-14:TP=-1.5:LRA=11 -ar 48000 out.mp4
フレームレートが 30fps でないときは、-r 30 で固定します。可変フレームレートのスマホ動画をそのまま使うと、プラットフォームによっては音ズレの原因になります。音声がモノラルのときは -ac 2 でステレオにします。
5. 複数ファイルを回す
ファイルが増えてきたら、ループで回して NG だけを拾います。
for f in out/*.mp4; do
echo "== $f"
./check_short.sh "$f" | grep '^NG' || echo "all OK"
done
書き出しスクリプトの最後にこのループを足しておけば、NG があったときだけ目に入ります。CI のように終了コードで判定したい場合は、スクリプトの最後で NG の数を数えて exit 1 するように変えると扱いやすくなります。
このチェックでわからないこと
-
文字の位置: 縦型の画面では、下の約 20%、右の約 15%、上の 8〜10% にボタンや説明文が重なります。ffprobe では判定できないので、フレームを書き出して目で確認します(
ffmpeg -i master.mp4 -frames:v 1 f0.png)。 - 最初のフレーム: プラットフォームによっては 0 秒目のフレームがサムネイルになります。上のコマンドで 0 秒目を確認しておきます。
- 音楽の権利: 技術的なチェックをすべて通っても、元の曲が第三者の音楽なら著作権の申し立てが付くことがあります。今回のクリップも元の動画の音声をそのまま使っています。
- 口の動き: 生成動画では口の動きと音が少しずれることがあるので、最後まで再生して確認します。
合格ラインについて
今回は -14 LUFS ±1 と -1 dBTP 以下を合格にしましたが、これは主要なショート動画プラットフォームの正規化の目安に合わせた値です。各プラットフォームは大きすぎる音を自動で下げる一方、小さすぎる音を上げないこともあるので、-14 より少し小さい程度なら問題ありませんが、-18 LUFS などかなり小さい場合は、ほかの動画と並んだときに聞き劣りします。合格ラインは、自分が投稿するプラットフォームに合わせて変えてください。スクリプトの awk の条件を書き換えるだけで調整できます。
まとめ
-
ffprobe -of csv=p=0で映像と音声のパラメータを1行で取る(列の順番に注意)。 -
loudnorm=print_format=jsonとjqでラウドネスとトゥルーピークを数値で取る。 - OK / NG を出すスクリプトにしておくと、生成直後のファイルと投稿用のファイルの取り違えを防げる。
この記事の例のクリップは MixVio の Bulin 47 AI で生成しました。
これは MixVio の独立した AI エフェクトで、第三者のアーティストや音楽の権利者とは提携していません。
開示:この記事は MixVio チームが AI の補助を受けて執筆しました。例の動画は AI 生成のポートレートから作ったものです。