PowershellでFFMPEGに使用できるシェルを作成する



通常のffmpeg出力



あなたは私のようにffmpegについて聞いたことがありますが、それを使うことを恐れていました。そのような人を尊重してください、プログラム全体はCで書かれています(si、#と++なし)。



プログラムの非常に高い機能にもかかわらず、ひどい、巨大な冗長、不便な引数、奇妙なデフォルト、オートコンプリートの欠如、容赦のない構文、そしてユーザーが常に詳細で理解できるとは限らないエラーが相まって、この優れたプログラムは不便です。



ffmpegと対話するための既製のコマンドレットがインターネット上に見つからなかったので、PowershellGalleryで公開するのが恥ずかしくないように、何を改善する必要があるかを確定し、すべてを実行しましょう。



パイプのオブジェクトを作成する



class VideoFile {
    $InputFileLiteralPath
    $OutFileLiteralPath
    $Arguments
}

      
      





それはすべてオブジェクトから始まります。FFmpegプログラムは非常にシンプルです。私たちが知る必要があるのは、どこで作業するか、どのように作業するか、そしてすべてをどこに置くかです。



開始、処理、終了



Beginブロックでは、受け取った引数を操作することはできません。つまり、文字列を引数ですぐに連結することはできません。Beginブロックでは、すべてのパラメーターがゼロです。



ただし、ここでは、実行可能ファイルをロードし、必要なモジュールをインポートし、処理されるすべてのファイルのカウンターを初期化し、定数とシステム変数を操作できます。



Begin-Processコンストラクトをforeachと考えてください。ここでは、関数が呼び出されてパラメーターが設定される前にbeginが実行され、foreachの最後にEndが実行されます。



これは、Begin、Process、Endの構造がない場合のコードの外観です。これは悪いコードの例です、あなたはそれを書くべきではありません。



#  begin
$InputColection = Get-ChildItem -Path C:\file.txt
 
function Invoke-FunctionName {
    param (
        $i
    )
    #  process
    $InputColection | ForEach-Object {
        $buffer = $_ | ConvertTo-Json 
    }
    
    #  end
    return $buffer
}
 
Invoke-FunctionName -i $InputColection
      
      





Beginブロックには何を入れる必要がありますか?



カウンター、実行可能ファイルへのパスを作成し、挨拶をします。これは私にとってBeginブロックがどのように見えるかです:



 begin {
        $PathToModule = Split-Path (Get-Module -ListAvailable ConvertTo-MP4).Path
        $FfmpegPath = Join-Path (Split-Path $PathToModule) "ffmpeg"
        $Exec = (Join-Path -Path $FfmpegPath -ChildPath "ffmpeg.exe")
        $OutputArray = @()
 
        $yesToAll = $false
        $noToAll = $false
 
        $Location = Get-Location
    }
      
      





私はあなたの注意を線に引き付けたいと思います、これは現実のハックです:



$PathToModule = Split-Path (Get-Module -ListAvailable ConvertTo-MP4).Path
      
      





Get-Moduleを使用して、モジュールを含むフォルダーへのパスを取得し、Split-Pathは入力値を取得して、1レベル下のフォルダーを返します。したがって、モジュールフォルダの隣に実行可能ファイルを保存できますが、このフォルダ自体には保存できません。



このような:



PSffmpeg/
├── ConvertTo-MP4/
│   ├── ConvertTo-MP4.psm1
│   ├── ConvertTo-MP4.psd1
│   ├── Readme.md
└── ffmpeg/
    ├── ffmpeg.exe
    ├── ffplay.exe
    └── ffprobe.exe

      
      





また、スプリットパスを使用すると、下のレベルまでスタイルを設定できます。



Set-Location ( Get-Location | Split-Path )
      
      





正しいParamブロックを作成する方法は?



Beginの直後に、ParamブロックとともにProcessがあります。Paramブロック自体がヌルチェックを保持し、引数を検証します。例:



リストの検証:



[ValidateSet("libx264", "libx265")]
$Encoder
      
      





ここではすべてが簡単です。値がリスト内の値のように見えない場合は、Falseが返され、例外がスローされます。



範囲の検証:



[ValidateRange(0, 51)]
[UInt16]$Quality = 21
      
      





fromとtoの数値を指定することにより、範囲で検証できます。Ffmpegのcrfは0から51までの数値をサポートしているため、この範囲をここで指定します。



スクリプトによる検証:



[ValidateScript( { $_ -match "(?:(?:([01]?\d|2[0-3]):)?([0-5]?\d):)?([0-5]?\d)" })]
[timespan]$TrimStart
      
      





複雑な入力は、通常のスクリプトまたはスクリプト全体で検証できます。主なことは、検証スクリプトがtrueまたはfalseを返すことです。



SupportsShouldProcessとforce



そのため、ファイルを別のコーデックで同じ名前で再エンコードする必要があります。従来のffmpegインターフェースは、ファイルを上書きするためにy / Nを押すようにユーザーに促します。そして、各ファイルについてもそうです。



最良のオプションは、標準の「すべてにはい」、「はい」、「いいえ」、「すべてにいいえ」です。



私は「すべてはい」を選択しました。ファイルをバッチで書き換えることができ、ffmpegは停止せず、このファイルを置き換えるかどうかを再度尋ねます。



function ConvertTo-WEBM {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'high')]
    param (
	 #      
  	[switch]$Force 
    )
      
      





