yt-dlp & ffmpeg で1分に速縮した動画を生成する shell script

こちらは sq
という Bash スクリプト.YouTube 動画を速めて縮めて1分に squish (押し潰す) する.
コード
#!/bin/bash
# 引数は [YouTube動画のURL] と [出力ファイル名] と [bgm.mp3 の絶対パス]
# yt-dlp と ffmpeg が前提
shorten () {
# output file
short=$1_short.mp4
# get video duration
duration=`ffmpeg -i "$1" 2>&1 | grep "Duration"| cut -d ' ' -f 4 | sed s/,// | sed 's/\..*//g' | awk '{ split($1, A, ":"); print 3600*A[1] + 60*A[2] + A[3] }'`
# change duration into $vid_sec and mute it
ffmpeg -y -i "$1" -filter:v "setpts=$vid_sec*PTS/$duration" -an "$short"
echo $short
}
##################################################
# Parameters setting
vid_sec=60 # output video duration in sec
fadetime="3" # fade out duration in sec
# file names ... used in command line
bgm=$3 # bgm file path
video=$2.mp4
sound=$2_sound.mp4
##################################################
# 1 download video
yt-dlp -o $video -f mp4 $1
# 2 shorten video
short=`shorten $video`
##################################################
# 3 add bgm and thumnbail
# 3.1 download thumbnail
thumbnail=$2.jpg
id=`echo $1 | rev | cut -c 1-11 | rev`
wget -O $thumbnail https://img.youtube.com/vi/$id/maxresdefault.jpg
# 3.2 change thumbnail size as same as video resolution
# 3.2.1 get x size of "${short}"
width=`ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=nw=1:nk=1 "$short"`
# 3.2.2 scale thumbnail as video size
thumb_scaled="$2_scaled.jpg"
ffmpeg -y -i "$thumbnail" -vf scale="$width":-1 "$thumb_scaled"
# 3.3 add thumbnail at beginning and bgm
ffmpeg -y -i "$short" -i "$thumb_scaled" -filter_complex "[0:v][1:v]overlay=0:0:enable=between(t\,0\,3)" -i $bgm -shortest "$sound"
##################################################
# 4 fade out at end
# 4.1 get frame rate
fr=`ffmpeg -i "$sound" 2>&1 | sed -n "s/.*, \(.*\) fp.*/\1/p"`
fadestarttime=`expr $vid_sec - $fadetime`
fadestartframe=`echo "scale=5; $fadestarttime * $fr" | bc`
fadingframe=`echo "scale=5; $fadetime * $fr" | bc`
# 4.2 fade out at end
out=${3%/*}/$2.mp4 # output in directory of bgm.mp3
ffmpeg -y -i "$sound" -vf fade=out:$fadestartframe:$fadingframe -af afade=out:st=$fadestarttime:d=$fadetime "$out"
##################################################
# remove all intermediate files
rm $short $thumb_scaled $sound $thumbnail $video
echo $out
exit 0
処理
入力は3つ,「Youtube 動画の URL」「出力ファイル名」「bgm.mp3 のパス」.こんな感じで使う.
$ sq [YouTube URL] [Output name] [Path to bgm.mp3]
そうすると4段階の処理が順に実行される.
- yt-dlp で動画をダウンロード (yt-dlp は youtube-dl のフォーク)
- 動画を早送りして1分に縮める (元動画が1分未満だと遅くなる?未確認)
- 音声を所定の bgm.mp3 に置き換え,冒頭にサムネイルを追加
- 動画末尾をフェードアウト
動画は bgm.mp3 と同じフォルダに生成される.サムネイルを付けないオプションを実装したいけど,ちょっとそれはまだできてない.
実はこのスクリプトはこの記事で紹介したものを改造したものなんだけど,実際にはあまり変わってない.変更点は,入出力の微調整と youtube-dl を廃止して yt-dlp に置き換えたことくらいかな?
動画の秒数取得はなぜこんなに難しい?
ffmpeg のオプションが難解すぎる.これはかつて自分が調べながら書いたコードだけど,とにかく何をどうしてるのか分かりにくい.これは僕のコードが下手くそだからというのもあると思うけど,単に ffmpeg が分かりにくいからだという理由もあると思う.
例えば動画の秒数を取得するだけに費やすコードの量が多すぎる.何でこんな素朴なプロパティを取得するのにこんなに文字数書かないといけないのか… 以下はそのコード (上のコードの9行目に該当) だけど,ffmpeg で取得したデータを grep してから文字列処理を4回もやってる.
duration=`ffmpeg -i "$1" 2>&1 | grep "Duration"| cut -d ' ' -f 4 | sed s/,// | sed 's/\..*//g' | awk '{ split($1, A, ":"); print 3600*A[1] + 60*A[2] + A[3] }'`
ffmpeg で情報を表示して grep
まず ffmpeg -i 動画 2>&1
で得られる出力はこんな感じ.
$ ffmpeg -i ~/Videos/sample.mpeg 2>&1
ffmpeg version 4.2.4-1ubuntu0.1 Copyright (c) 2000-2020 the FFmpeg developers
(...中略...)
Input #0, mpeg, from '/home/takeru/Videos/その他/スマホ撮影/既定のプロジェクト.mpeg':
Duration: 00:10:36.73, start: 0.022456, bitrate: 5281 kb/s
Stream #0:0[0x1e0]: Video: mpeg2video (Main), yuv420p(tv, smpte170m/unknown/smpte170m, progressive), 640x480 [SAR 1:1 DAR 4:3], 29.97 fps, 29.97 tbr, 90k tbn, 59.94 tbc
Stream #0:1[0x1c0]: Audio: mp2, 44100 Hz, stereo, s16p, 256 kb/s
At least one output file must be specified
大量に出力される情報の中から欲しい Duration だけを取り出すために grep
してみる.Duration が書かれてる行にはコンマ区切りで計3つのプロパティが書かれているので,3つとも出力されちゃう.
$ ffmpeg -i ~/Videos/sample.mpeg 2>&1 | grep Duration
Duration: 00:10:36.73, start: 0.022456, bitrate: 5281 kb/s
怒涛の文字列処理 3連発と,awk の計算
上記の出力にさらに cut -d ' ' -f 4 | sed s/,// | sed 's/\..*//g'
という3段階の処理をして,欲しい文字列 (時間,分,秒) を取り出す.
まず半角スペースで区切って前から4番目を取り出す.なぜ4番目かと言うと,Duration の前に半角スペースが2つ挿入されてるので,出力は (空文字)␣(空文字)␣Duration:␣00:10:36.73,␣...
となっているから.欲しい 00:10:36.73,
の箇所は4番目なのです.
そのあと sed s/,//
でコンマを消して,さらに
でドット以降 (= 秒の小数点以下) を消してる.ここで sed 's/\..*//g'
\.
とエスケープしてるのは,正規表現のワイルドカードとしての .
ではなく,文字としての .
にマッチしてほしいからですね.ちなみに実は,小数点以下を消す必然性はない (消さなくてもawk
の計算は問題ない) よ.
最後に awk
でコロン繋ぎの時分秒の表記をシンプルな秒数表記に変換してる.awk
に入力される 00:10:36
みたいな入力を split
で :
で区切って変数 A
に入れて,print 3600*A[1] + 60*A[2] + A[3]
と計算して出力してる.こんな感じね.
$ echo "00:10:36" | awk '{ split($1, A, ":"); print 3600*A[1] + 60*A[2] + A[3] }'
636
面倒くさすぎでしょ.もっと良いツールがきっとあるとは思うけど,知らないんだよね…😢