2016/12/09 初回アップ

PowerShell から QSVEncを起動して
動画ファイルを自動変換する

最初に
PCの TVボードで録画したファイルは Mpeg-2なのでファイルサイズが ≒ 8GB/1Hとかさばる。 Mpeg4/H.264で圧縮するとFullHDでも ≒1GB/1H以下とかなり軽くなる。しかし、再エンコードするには i7-2600K のマシンでも実再生時間の半分くらいの時間がかかって、その間 CPU負荷はほぼ 100%で冷却ファンはシャカリキに回る。今年の夏はCPUファンにちょっとホコリが付いていただけでオーバーヒート警告が出っぱなし。しかも定期的にエンコードアプリを立ち上げてファイルを D&Dして・・・と面倒くさい。
いつかは録画フォルダの MPEG-2ファイルを自動で h.264に・・・・と思っていたが、常用しているエンコードアプリ XMediaRecode はコマンドラインに対応していないので自動化が困難。エンコードツールの代表格 ffmpegは当然コマンドラインから使えるが、オプションの設定などが分かりづらいし SandyBridgeではハードウェアエンコーダが上手く動作しない。・・・・と思っていたら、QSVEncという便利なツールが有ることが分かった。しかもこのツールは CPUのハードウェアエンコーダーの QuickSync(Intel QSV)を使うので CPU負荷は低くて高速。こちらも当然オプションは動画フォーマットに精通していないと結構分かりづらいんだが、-Bluray という BDAV互換フォーマットを出力できるオプションがあるということで、PCで録画 > BluRay レコーダーで再生という私の用途では敷居が数段低く出来る。
今回は 1月ほどかけて PowerShell上でこの QSVEnc を使用して変換の自動化をするスクリプトを作成したので解説する。
いつものお約束で、ここで書いたことは単に私が実験的に試した数回の結果であって、結果の保証はもちろん既存システムへの思わぬ弊害やとりわけ著作権絡みの問題がないことを保証するものではありません。情報を利用する場合は、利用者自身の適切な判断と責任で行って下さい。

1.QSVEnc を PowerShellから使う

QSVEnc に関してはコチラ
Windows PowerShell についてはここでは説明を省くが、従来の DOSプロンプトが進化した コマンドラインインターフェース(CLI)だと思えばいい。同じCLIである DOSコマンドのバッチファイル(*.bat)に対して、PowerShellでは PowerShell Script(*.ps1) というプログラム言語のスクリプトを記述したファイルで実行するが、実際の実行方法は逐次説明する。
また、 QSVEncとは<rigayaの日記兼メモ帳>で rigaya氏が配布している Mpeg-2/Mpeg-4などの動画ファイルのデコード/エンコードを行うフリーのツールだが、Intel Core-iシリーズ CPUが搭載する ハードウェアエンコーダーの Intel QuickSync Video (QSV)機能を使うことが出来る。 QSVが使える動画ツールは 有名な ffmpeg など幾つか存在するが、私の私の試した中で Core i7-2600K(SandyBridge) 上で QSV機能が問題なく使える唯一のツールだった。 ffmpeg というツールや XmediaRecodeというアプリは、QSVを指定すると私の環境では満足できる結果は得られなかった。
また 私は変換後の動画を最終的に BluRayレコーダーで再生するために、BDAVか BDMV形式に変換する必要があるんだが、QSVEncは --bluray というオプション一つで BDAV互換に変換してくれるのでビデオコーデックの他のオプション設定に悩まなくて済む。


例えば QSVEnc で input.mpg という動画ファイル(音声コーデック: AAC/ビデオコーデック:mpeg-2)を BluRay互換(BDAV)の AAC/h.264 の mp4ビデオファイル output.mp4 に変換するには、実行ファイルの QSVEncC64.exe と input.mpg を同じフォルダにおいて、 PowerShellコンソールでディレクトリを そのフォルダに移動してから
powershell -Command QSVEncC64.exe --audio-copy -i input.mpg --bluray --vbr 2800 --maxbitrate 30000 -o output.mp4
と入力すると変換が始まる。

2.QSVEnc による動画変換