これは健康な人の裸のParamブロックがどのように見えるかです。SupportsShouldProcessを使用すると、関数は破壊的なアクションを実行する前に要求できるようになり、強制スイッチはそれを完全に無視します。



この例では、ビデオファイルを操作しており、ファイルを上書きする前に、関数が何をしているのかをユーザーが理解していることを確認する必要があります。 #Force



パラメータが指定されている場合、すべてのファイルはサイレントに上書きされます

if($ Force){

$ continue = $ true

$ yesToAll = $ true

}



$Verb = "Overwrite file: " + $Arguments.OutFileLiteralPath #  ,       ShouldContinue
    
# ,     .
if (Test-Path $Arguments.OutFileLiteralPath) {
    #     , ,        
    $continue = $PSCmdlet.ShouldContinue($OutFileLiteralPath, $Verb, [ref]$yesToAll, [ref]$noToAll)
        
    #    - ,  ,     ,    
    if ($continue) {
        Start-Process $Exec -ArgumentList $Arguments.Arguments -NoNewWindow -Wait
                
    }
    #    -    
    else {
        break
    }
}
#    ,  
else {
    Start-Process $Exec -ArgumentList $Arguments.Arguments -NoNewWindow -Wait
    
}
      
      







通常のパイプを作る



機能的なスタイルでは、通常のパイプは次のようになります。



function New-FfmpegArgs {
            $VideoFile = $InputObject
            | Join-InputFileLiterallPath 
            | Join-Preset -Preset $Preset
            | Join-ConstantRateFactor -ConstantRateFactor $Quality
            | Join-VideoScale -Height $Height -Width $Width
            | Join-Loglevel -VerboseEnabled $PSCmdlet.MyInvocation.BoundParameters["Verbose"]
            | Join-Trim -TrimStart $TrimStart -TrimEnd $TrimEnd -FfmpegPath "C:\Users\nneeo\Documents\lib.Scripts\PSffmpeg\ConvertTo-WEBM\ffmpeg\" -SourceVideoPath ([IO.Path]::GetFullPath($InputObject))
            | Join-Codec -Encoder $Encoder -FfmpegPath "C:\Users\nneeo\Documents\lib.Scripts\PSffmpeg\ConvertTo-WEBM\ffmpeg\" -SourceVideoPath ([IO.Path]::GetFullPath($InputObject))
            | Join-OutFileLiterallPath -OutFileLiteralPath $OutFileLiteralPath -SourceVideoPath ([IO.Path]::GetFullPath($InputObject))
 
            return $VideoFile
        }

      
      





しかし、これはひどいです、すべてが麺のように見えます、あなたは本当にすべてをきれいにすることができませんか?

もちろん可能ですが、これにはネストされた関数を使用する必要があります。親関数の変数宣言を見ることができるので、とても便利です。次に例を示します。



function Invoke-FunctionName  {
    $ParentVar = "Hello"
    function Invoke-NetstedFunctionName {
        Write-Host $ParentVar
    }
    Invoke-NetstedFunctionName
}

      
      





ただし、同時に、同じ関数が多数ある場合は、毎回同じコードをコピーして各関数に貼り付ける必要があります。ConvertTo-Mp4、ConvertTo-Webpなどの場合。私がしたように行うのは簡単です。



ネストされた関数を使用した場合、次のようになります。



