ijsのガラクタ倉庫

ijs01140の作業メモ等

EDCB録画後PowerShellで自動エンコする(解説)

この記事は書きかけなので、後で大幅に追記する可能性が高いので注意
あと、この記事は解説が古いです

はじめに

この記事では、ijsがEDCBで使っているPowerShellスクリプト(PostRecEnd.ps1)についての概要を解説する。
このスクリプトでは、録画後のTSを自動エンコしてGoogleフォトに高画質モードで無限保存する。

スクリプト解説

スクリプト全体を見たい方は、以下の記事を参照してください。
ijs01140.hatenablog.com

EDCBから環境変数受け取るのに必要なやつ

# _EDCBX_DIRECT_

必要な関数

関数は先に書かないと動作しない。今回はTweetする部分だけ関数化している。
この関数は、Tweetする文をtxtに保存してTweetRubyスクリプトに渡している。

PostRecEnd.ps1側の記述
#===================Tweet用の関数=====================
#関数は使う前に書かないといけないみたいですね(白目)
function Tweet ($tw) {
    Write-Output "${tw}" | Out-File -LiteralPath 'C:\tools\script\tweet_utf8.txt' -Encoding UTF8
    ruby 'C:\tools\script\tweet_utf8.rb' 'C:\tools\script\tweet_utf8.txt'
    Write-Output "ツイート完了`:${tw}"
}
TweetRubyスクリプトの記述
#!/usr/bin/env ruby
require "twitter"
require "csv"

client = Twitter::REST::Client.new do |config|
  config.consumer_key        = '(ここにconsumer_keyを入力)'
  config.consumer_secret     = '(ここにconsumer_secretを入力)'
  config.access_token        = '(ここにaccess_tokenを入力)'
  config.access_token_secret = '(ここにaccess_token_secretを入力)'
end

talk = CSV.read( ARGV[0] )
client.update( talk[rand(talk.length - 1)][0] )

ログ出力設定

これで、エラー時に原因を特定しやすくなる

#====================ログ出力設定====================
Start-Transcript -LiteralPath "${env:FilePath}.log"

夜間電力を利用する(非推奨)

ただ単に夜間電力の時間帯になるまで待つというもの。処理が詰まる恐れがあるので非推奨。 ijsは現に使っていない。

#==============ピークシフト機能(大嘘)===============
#時間を取得し、7以上且つ22未満(昼)の場合22時まで待つ
#もちろん言わずもがな夜間料金のため
$nowhour=Get-Date -Format 'H'
if ($($nowhour -ge 7) -and $($nowhour -lt 22)) {
    #待ち時間の計算
    $waittime = 22 - $(Get-Date -Format 'HH') * 3600 - $(Get-Date -Format 'mm') * 60 - $(Get-Date -Format 'ss')
    start-sleep $waittime
}

変数・環境変数を設定

必要な変数や環境変数をここで設定する。ffmpegのパスやGoogleフォトへのアップロード用のフォルダのパス等はここで設定。

#====================環境変数設定====================
#ファイル名をタイトルバーに表示
$myHost = (Get-Host).UI.RawUI
$myHost.WindowTitle = "${env:FileName}.tsをエンコード中..."
#ffmpeg.exeがあるフォルダのパス
$FFFolderPATH='C:\tools\ffmpeg'
#一時的にmp4を吐き出すフォルダのパス
$MP4FolderPATH='D:\temp\mp4'
#backup and sync用フォルダのパス
$BASFolderPATH='D:\temp\gsync\tv'
#backup and sync用フォルダのパス
$treatedTSPATH='D:\treatedTS'
#10GB以上用フォルダのパス
$OtherFolderPATH='D:\temp\sizeover'
#Tweet用証明書の環境変数を設定
Set-Item env:SSL_CERT_FILE -Value 'C:\tools\script\cacert.pem'

エンコード時のビデオフィルタを設定

変数$VFILTERにビデオフィルタを順番に足していく

インタレース解除フィルタを追加

ffmpegでのインタレース解除フィルタでよく使われるもの(ijsの偏見)として、bwdifyadifが挙げられる。
yadifのほうが処理速度的には優れているが、品質はbwdifのほうが上である。パソコンの処理速度や求める品質を考慮して、どちらかお好きな方を。
FFmpeg Filters Documentation 10.12 bwdif
FFmpeg Filters Documentation 10.193 yadif
ijsの場合はbwdifを使っている。

