テレビ録画メディアサーバー構築入門(第22回)

前回の続き(効率化)

  1. EPGStationで録画した動画 m2ts をmp4に変換して、

  2. 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 install

ffmpegを再コンパイルする。

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) を使うことにした。

いいなと思ったら応援しよう!

ピックアップされています

全自動TV録画 視聴環境構築

  • 26本

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
テレビ録画メディアサーバー構築入門(第22回) |takya
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1