実際に上記コマンドでエンコードを行うと i7-2600K(3.4GHz)/QSV HD-3000 の場合で、地デジ録画などのFull-HD動画(1920x1080)1時間のファイル変換が 10分程度で終わる。ソフトウェアエンコードの場合同じマシンだと30分程度かかるのに較べて 1/2 ~ 1/3 ですむ。但し画質はファイルサイズにもよるが、ソフトウェアエンコーダと比べると若干劣るとされている。(私はドラマなどの録画が主なのであまり気にしたことはない)

3.仕様

実際の PowerShellスクリプトは以下の動作仕様とした。

① エンコード用フォルダ(入力フォルダ)内の *.tsファイルを h.264変換して 出力用フォルダに *.m2tsファイルとして出力する。出力フォーマットは 音声AAC/ビデオ h.264 BDAV互換。

② エンコード中はスリープを抑止して、入力フォルダ中の全てのファイルの変換が終わったらスリープを許可する。また入力フォルダ内に sleep.txtというファイルが有った場合、変換中のファイルを変換後即スリープ移行する。(空ファイルでOK。実行中に変換後スリープ移行させたくなった場合に便利)

③ 入力フォルダに stop.txt というファイルが有った場合、一つのファイルの変換が終わった時点でスクリプトは終了する。

④ 変換が終わった元ファイルは入力フォルダから変換済みフォルダに移動させる。

⑤ 仮に同名ファイルがすでに出力フォルダに存在した場合は変換は行わず、入力ファイルを変換済みフォルダに移動させる。

⑥ 変換後の出力ファイルのファイルサイズが異常に小さい場合、変換失敗と判断して変換失敗フォルダに移動させる。

⑦ エンコード用入力フォルダや出力ファルダなどのフォルダや、QSVEnc のオプション、変換中のスリープ禁止などの動作に関わる設定を別ファイル(qsv.ini)として、起動時に読み込んで処理に反映する。

およそ以上の動作を実装する。
但し、QSVEncという外部エンコーダーを利用するので、一つの動画ファイル変換途中に動作を停止したり中断したりすることは出来ない。どうしても途中で停止させたければ QSVEncを強制終了させるしかないが、その場合処理中の出力ファイルは不完全な動画ファイルのままとなってしまうので、再開するにはその不完全ファイルを削除してから再実行するしかない。


4.実際のスクリプト

実際のスクリプトは以下の通り。
このスクリプトをメモ帳などのテキストエディタにコピペして例えば QSV.ps1 というファイル名を付けて保存する。(ファイル名は任意、拡張子は ps1)

# QSVEncで AVC/h.264/ACC の BDAV互換 m2tsファイルを生成する
# Intel QSV を利用して動作時間を短縮する ソフトエンコーダ(ffmpeg)の 1/4の時間で変換する
# 変換済みファイルは フォルダ移動する		2016/11/17
# 変換中はスリープを抑止する				2016/12/05
# qsv.ini に設定値を保存する			2016/12/11
# サブプロセスでダイアログを表示してボタン操作でエンコードを中断する    2016/12/13
# ダイアログ上に 処理中のファイル名を表示    2016/12/23