#==========インタレース解除フィルタを追加==============
$VFILTER='bwdif=0:-1:1'
Write-Output 'インタレース解除を行います'
24fps化フィルタを追加

アニメや映画は、24fpsで制作されている場合が多い。しかし、テレビ放送は30fps(インタレース形式なので実質60fps)なので、フレームのダブリが生じてカクカク感が出てしまう。(鈍感なijsにはわからないけど)
そこで、アニメをエンコしている人たちなら誰しもが聞いたことのあるだろう、24fps化を行う。
・・・と言いたいところだが、アニメや映画でも24fps制作でない場合もあり、そこを自動判定するのはキツイものがある。

現にijsは即座に諦め、24fps制作であるとの情報を得られた某自称クソアニメだけを24fps化している。 ポプテピピックを検出する方法は簡単で、EDCBから出力される(TSファイル名).program.txtを読み込み、「ポプテピピック」という文字列が含まれるか判定するだけである。

#==============24fps化フィルタを追加==================
#ただのネタ(実用性の問題を解決できなかったため)
$decimatechk = $(Get-Content -LiteralPath "${env:FilePath}.program.txt" | Select-String -SimpleMatch 'ポプテピピック' -quiet)
if ($decimatechk -eq $True) {
    #$VFILTER="$VFILTER,decimate"
    $VFILTER += ',decimate'
    Write-Output 'ポプテピピックが検出されたので、24fps化します'
}
自動ロゴ消し

放送局をServiceNameから判定し、それぞれの放送局用に用意した局ロゴ画像を用いてロゴ消しする。
一応、ロゴ消した部分にフィルタ適用することも考えて、フィルタデータの入ったtxtを検知してビデオフィルタに自動で追加する機能もつけている。(が、まだ使っていない)
この部分は詳しく解説したいが、記事が見づらくなるのであえて概要に留める。詳しい解説については後日、単独で記事にまとめる予定。

#====================ロゴ消し設定====================
#ロゴ消しのため、放送局を判別する
#まだ画像用意できてない場合、エラーの原因になるので(それはそう)、用意できるまでコメントアウトする
$LogoNA=0
switch ($env:ServiceName) {
#BS
    "NHKBS1" {$BROADCASTER='nhkbs1'} #ロゴ画像の精度が怪しいので要チェック
    "NHKBSプレミアム" {$BROADCASTER='nhkbsp'}
    "BS日テレ" {$BROADCASTER='bsntv'}
    #"BS朝日1" {$BROADCASTER='bsasahi'}
    "BS-TBS" {$BROADCASTER='bstbs'}
    "BSジャパン" {$BROADCASTER='bsjapan'} #ロゴ画像の精度が怪しいので要チェック
    "BSフジ・181" {$BROADCASTER='bsfuji'} #ロゴ画像の精度、おそらく大丈夫だが要チェック
    "BS11イレブン" {$BROADCASTER='bs11'}
    #"BS12トゥエルビ" {$BROADCASTER='bs12'}
#地デジ
    "NHK総合1・福岡" {$BROADCASTER='nhk'} #ロゴ画像の精度、おそらく大丈夫、なはず
    "NHKEテレ1福岡" {$BROADCASTER='etv'}
    "FBS福岡放送1" {$BROADCASTER='fbs'}
    "KBCテレビ" {$BROADCASTER='kbc'}
    #"RKB毎日放送" {$BROADCASTER='rkb'}
    "TVQ九州放送1" {$BROADCASTER='tvq'}
    "テレビ西日本1" {$BROADCASTER='tnc'}
#その他
    default {$LogoNA = 1}
}
if ($LogoNA -eq 0) {
    #局別ロゴ消し用画像を使用し、removelogoフィルタを適用する
    $VFILTER += ",removelogo=${BROADCASTER}.png"
    Write-Output "放送局ロゴ消しに${BROADCASTER}.pngが使用されます"
    #局別フィルタ用txtファイルが存在する場合、読み込んでフィルタ適用
    if (Test-Path "C:\TV\logo\${BROADCASTER}.txt") {
        $logofilter = $(Get-Content -LiteralPath "C:\TV\logo\${BROADCASTER}.txt")
        $VFILTER += $logofilter
        Write-Output "ロゴ修正フィルタ適用:${logofilter}"
    }
} else {
    Write-Output "ロゴ消しは、いたしません"
}
デブロック・デノイズフィルタを追加

