テレビ録画メディアサーバー構築入門(第22回)
前回の続き(効率化)
EPGStationで録画した動画 m2ts をmp4に変換して、
NHK以外ならCMカットしたmp4に変換している。
今回のテーマは、この2段目の動画編集工程の効率化・負荷軽減です。
工夫1: QSVデコード
ffmpegで -hwaccel qsv オプションを使えるようにする。
`-hwaccel qsv` オプションは、FFmpeg で Intel Quick Sync Video (QSV) を利用してハードウェアアクセラレーションを行うための重要なオプションです。適切に使用することで、動画のエンコード・デコード処理を大幅に高速化できます。ただし、QSV を利用するには、Intel 製 CPU が必要であり、FFmpeg が QSV に対応している必要があります。
残念ながら、Ubuntu 22.04では以下のように怒られる:
[AVHWDeviceContext @ 0x55923ba54780] libva: This version of libva doesn't support retrieving the device information from the driver. Please consider to upgrade libva to support VA-API 1.15.0
なので、既存のパッケージ版 (libva2/libva-dev) を残したまま、ユーザ領域にソースからビルドして ffmpeg にリンクさせる。
工夫2: カット処理
番組本編部分の切り出し方を工夫する。
ffmpegでは、-ss をファイル入力 -i の前に置くか
(-ss "$STARTSEC" -t "$DURATION" -i "$INPUT")
後に置くか
(-i "$INPUT" -ss "$STARTSEC" -t "$DURATION")
で挙動が変わります。前に置くほうが処理速度は早いが、後に置くほうが処理精度は高い。
両者の良いとこ取りをするように、「$STARTSEC マイナス5秒」までは-ss -i の順序でシークして、そこから5秒後から -i -ss 5 -t "$DURATION" で切り出すことにする。
(-ss "$START_BEFORE" -i "$INPUT" -ss 5 -t "$DURATION" )
前回の方法よりは精度が落ちるが、特に長時間の番組で、速度向上のメリットが大きい。
作業
libvaをソースからローカルにインストールする。
sudo apt install libdrm-dev=2.4.110-1ubuntu1 libdrm2=2.4.110-1ubuntu1 libdrm-intel1=2.4.110-1ubuntu1 libdrm-radeon1=2.4.110-1ubuntu1 libdrm-amdgpu1=2.4.110-1ubuntu1 libdrm-nouveau2=2.4.110-1ubuntu1
sudo apt-get install -y meson ninja-build build-essential pkg-config libdrm-dev wayland-protocols libwayland-dev libx11-dev libxext-dev libxfixes-dev libgl1-mesa-dev
git clone https://github.com/intel/libva.git
cd libva/
cd build
meson setup .. --prefix=\$HOME/build/ffmpeg_build --libdir=lib
ninja
ninja installffmpegを再コンパイルする。
cd ~/ffmpeg_611
make distclean
PKG_CONFIG_PATH="$HOME/build/ffmpeg_build/lib/pkgconfig" ./configure --prefix="/usr/local" --pkg-config-flags="--static" --extra-cflags="-I$HOME/build/ffmpeg_build/include" --extra-ldflags="-L$HOME/build/ffmpeg_build/lib:/usr/lib/x86_64-linux-gnu" --extra-libs="-lpthread -lm" --ld="g++" --bindir="$HOME/bin" --enable-gpl --enable-version3 --enable-gnutls --enable-libaom --enable-libass --enable-libfdk-aac --enable-libfreetype --enable-libmp3lame --enable-libopus --enable-libdav1d --enable-libvorbis --enable-libx264 --enable-shared --enable-avisynth --enable-vaapi --enable-libvpx --enable-nonfree --disable-doc --disable-debug --enable-libaribb24 --enable-libmfx && make -j"\$(nproc)"
sudo make install新 enc01.sh
#!/bin/bash
SOURCEDIR="$HOME/docker-mirakurun-epgstation/recorded/"
WORKDIR="$HOME/work/"
OUTDIR="$HOME/media/"
NHK1="GR26"
NHK2="GR27"
NTFY_URL="https://fearow22.taile153a7.ts.net/PX-S1UD"
#CMCUT=false #CMカットしないなら false
bc_calc() {
echo "scale=6; $1" | bc -l | awk '{printf "%.6f", $0}'
}
watch_iowait() {
MAX_WAIT=600 # 最大待機秒数
THRESHOLD=10 # 許容IOWAIT(%)
INTERVAL=5 # チェック間隔(秒)
elapsed=0
echo "Monitoring IOWAIT... (threshold=${THRESHOLD}%, max_wait=${MAX_WAIT}s)"
while true; do
# /proc/statからIOWAIT%計算
read cpu user nice system idle iowait irq softirq steal guest guest_nice < /proc/stat
sleep 1
read cpu2 user2 nice2 system2 idle2 iowait2 irq2 softirq2 steal2 guest2 guest_nice2 < /proc/stat
# 差分計算
total_diff=$(( (user2-user) + (nice2-nice) + (system2-system) + (idle2-idle) + (iowait2-iowait) + (irq2-irq) + (softirq2-softirq) + (steal2-steal) ))
iowait_diff=$(( iowait2 - iowait ))
if [ $total_diff -gt 0 ]; then
IOWAIT=$(awk "BEGIN {printf \"%.1f\", ($iowait_diff*100)/$total_diff}")
else
IOWAIT=0
fi
echo " elapsed=${elapsed}s, IOWAIT=${IOWAIT}%"
if (( $(echo "$IOWAIT < $THRESHOLD" | bc -l) )); then
echo "IOWAIT < ${THRESHOLD}%, resuming..."
break
fi
sleep "$INTERVAL"
elapsed=$((elapsed + INTERVAL))
if (( elapsed >= MAX_WAIT )); then
echo "IOWAIT did not drop below ${THRESHOLD}% after ${MAX_WAIT}s. Exiting."
curl -H "X-Priority: 3" -d "ERROR: IOWAIT > ${THRESHOLD}%: $FILENAME" $NTFY_URL
exit 1
fi
done
}
trim() {
local INPUT="$1"
local TRIMFILE="$2"
local OUTPUT="$3"
# 地上波のfpsを指定(例: 29.97)
FPS=29.97
# jls_out.txt が空なら入力をそのまま出力
if [ ! -s "$TRIMFILE" ]; then
echo "No trim info. Copying input to output..."
ffmpeg -hide_banner -loglevel error -y \
-i "$INPUT" \
-c copy -map 0 \
"$OUTPUT"
exit 0
fi
TEMPDIR=$(mktemp -d)
PARTS_LIST="$TEMPDIR/parts.txt"
INDEX=0
> "$PARTS_LIST"
# 入力に字幕ストリームがあるかチェック
if ffprobe -v error -select_streams s -show_entries stream=index \
-of csv=p=0 "$INPUT" | grep -q .; then
SUB_OPT=(-c:s copy -map 0)
else
SUB_OPT=()
fi
while read -r line; do
while [[ "$line" =~ Trim\(([0-9]+),([0-9]+)\) ]]; do
STARTF="${BASH_REMATCH[1]}"
ENDF="${BASH_REMATCH[2]}"
STARTSEC=$(bc_calc "$STARTF / $FPS")
ENDSEC=$(bc_calc "$ENDF / $FPS")
DURATION=$(bc_calc "$ENDSEC - $STARTSEC")
PART="$TEMPDIR/part_${INDEX}.mp4"
watch_iowait
if (( $(echo "$STARTSEC >= 5" | bc -l) )); then
# マージン5秒を引いた開始秒を計算
START_BEFORE=$(echo "$STARTSEC - 5" | bc -l| sed 's/^\./0./')
ffmpeg -ss "$START_BEFORE" -i "$INPUT" -ss 5 -t "$DURATION" -c:v libx264 -crf 26 -preset veryfast -c:a libfdk_aac -b:a 192k "${SUB_OPT[@]}" "$PART"
# ffmpeg -hwaccel qsv -hwaccel_output_format qsv -ss "$START_BEFORE" -i "$INPUT" -ss 5 -t "$DURATION" -c:v h264_qsv -global_quality 20 -preset fast -vf 'hwdownload,format=nv12' -c:a libfdk_aac -b:a 192k "${SUB_OPT[@]}" "$PART"
else
ffmpeg -i "$INPUT" -ss "$STARTSEC" -t "$DURATION" -c:v libx264 -crf 26 -preset veryfast -c:a libfdk_aac -b:a 192k "${SUB_OPT[@]}" "$PART"
# ffmpeg -hwaccel qsv -hwaccel_output_format qsv -i "$INPUT" -ss "$STARTSEC" -t "$DURATION" -c:v h264_qsv -global_quality 20 -preset fast -vf 'hwdownload,format=nv12' -c:a libfdk_aac -b:a 192k "${SUB_OPT[@]}" "$PART"
fi
echo "file '$PART'" >> "$PARTS_LIST"
INDEX=$((INDEX+1))
line=${line#*"Trim("}
done
done < "$TRIMFILE"
# concat で結合
ffmpeg -hide_banner -loglevel error -y \
-f concat -safe 0 -i "$PARTS_LIST" \
-c copy -map 0 \
"$OUTPUT"
rm -rf "$TEMPDIR"
echo "Done: $OUTPUT"
}
jls() {
local filename="$1"
local output_file="$2"
# 引数チェック
if [[ -z "$filename" || -z "$output_file" ]]; then
echo "Usage: jls FILENAME OUTPUT_FILE" >&2
return 1
fi
# 一時ファイルの作成
local avs_file="join.avs"
# avsファイルの作成
cat > "$avs_file" << EOF
TSFilePath="$filename"
LWLibavVideoSource(TSFilePath, repeat=true, dominance=1)
AudioDub(last,LWLibavAudioSource(TSFilePath, av_sync=true))
EOF
# 各処理の実行
timeout -k 60 1800 chapter_exe -v "$avs_file" -o chap_out.txt || {
echo "chapter_exe failed" >&2
return 1
}
if [ ! -f "$GRSTRING.lgd" ]; then
echo "Error: $GRSTRING.lgd is not found" >&2
return 1
fi
logoframe "$avs_file" -logo "$GRSTRING.lgd" -oa lf_out.txt || {
echo "logoframe failed" >&2
return 1
}
join_logo_scp -inlogo lf_out.txt -inscp chap_out.txt -incmd JL_標準.txt -o "$output_file" || {
echo "join_logo_scp failed" >&2
return 1
}
# 一時ファイルのクリーンアップ(必要に応じてコメントアウト)
# rm -f "$avs_file" chap_out.txt lf_out.txt
}
$WORKDIR/toprocess.py | while IFS= read -r FILE;
do
cd $SOURCEDIR
if [ -f "$FILE" ]; then
start_time=$(date +%s)
FILENAME=${FILE%.*}
GRSTRING=`echo $FILENAME | sed -n 's/.*\(GR[0-9][0-9]\).*/\1/p'`
# $WORKDIR/chkdrp.sh "$FILE" #エラーチェック
cd $WORKDIR
NAME=$(echo "$FILENAME" | sed 's/__.*//')
AUDIOCOMPONENTTYPE=`./act.sh $NAME`
./enc.js $SOURCEDIR/"$FILE" $AUDIOCOMPONENTTYPE
if [ -f "$FILENAME".mp4 ] && [ "$CMCUT" != "false" ] && [ "$GRSTRING" != "$NHK1" ] && [ "$GRSTRING" != "$NHK2" ]; then
jls "$FILENAME".mp4 jls_out.txt
trim "$FILENAME".mp4 jls_out.txt temp$$.mp4
if [ -s temp$$.mp4 ] ; then
rm "$FILENAME".mp4
mv temp$$.mp4 "$FILENAME".mp4
else
echo "Error: trim failed" >&2
curl -H "X-Priority: 3" -d "ERROR: trim failed: $FILENAME" $NTFY_URL
./mvjf.sh "$FILENAME".mp4 $OUTDIR
rm "$FILENAME".mp4.lwi
./processed.py "$FILE"
exit 1
fi
rm "$FILENAME".mp4.lwi
fi
if [ -s "$FILENAME".mp4 ] ; then
./mvjf.sh "$FILENAME".mp4 $OUTDIR
curl -H "X-Priority: 2" -d "mp4 created: $FILENAME" $NTFY_URL
else
echo "ERROR: mp4 not created" >&2
curl -H "X-Priority: 3" -d "ERROR: mp4 not created: $FILENAME" $NTFY_URL
exit 1
fi
./processed.py "$FILE" || true
end_time=$(date +%s)
duration=$((end_time - start_time))
minutes=$((duration / 60))
seconds=$((duration % 60))
echo "RUN TIME; $minutes min $seconds sec"
fi
done
#Time stamp: 2025/10/26注意事項
工夫2により、SSDに負担をかけるようになったことで、まれにCRITICAL on CPU_IOWAITがでるようになったので、trim関数内のトリム処理の直前に 監視watch_iowaitをはさむことにした。MAX_WAIT=600秒 (10分)待っても詰まった状態 (IOWAIT >THRESHOLD=10%)なら exit 1 するようにした。強制終了なので、/tmp/encode.lockが残るので、これを手動で削除しない限り encode.sh はなにもせずに終了する。つまり、未処理動画が溜まっていくことになる。
SSDは容量満杯に近いと性能低下が顕著になるので、第10回の diskcheck.sh は 80%で警告を出すように変更した。
疑問1
字幕かCMカットか、どちらか一方をあきらめる方がよい? (PCの健康を最優先するなら)
字幕不要なら、第11回のenc01.shにロールバック。
CMカット不要なら CMCUT=falseとする。2段目の変換工程をスキップするので高速になるし、1段目にも今回のハードウェアデコードを利用できるようになるのでさらに負荷軽減できる(1段目で処理負荷の高い yadifを用いているのは2段目の精度を考えてのこと)。
疑問2
ハードウェアトランスコード(QSV)を使うことで、USBチューナー(PX-S1UD) を不安定にしていないか?
機能的には、かなり満足な状態に落ち着いてきたので、あとは安定稼働のコツを地道に探っていく。
追記:字幕がずれるので、ソフトウェアエンコーダー (libx264) を使うことにした。



コメント