$VideoFile = $InputObject
| Join-InputFileLiterallPath 
| Join-Preset 
| Join-ConstantRateFactor 
| Join-VideoScale 
| Join-Loglevel 
| Join-Trim 
| Join-Codec 
| Join-OutFileLiterallPath 
      
      





しかし、繰り返しになりますが、これによりコードの互換性が大幅に低下します。



通常の機能を作る



ffmpeg.exeの引数を作成する必要があります。これには、パイプラインに勝るものはありません。パイプラインが大好きです!



補間や文字列ビルダーの代わりに、引数を修正したり、関連するエラーを書き込んだりできるパイプを使用します。あなたはパイプ自体を上に見ました。



次に、パイプラインの最もクールな機能がどのように見えるかについて説明し



ます。1。測定-VideoResolution



function Measure-VideoResolution {
    param (
        $SourceVideoPath,
        $FfmpegPath
    )
    Set-Location $FfmpegPath 
 
    .\ffprobe.exe -v error -select_streams v:0 -show_entries stream=height -of csv=s=x:p=0 $SourceVideoPath | ForEach-Object {
        return $_
    }
}
      
      





h265は、1080以上からビットレートを節約します。低いビデオ解像度ではそれほど重要ではないため、大きなビデオをエンコードする場合は、デフォルトとしてh265を指定する必要があります。

Foreachに戻る-オブジェクトは非常に奇妙に見えます。しかし、それについてあなたができることは何もありません。 FFmpegはすべてをstdoutに書き込みます。これは、そのようなプログラムから値を抽出する最も簡単な方法です。 stdoutから何かをプルする必要がある場合は、このトリックを使用してください。この例のように、stdoutをプルするには、実行可能ファイルを直接呼び出す必要があるため、Start-Processを使用しないでください。



フルパスに沿って実行可能ファイルを呼び出し、他の方法でstdoutを取得することはできません。具体的には、実行可能ファイルのあるフォルダーに移動し、そこから名前で呼び出す必要があります。このため、Beginブロックでは、スクリプトは開始したパスを記憶しているため、作業の終了後にユーザーを煩わせることはありません。



  begin {
        $Location = Get-Location
    }
      
      





この関数は、個別のコマンドレットとして見栄えがよく、便利ですが、将来的には役立ちます。



2.参加-VideoScale



function Join-VideoScale {
    param(
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [psobject]$InputObject,
        $Height,
        $Width
    )
 
    switch ($true) {
        ($null -eq $Height -and $null -eq $Width) {
            return $InputObject
        }
        ($null -ne $Height -and $null -ne $Width) {
            $InputObject.Arguments += " -vf scale=" + $Width + ":" + $Height
            return $InputObject
        }
        ($null -ne $Height) { 
            $InputObject.Arguments += " -vf scale=" + $Height + ":-2" 
            return $InputObject 
        }
        ($null -ne $Width) { 
            $InputObject.Arguments += " -vf scale=" + "-2:" + $Width 
            return $InputObject 
        }
    }
}

      
      



私のお気に入りのギャグの1つは、裏返しのスイッチです。Powershellには一致するパターンはありませんが、ほとんどの場合、そのような構造がそれに取って代わります。

実行する関数は括弧内にあります。そして、この関数の結果がスイッチの条件と等しい場合、スクリプトブロックがスイッチで実行されます。



3.参加-トリム