せっかく自分でエンコするのだから、品質改善ができるならしたいでしょ?ijsはしたい。ということでこのフィルタ適用で!
あと、ロゴ消し後の不自然な部分を目立たなくなるよう矯正するという目的もある。

#==========デブロック・デノイズフィルタを追加=========
$VFILTER += ',pp=ac'
Write-Output 'デブロック・デノイズフィルタが適用されます'
リサイズフィルタを追加

ffproveに録画ファイルをぶちこんで、吐き出されてきたログに1920x1080が含まれるか判定し、含まれてなかったら720pにリサイズする。
正直、ここの部分はもっとスマートに書けるはずなのだが、面倒なのでさっさと諦めてわかりやすい方法を使う。

#==============リサイズフィルタを追加================
#生TSのサイズが1920x1080か1440x1080か調べる
#.NET Freamworkのオブジェクト取得する方式はスマートに書けるけどスマートに書けなくてめんどくさいからこれまたボツね
Start-Process -FilePath "${FFFolderPATH}\ffprobe.exe" -ArgumentList "`"${env:FilePath}`"" -RedirectStandardError 'D:\temp\temp.txt' -Wait
$fhdchk = $(Get-Content -LiteralPath 'D:\temp\temp.txt' | Select-String -SimpleMatch '1920x1080' -quiet)
if ($fhdchk -ne $True) {
    $VFILTER += ',scale=1280:720:flags=lanczos+accurate_rnd'
    Write-Output '解像度が1440x1080なので、720pにリサイズされます'
} else {
    Write-Output '解像度が1920x1080なので、そのまま1080pでエンコします'
}
3次元デノイズフィルタを追加

これも上記のデブロック・デノイズフィルタと目的は同じで、品質改善が目的である。
このフィルタは処理速度向上を目的に、リサイズ後に適用する。

#==========3次元デノイズフィルタを追加=========
$VFILTER += ',hqdn3d=3.000'
Write-Output '3次元デノイズフィルタが適用されます'

ffmpegのビデオオプションを設定

ビデオフィルタは指定し終えたので、ビデオオプションを指定する。エンコード時のエンコーダやプリセットやCRF値はあえて変数化し、将来的に条件ごとに変更できるようにしている。
※ijsの使っているPCはQSVが使えないので、ソフトエンコを採用している※

#==============ffmpegのビデオオプションを設定==============
$ENCPRESET = 'medium'
$ENCFORMAT = 'libx265'
$ENCCRF = 24
$VideoOption = "-vf $VFILTER -crf $ENCCRF -c:v $ENCFORMAT -preset:v $ENCPRESET -g 300 -bf 16 -refs 16 -b_strategy 1 -pix_fmt yuv420p"

オーディオオプションを設定

ビデオオプションを設定ときたら、次はオーディオオプションを設定するのだなと大半の人は察したはずだ。そう、もちろん設定する。
ffmpegを知っている方なら、音声形式ならエンコード前も後も変わらないから-c:a copyでいいだろ!と思われるかもしれないが実際はそうはいかない。それをやってしまうと高確率で音ズレが訪れで阿鼻叫喚まっしぐらであるし、デュアルモノ音声の番組だと、片方のスピーカーからは日本語もう片方からは英語と、語学教材状態になってしまう。(また、厳密に言えば、同じAACでもMPEG-2とH.265では微妙に異なっているらしい)
それを防ぐためには、再エンコは免れない。どうせ音声の再エンコなんてすぐ終わるのだからそのくらい我慢しよう。

基本部分

エンコ後の音声形式をAACに指定する

$AudioOption = '-c:a aac' #エンコ後の音声形式をAACに設定(-c:a copyはあまりよろしくないので非採用)
デュアルモノ判定

テレビ放送で厄介なのが、デュアルモノ音声である。デュアルモノ音声とは、読んで時のごとく、言語ごとに片方ずつモノラル音声が割り当てられている2言語放送のことを言う。
この対策をしないと、先述した通り、片方のスピーカーからは日本語、もう片方からは英語が流れてくる、まさに語学教材状態になってしまう。
それを避けるために、デュアルモノ音声を判別する。
方法は、EDCBから出力される(TSファイル名).program.txtを読み込み、デュアルモノという文字列が含まれるか判定するというもの。含まれれば、それぞれの音声に128kbpsを割り当て、分離する。含まれていなければ、そのまま256kbpsにする。

$dualmonochk = $(Get-Content -LiteralPath "${env:FilePath}.program.txt" | Select-String -SimpleMatch 'デュアルモノ' -quiet)
if ($dualmonochk -eq $True) {
    Write-Output 'デュアルモノ音声が検出されました'
    $AudioOption += ' -b:a 128k -filter_complex channelsplit'
} else {
    $AudioOption += ' -b:a 256k'
}

ffmpegエンコード

いよいよ、お待ちかねのエンコードである。長かった。

注意点
  • ロゴ消し処理はカレントディレクトリにある画像しか使えないので、ロゴ消し用画像のあるフォルダをカレントディレクトリに設定する
  • 開始してすぐにエンコに失敗するとファイルが出力されなかったりファイルが0Bだったりするので、10回まではループして復旧を試みる
  • ffmpegはlogを全て標準エラー出力に出力するので、標準エラー出力をテキストに保存する
  • Start-ProcessのRedirectStandardErrはファイルパスでなぜかワイルドカード判定するので、一旦無難なファイル名で出力して後ほどリネーム・移動してやる
#====================エンコード====================
#エンコ開始時刻を取得
#ロゴ画像データがあるフォルダにカレントディレクトリを移動
Set-Location 'C:\TV\logo'
#ループ処理用
$CNT = 0
do {
    #10回までループし,それでもダメなら諦めて無限ループを回避
    $CNT = $CNT+1
    if ($CNT -gt 10) {
        #エンコに失敗したならtsとmp4を10GB以上用フォルダに移動
        Move-Item -LiteralPath "${env:FilePath}" "${OtherFolderPATH}" -force
        Move-Item -LiteralPath "${MP4FolderPATH}\${env:FileName}.mp4" "${OtherFolderPATH}" -force
        #予期せぬエラーが発生したことをツイートで警告
        Tweet "${env:FileName}.tsのエンコードを10回トライしてもダメだったよぉ。10GB以上用フォルダに退避させておいたから早く助けてあげて!! #ijsrec_err"
        Write-Output "${env:FileName}.tsのエンコードは異常終了しました。設定を見直しましょう。"
        exit 1
    }
    Write-Output "エンコード${CNT}回目"

    #録画の開始終了でビジーなので負荷を減らすために10秒待つ
    Start-Sleep -s 10

    #エンコ
    $FFOPTION="-analyzeduration 30M -probesize 100M -loglevel warning -y -fflags +discardcorrupt -i `"${env:FilePath}`" ${VideoOption} ${AudioOption} -map 0:p:${env:SID10}:0 -map 0:p:${env:SID10}:1 `"${MP4FolderPATH}\${env:FileName}.mp4`""
    Write-Output "FFOPTION`:${FFOPTION}"
    Start-Process "${FFFolderPATH}\ffmpeg.exe" -ArgumentList "${FFOPTION}" -RedirectStandardError 'D:\temp\postrecend_tmp.txt' -Wait #RedirectStandardErrorはなぜかワイルドカードパス扱いされるのでその問題を回避

    #====================mp4ファイルサイズ判別====================
    #エンコ後ファイルのサイズを環境変数"MP4SIZE"に指定
    $MP4SIZE = $(Get-ChildItem -LiteralPath "${MP4FolderPATH}\${env:FileName}.mp4").Length 
} while ($MP4SIZE -eq 0) #ファイルサイズが0バイトなら失敗とみなしループさせ復旧を試みる
エンコード時間とfps(処理速度)を取得