# ★サブルーチン
function Filelock{							# ファイルロック中のファイルを検査する
    param (
        [parameter(
            position = 0,
            mandatory
        )]
        [System.IO.FileInfo]
        $Path
    )
    try
    {
        # initialise variables
        $script:filelocked = $false
        # attempt to open file and detect file lock
        $script:fileInfo = New-Object System.IO.FileInfo $Path
        $script:fileStream = $fileInfo.Open([System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
        # close stream if not lock
        $f1 = $fileStream
        Write-Host $f1
        $f2 = $filelocked
        if ($fileStream) {
            $fileStream.Close()
        $f3 = $fileStream
        $f4 = $filelocked
        }
    }
    catch
    {
        # catch fileStream had falied
        $filelocked = $true
    }
    finally
    {
        # return result
        [PSCustomObject]@{
            path = $Path
            filelocked = $filelocked
        }
    }
}

function Test-FileLock {
    param (
        [parameter(
            position = 0,
            mandatory
        )]
        [string]
        $Path
    )

    try
    {    
        if(Test-Path -LiteralPath $Path)
        {
            if ((Get-Item -LiteralPath $path) -is [System.IO.FileInfo])
            {
                return (filelock -Path $Path).filelocked
            }
            elseif((Get-Item -LiteralPath $Path) -is [System.IO.DirectoryInfo])
            {
                Write-Verbose "[$Path] detect as $((Get-Item -LiteralPath -path $Path).GetType().FullName). Skip cehck."
            }
        }
        else
        {
            Write-Error "[$Path] could not find. 
            + CategoryInfo          : ObjectNotFound: ($Path), ItemNotFoundException
            + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetItemCommand"
        }
    }
    catch
    {
        Write-Error $_
    }
}

# ★ Dialog 表示ルーチン
# ダイアログ上のLabel.Textをファイル渡ししてタイマーで書き換える
function Disp_Dialogue ($Fullpath) {
    $job1=Start-Job -ScriptBlock { 
        param($Fullpath)
        Add-Type -AssemblyName System.Windows.Forms | Out-Null

        $Watch = New-Object System.Diagnostics.Stopwatch
        $Timer = New-Object System.Windows.Forms.Timer
        $Timer.Interval = 10
        $Time = {
            $Now = $Watch.Elapsed
            $Label.Text = Get-Content $Fullpath.Value -TotalCount 1
        }
        $Timer.Add_Tick($Time)
        $Form = New-Object system.Windows.Forms.Form
        $Form.StartPosition =  [System.Windows.Forms.FormStartPosition]::Manual
        $Form.Text = "QSV Encoding PS"
        $Form.size = "300,100"
        $screen = [Windows.Forms.Screen]::PrimaryScreen.WorkingArea
        $Fv = $screen.height - 110
        $Fh = $screen.width-1 - 300
        $Form.Location = "$Fh,$Fv"
        $Label = New-Object System.Windows.Forms.Label
        $Label.Text = $labeltxt
        $Label.Location = "10,10"
        $Label.Font = New-Object System.Drawing.Font("MS ゴシック",10)
        $Label.AutoSize = $True
        $Form.Topmost = $True
        $Form.MaximizeBox = $false
        $Form.MinimizeBox = $true

        $ButtonA = New-Object System.Windows.Forms.Button
        $ButtonA.Location = "120,35"
        $ButtonA.size = "60,20"
        $ButtonA.text = "中断"
        $ButtonA.FlatStyle = "popup"
        $ButtonB = New-Object System.Windows.Forms.Button
        $ButtonB.Location = "200,35"
        $ButtonB.size = "60,20"
        $ButtonB.text = "隠す"
        $ButtonB.FlatStyle = "popup"
        $Form.Controls.AddRange(@($Label, $ButtonA, $ButtonB))
        $ButtonA.DialogResult = [System.Windows.Forms.DialogResult]::OK
        $ButtonB.DialogResult = [System.Windows.Forms.DialogResult]::Cancel

        $ButtonA.Add_Click({$ret = [System.Windows.Forms.DialogResult]::OK})
        $ButtonB.Add_Click({$ret = [System.Windows.Forms.DialogResult]::Cancel})
        $Timer.Start()
        $Watch.Start()
 
        $Form.ShowDialog()
        $ret
        $Timer.Stop()
        $Watch.Stop()
    } -ArgumentList ($Fullpath)
return $job1
}
# ★ここまでサブルーチン

# ★ここからメインルーチン
# 初期化、設定値を qsv.ini ファイルから読み込んで 定数に設定する
[int]$sleep=0
[int]$goSleep=0
[int]$goShutdown=0
[int]$fMove=0
[int]$fDelete=0

$myPath = Split-Path -Parent $MyInvocation.MyCommand.Path
$LabelfPath = Join-Path $MyPath "label.txt"
if (Test-Path $LabelfPath) {exit}

$arr = Get-Content (Join-Path $myPath qsv.ini)
foreach ($str in $arr) {
    Switch -regex ($str) {
        "^inhibit Sleep" {if ($str.LastIndexOf("1") -ge 0) {$dontSleep = 1}}
        "^After complete" { Switch -regex ($str.Trim()) { "1$" {$goSleep = 1} "2$" {$goShutdown = 1}}}
	    "^After encoding" { Switch -regex ($str.Trim()) { "1$" {$fMove = 1} "2$" {$fDelete = 1}}}
        "^QSVEncC.exe Path" { if ($str.IndexOf(":") -gt 1) { $QSVENC = $str.Substring($str.IndexOf(":") + 1).Trim()}}
        "^Target Dir" { if ($str.IndexOf(":") -gt 1) { $TARGET_DIR = $str.Substring($str.IndexOf(":") + 1).Trim()}}
        "^Output Dir" { if ($str.IndexOf(":") -gt 1) { $OUTPUT_DIR = $str.Substring($str.IndexOf(":") + 1).Trim()}}
        "^Move Dir" { if ($str.IndexOf(":") -gt 1) { $MOVE_DIR = $str.Substring($str.IndexOf(":") + 1).Trim()}}
        "^ffMove Dir" { if ($str.IndexOf(":") -gt 1) { $MOVE_fDIR = $str.Substring($str.IndexOf(":") + 1).Trim()}}
        "^Log Path" { if ($str.IndexOf(":") -gt 1) { $LOG_PATH = $str.Substring($str.IndexOf(":") + 1).Trim()}}
        "^Audio Option" { if ($str.IndexOf(":") -gt 1) { $QSV_OPT_A = $str.Substring($str.IndexOf(":") + 1).Trim()}}
        "^Video Option" { if ($str.IndexOf(":") -gt 1) { $QSV_OPT_V = $str.Substring($str.IndexOf(":") + 1).Trim()}}
    }
 }
$nowtime = Get-Date -Format "MM-dd-HH-mm"
$LOG_PATH = "$LOG_PATH" + "$nowtime" + ".log"
 Write-Host " $QSVENC `r`n $TARGET_DIR `r`n $OUTPUT_DIR `r`n $MOVE_DIR `r`n $MOVE_fDIR `r`n $LOG_PATH `r`n $QSV_OPT_A `r`n $QSV_OPT_V"

[int]$n = 0
[string]$fn = ""
[string]$ofn = ""
[string]$nowtime = ""
[string]$info = ""
$arr = @()
Write-Host "QSVEnc  encode .ts file --> .m2ts file & move .ts file."

"Serch ts file" | Out-File $LabelfPath
Get-Job|Remove-Job
$jobs = Disp_Dialogue([ref]$LabelfPath)
[String]$result =""
[String]$Text = ""
[String]$Labelt = ""

$SystemRequired = [uint32]1
$Continuous = [uint32]"0x80000000"
$AwayMode = [uint32]"0x00000040"
$signature = @"
[DllImport("kernel32.dll")]
public extern static uint SetThreadExecutionState(uint esFlags);
"@
$func = Add-Type -memberDefinition $signature -namespace "Win32Functions" -name "SetThreadExecutionStateFunction" -passThru
if ($dontSleep -eq 1) { $func::SetThreadExecutionState($SystemRequired -bor $Continuous -bor $AwayMode)}

cd $TARGET_DIR
$arr = get-childitem $TARGET_DIR
write-host "★ " $arr.length "個のアイテムがあります。"
[int]$count = 0
foreach ($fn in $arr) {
    write-host "  ★★ " $fn
	$Basefn = [System.IO.Path]::GetFileNameWithoutExtension($fn)
    $exta=[System.IO.Path]::GetExtension($fn)
    $Fullpath = (Get-Item -LiteralPath $fn).FullName

	if ($exta -eq ".ts") {
    	if (Test-FileLock -Path $Fullpath) { 
        	Write-Host "★ $fn はファイルロック中"
        	continue
    	}
		Write-Host "  ★ ts file exist" -NoNewline
		$ofn = $OUTPUT_DIR + "\" + $Basefn + ".m2ts"
		Write-Host "  ★ "  $fn.Fullname
		if (Test-Path -LiteralPath $ofn) {
			$info = $ofn + " : already exist !"
			Write-Host "  ★ " $info
			$fn="'" + "$fn" + "'"
		} else {
			$info = $ofn + " : not exist !   now encoding !"
			Write-Host "  ★ " $info
			$ofn="'" + "$ofn" + "'"
			$fn="'" + "$fn" + "'"
            		$fn + "を処理中・・・" | Out-File $LabelfPath
			$qsv_opt="$QSV_OPT_A -i $fn $QSV_OPT_V -o $ofn --log $LOG_PATH"

			powershell -Command "$QSVENC $qsv_opt"
            Remove-Item $Basefn + '.ts.program.txt'
            Remove-Item $Basefn + '.ts.err'
            $count++
		}
		$ofn = $ofn -replace "`'", ""
        	$fn = $fn -replace "`'", ""
		if(($(Get-ChildItem -LiteralPath $ofn).Length / 1MB) -gt 2) {
			if((Test-Path -LiteralPath "$MOVE_DIR\$fn") -or ($fDelete -eq 1)) {
				Remove-Item $fn
			} elseif ($fMove -eq 1) {
				Move-Item -LiteralPath $fn $MOVE_DIR
				Write-Host "  ★ " $fn "  moved"
			}
		} else {
			Write-Host "▲ sorry " $fn "  failed encode !"
            Move-Item  -LiteralPath $fn $MOVE_fDIR
            Remove-Item -LiteralPath $ofn
		}
 	    
	} else {
		Write-Host "  ★ Non ts file : $fn"
	}
    $results = Receive-Job -Job $jobs
    if ($jobs.State -eq "Completed") { $result = $results[0].value }
	if ($result -eq "OK") {	break}

}


if ($jobs.State -ne "Completed") {$jobs|Remove-Job -Force}

if ($dontSleep -eq 1) { $func::SetThreadExecutionState($Continuous)}
if (($goShutdown -eq 1) -and ($count -ne 0)) { Stop-Computer
} elseif (($goSleep -eq 1) -and ($count -ne 0)) { [System.Windows.Forms.Application]::SetSuspendState("Suspend", $false, $true)
}
Remove-Item $LabelfPath
Write-Host "Complete !   $count file(s) converted."

qsv.ini の内容は以下の通り(サンプル)

# QSVEnc options
-------------------------------------
inhibit Sleep: 1
After complete (sleep:1 or shutdown:2): 1
After encoding (FileMove:1 or Delete:2): 1
-------------------------------------
QSVEncC.exe Path: D:\Aplications\Movie\QSVEnc\QSVEncC\x64\QSVEncC64.exe
Target Dir: E:\TV_Record
Output Dir: L:\TV_Record\Encoded
Move Dir: E:\TV_Record\encoded
ffMove Dir: E:\TV_Record\Enc失敗
Log Path: L:\TV_Record\Encoded\QSV_
-------------------------------------
Audio Option: --audio-codec aac
Video Option: --avqsv-analyze 10 --bluray --vbr 2500 --maxbitrate 30000 --quality higher --trim 200:0 --max-procfps 120

各行の意味は以下の通り

-------------------------------------
inhibit Sleep: 変換中のスリープを禁止する時 : 1
After complete (sleep:1 or shutdown:2): 変換後にスリープ移行: 1 /シャットダウン移行: 2
After encoding (FileMove:1 or Delete:2): 変換完了ファイルを移動: 1 /削除: 2
-------------------------------------
QSVEncC.exe Path: QSVEncC64.exe のファイルパス/32bit の場合 x86\QSVEncC.exe を指定する
Target Dir: 元ファイルのフォルダ
Output Dir: 変換後のファイルの保存フォルダ
Move Dir: 変換後の元ファイル移動フォルダ
ffMove Dir: エンコード失敗元ファイルの移動フォルダ
Log Path: ログファイルのパス
-------------------------------------
Audio Option: QSVEncのオーディオコーデックオプション
Video Option: QSVEncのビデオコーデックオプション

5.実行

上記スクリプトを実行するには、デスクトップなど適当なフォルダに QSV.ps1 のショートカットを作成し、リンク先を以下のように書き換える。
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy RemoteSigned -File <上記スクリプトのファイルパス> また、ショートカットをスタートアップなどに登録しても可。



Access Counter:  総アクセス数

Page TOP Site TOP