function Join-Trim {
    param(
        [Parameter(Mandatory = $true,
            ValueFromPipeline = $true,
            ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [SupportsWildcards()]
        [psobject]$InputObject,
        $TrimStart,
        $TrimEnd,
        $FfmpegPath,
        $SourceVideoPath
    )
    if ($null -ne $TrimStart) {
        $TrimStart = [timespan]::Parse($TrimStart)
    }
    if ($null -ne $TrimEnd) {
        $TrimEnd = [timespan]::Parse($TrimEnd)
    }
    
    if ($TrimStart -gt $TrimEnd -and $null -ne $TrimEnd) {
        Write-Error "TrimStart can not be equal to TrimEnd" -Category InvalidArgument
        break
    }
    if ($TrimStart -ge $TrimEnd -and $null -ne $TrimEnd) {
        Write-Error "TrimStart can not be greater than TrimEnd" -Category InvalidArgument
        break
    }
    $ActualVideoLenght = Measure-VideoLenght -SourceVideoPath $SourceVideoPath -FfmpegPath $FfmpegPath
   
    if ($TrimStart -gt $ActualVideoLenght) {
        Write-Error "TrimStart can not be greater than video lenght" -Category InvalidArgument
        break
    }
 
    if ($TrimEnd -gt $ActualVideoLenght) {
        Write-Error "TrimEnd can not be greater than video lenght" -Category InvalidArgument
        break
    }
 
    switch ($true) {
        ($null -eq $TrimStart -and $null -eq $TrimEnd) {
            return $InputObject
        }
        ($null -ne $TrimStart -and $null -ne $TrimEnd) {
            
            $ss = " -ss " + ("{0:hh\:mm\:ss}" -f $TrimStart)
            $to = " -to " + ("{0:hh\:mm\:ss}" -f $TrimEnd)
            $InputObject.Arguments += $ss + $to
            return $InputObject 
        }
        ($null -ne $TrimStart) { 
            $ss = " -ss " + ("{0:hh\:mm\:ss}" -f $TrimStart)
            $InputObject.Arguments += $ss
            return $InputObject
        }
        ($null -ne $TrimEnd) { 
            $to = " -to " + ("{0:hh\:mm\:ss}" -f $TrimEnd)
            $InputObject.Arguments += $to
            return $InputObject
        }
    }
}
      
      





パイプラインの最大の機能。正しく記述された関数は、エラーについてユーザーに表示する必要があります。このようにコードを肥大化させる必要があります。

簡単にするために、クラス内の実行可能ファイルへのパスをカプセル化しないことが決定されました。そのため、関数は非常に多くの引数を取ります。



新しいオブジェクトの表示



このスクリプトを他のパイプラインに埋め込むことができるようにするには、何かを返すようにスクリプトを作成する必要があります。Get-ChildItemから取得したInputObjectがありますが、Nameフィールドは読み取り専用であり、ファイル名を変更するだけでは不十分です。



コマンドの出力をシステム出力のようにするには、再コード化されたオブジェクトの名前を保存し、Get-Chilitemを使用してそれらを配列に追加して表示する必要があります。



1. Beginブロックで、配列を宣言します



begin {
        $OutputArray = @()
}
      
      





2. Processブロックに、再コード化されたファイルを入力します。



関数型プログラミングでもnullチェックが必要であることを忘れないでください。



process {    
 
  if (Test-Path $Arguments.OutFileLiteralPath) {
      $OutputArray += Get-Item -Path $Arguments.OutFileLiteralPath
  }
}
      
      





3. Endブロックで、結果の配列を返します



end {
        return $OutputArray
    }
      
      





やったー、終了ブロックを終了しました。スクリプトを適切に使用する時が来ました。



スクリプトを使用します



例1



このコマンドは、フォルダー内のすべてのファイルを選択し、それらをmp4形式に変換して、すぐにこれらのファイルをネットワークドライブに送信します。



Get-ChildItem | ConvertTo-MP4 -Width 320 -Preset Veryslow | Copy-Item –Destination '\\local.smb.server\videofiles'
      
      





例2



指定したフォルダー内のすべてのゲームビデオを再コーディングし、ソースを削除しましょう。



ConvertTo-MP4 -Path  "C:\Users\Administrator\Videos\Escape From Tarkov\" | Remove-Item -Exclude $_
      
      





例3



フォルダーからすべてのファイルをエンコードし、新しいファイルを別のフォルダーに移動します。



Get-ChildItem | ConvertTo-WEBM | Move-Item -Destination D:\OtherFolder
      
      





結論



そこでffmpegを修正しましたが、重要なものを見逃していなかったようです。しかし、それは何ですか、ffmpegは通常のシェルなしでは使用できませんでしたか?

はい、わかりました。

しかし、まだまだ多くの作業があります。Measure-videoLenghtなどのコマンドレットをモジュールとして使用すると、ビデオの長さをタイムスパンの形式で返します。これらのコマンドレットを使用すると、パイプを簡素化し、コードをよりコンパクトにすることができます。

それでも、ConvertTo-Webpコマンドとこの精神のすべてを作成する必要があります。また、ユーザー用のフォルダーが存在しない場合は、再帰的に作成する必要があります。また、読み取りおよび書き込みアクセスを確認することもできます。



それまでの間、githubでプロジェクトをフォローしてください






All Articles