せっかくなら、処理速度を知りたいという気持ちからつけてみた機能。
ffmpegのlogから「encoded」の含まれる行を抜き出し、空白で区切り、目的の数値を取り出す。

#エンコード時間とfpsをffmpegのログから取り出す
#$EncodeTime = $(Get-Content -LiteralPath "${TSFolder}\${TSName}\logo.txt" | Select-String -Pattern 'encoded' -SimpleMatch | ForEach-Object { $($_ -split)[4] })
$EncodeTimeLine = $(Get-Content -LiteralPath 'D:\temp\postrecend_tmp.txt' | Select-String -Pattern 'encoded' -SimpleMatch) #encodedの含まれる行を抜き出し、変数EncodeTimeLineに格納
#エンコード時間
$EncodeTime = $(-split $EncodeTimeLine)[4] #変数EncodeTimeLineを空白文字で区切り、5つ目を取り出す
$EncodeTimeTrim = $($EncodeTime.substring(0,${EncodeTime}.length-1)) #最後から1文字(s)を除去
#fps
$EncodeFps = $(-split $EncodeTimeLine)[5] #変数EncodeTimeLineを空白文字で区切り、5つ目を取り出す
$EncodeFpsTrim = $($EncodeFps.substring(1,${EncodeFps}.length-1)) #最初から1文字(括弧)を除去
#エンコード時間とfpsを含め、エンコード完了をTweetする
Tweet "${env:FileName}.mp4エンコード完了。エンコードに要した時間は${EncodeTimeTrim}秒(${EncodeFpsTrim}fps)です。"

ファイルのアップロード

いよいよ、最終段階である。Googleフォトに高画質モードでうpする。
※ここでは、Googleフォトへのアップロード用のフォルダを用意し、Backup and Syncの設定まで済ませた前提で進める
Googleフォトは10GB制限があるので、
10GB以内の場合、エンコ後mp4をアップロード用のフォルダに移動し、関連ファイルをまとめて処理済みフォルダに移動、
10GB超過の場合、当該録画関連ファイルをまとめて処理失敗フォルダに移動する。

#10GB以下ならbackup and sync用フォルダ,大きいなら10GB以上用フォルダへ(10GB以上はうp出来ない)
#====================ファイル移動====================
if ($MP4SIZE -le 10GB) {
    Write-Output 'エンコ後10GB以内に収まったよ!やったね!'
    <#
    エンコしたファイルが10GB以下なら
    ・mp4をbackup and sync用フォルダに移動
    ・生TSやその関連ファイルを処理済みフォルダに移動
    #>
    Move-Item -LiteralPath "${MP4FolderPATH}\${env:FileName}.mp4" "${BASFolderPATH}\mp4" -force
    Move-Item -LiteralPath "${env:FilePath}" "${treatedTSPATH}" -force
    Move-Item -LiteralPath "${env:FilePath}.program.txt" "${treatedTSPATH}" -force
    Move-Item -LiteralPath "${env:FilePath}.err" "${treatedTSPATH}" -force
    Move-Item -LiteralPath 'D:\temp\postrecend_tmp.txt' "${treatedTSPATH}\${env:FileName}.ffmpeg.txt" -force
    #処理が正常終了したことをツイートで報告
    Tweet "${env:FileName}.mp4アップロード準備完了。エンコーダは${ENCFORMAT}、ファイルサイズは約$([math]::round(${MP4SIZE}/1MB, 2))MBです。"
} else {
    Write-Output '残念ながら、サイズオーバーです。エンコ設定を見直しましょう。'
    #エンコしたファイルが10GBより大きいならmp4とtsとその関連ファイルを10GB以上用フォルダに移動
    Move-Item -LiteralPath "${env:FilePath}" "${OtherFolderPATH}" -force
    Move-Item -LiteralPath "${MP4FolderPATH}\${env:FileName}.mp4" "${OtherFolderPATH}" -force
    Move-Item -LiteralPath "${env:FilePath}.program.txt" "${OtherFolderPATH}" -force
    Move-Item -LiteralPath "${env:FilePath}.err" "${OtherFolderPATH}" -force
    Move-Item -LiteralPath 'D:\temp\postrecend_tmp.txt' "${treatedTSPATH}\${env:FileName}.ffmpeg.txt" -force
    #10GBより大きいので手動でうpする必要があることをツイートで報告
    Tweet "${env:FileName}.mp4は残念ながら、サイズオーバーです。エンコ設定を見直しましょう。 #ijsrec_err"
}
#処理終了をTweetで知らせる
Tweet "${env:FileName}.mp4の処理が終了しました"
Write-Output '処理終了おつかれさん'
exit

エンコスクリプト全体

※必ず自己責任で使うこと※

ijs01140.hatenablog.com