Menulis shell yang dapat digunakan untuk FFMPEG di Powershell



Output ffmpeg normal



Anda, seperti saya, pernah mendengar tentang ffmpeg, tetapi takut menggunakannya. Hormati orang-orang seperti itu, seluruh program ditulis dalam C (si, no # dan ++).



Terlepas dari fungsionalitas program yang sangat tinggi, mengerikan, verbose besar, argumen yang tidak nyaman, default yang aneh, kurangnya sintaksis otomatis dan tak kenal ampun, ditambah dengan kesalahan yang tidak selalu mendetail dan dapat dipahami oleh pengguna, membuat program yang luar biasa ini menjadi tidak nyaman.



Saya tidak menemukan cmdlet yang sudah jadi di Internet untuk berinteraksi dengan ffmpeg, jadi mari selesaikan apa yang perlu ditingkatkan dan lakukan semuanya sehingga tidak memalukan untuk mempublikasikannya di PowershellGallery.



Membuat benda untuk pipa



class VideoFile {
    $InputFileLiteralPath
    $OutFileLiteralPath
    $Arguments
}

      
      





Semuanya dimulai dengan sebuah objek. Program FFmpeg cukup sederhana, yang perlu kita ketahui adalah di mana kita bekerja, bagaimana kita bekerja dengannya dan di mana kita meletakkan semuanya.



Mulai, proses, akhiri



Di blok mulai, Anda tidak bisa bekerja dengan argumen yang diterima dengan cara apa pun, yaitu, Anda tidak bisa langsung menggabungkan string dengan argumen, di blok mulai semua parameter adalah nol.



Namun, di sini Anda dapat memuat file yang dapat dieksekusi, mengimpor modul yang diperlukan dan menginisialisasi penghitung untuk semua file yang akan diproses, bekerja dengan konstanta dan variabel sistem.



Pikirkan konstruksi Begin-Process sebagai foreach, di mana begin dieksekusi sebelum fungsi dipanggil dan parameter ditetapkan, dan End dieksekusi terakhir setelah foreach.



Beginilah tampilan kode jika tidak ada konstruksi Begin, Process, End. Ini adalah contoh kode yang buruk, Anda tidak boleh menulisnya.



#  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
      
      





Apa yang harus diletakkan di blok Begin?



Penghitung, buat jalur ke file yang dapat dieksekusi dan buat salam. Beginilah tampilan blok Begin bagi saya:



 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
    }
      
      





Saya ingin menarik perhatian Anda ke garis, ini adalah peretasan kehidupan nyata:



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





Menggunakan Get-Module, kami mendapatkan jalur ke folder dengan modul, dan Split-Path mengambil nilai input dan mengembalikan folder satu tingkat di bawah. Dengan demikian, Anda dapat menyimpan file yang dapat dieksekusi di sebelah folder modul, tetapi tidak di folder itu sendiri.



Seperti ini:



PSffmpeg/
β”œβ”€β”€ ConvertTo-MP4/
β”‚   β”œβ”€β”€ ConvertTo-MP4.psm1
β”‚   β”œβ”€β”€ ConvertTo-MP4.psd1
β”‚   β”œβ”€β”€ Readme.md
└── ffmpeg/
    β”œβ”€β”€ ffmpeg.exe
    β”œβ”€β”€ ffplay.exe
    └── ffprobe.exe

      
      





Dan dengan bantuan Split-Path, Anda dapat mengatur gaya hingga ke tingkat di bawah.



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





Bagaimana cara membuat blok Param yang benar?



Segera setelah Mulai, ada Proses bersama dengan blok Param. Blok Param itu sendiri menahan pemeriksaan nol, dan memvalidasi argumen. Contoh:



Validasi Daftar:



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





Semuanya sederhana di sini. Jika nilainya tidak terlihat seperti dalam daftar, maka False dikembalikan dan kemudian pengecualian dilemparkan.



Validasi rentang:



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





Anda dapat memvalidasi pada rentang dengan menentukan angka dari dan ke. Crf ffmpeg mendukung angka dari 0 hingga 51, jadi kisaran ini ditentukan di sini.



Validasi dengan skrip:



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





Masukan kompleks dapat divalidasi dengan tetap atau seluruh skrip. Hal utama adalah bahwa skrip validasi mengembalikan true atau false.



Mendukung Proses dan kekuatan



Jadi, Anda perlu mengenkode ulang file dengan codec yang berbeda, tetapi dengan nama yang sama. Antarmuka ffmpeg klasik meminta pengguna menekan y / N untuk menimpa file. Dan untuk setiap file.



Pilihan terbaik adalah Ya standar untuk semua, Ya, Tidak, Tidak untuk semua.



Saya memilih "Ya untuk semua" dan Anda dapat menulis ulang file dalam batch dan ffmpeg tidak akan berhenti dan bertanya lagi apakah Anda ingin mengganti file ini atau tidak.



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





Beginilah tampilan blok Param telanjang dari orang yang sehat. Dengan SupportsShouldProcess, fungsi tersebut dapat bertanya sebelum melakukan tindakan destruktif, dan sakelar gaya sepenuhnya mengabaikannya.



Dalam kasus kami, kami bekerja dengan file video dan sebelum menimpa file tersebut, kami ingin memastikan bahwa pengguna memahami apa yang dilakukan fungsi tersebut.



# Jika parameter Force ditentukan, maka semua file akan ditimpa secara diam-diam

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
    
}
      
      







Membuat pipa biasa



Dalam gaya fungsional, pipa normal akan terlihat seperti ini:



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
        }

      
      





Tapi ini mengerikan, semuanya terlihat seperti mie, tidak bisakah kamu benar-benar membuat semuanya lebih bersih?

Tentu saja Anda bisa, tetapi Anda perlu menggunakan fungsi bertingkat untuk ini. Mereka dapat melihat deklarasi variabel di fungsi induk, yang sangat memudahkan. Berikut contohnya:



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

      
      





Tetapi pada saat yang sama, jika Anda memiliki banyak fungsi yang sama, Anda harus menyalin dan menempelkan kode yang sama ke setiap fungsi setiap saat. Dalam kasus ConvertTo-Mp4, ConvertTo-Webp, dll. lebih mudah dilakukan seperti yang saya lakukan.



Jika saya menggunakan fungsi bersarang, akan terlihat seperti ini:



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





Tetapi sekali lagi, ini sangat mengurangi pertukaran kode.



Membuat fungsi normal



Kita perlu membuat argumen untuk ffmpeg.exe, dan untuk ini tidak ada yang lebih baik dari pipeline. Betapa saya menyukai saluran pipa!



Alih-alih interpolasi atau pembuat string, kami menggunakan pipa yang dapat mengoreksi argumen atau menulis kesalahan yang relevan. Anda melihat pipa itu sendiri di atas.



Sekarang tentang seperti apa fungsi pipeline paling keren itu :



1. Measure-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 menyimpan bitrate mulai dari 1080 dan lebih tinggi, pada resolusi video yang lebih rendah itu tidak terlalu penting, oleh karena itu, untuk encoding video besar, Anda harus menentukan h265 sebagai default.

Return in Foreach-Object terlihat sangat aneh. Tapi tidak ada yang bisa Anda lakukan. FFmpeg menulis semuanya ke stdout dan ini adalah cara termudah untuk mengekstrak nilai dari program tersebut. Gunakan trik ini jika Anda perlu menarik sesuatu dari stdout. Jangan gunakan Start-Process, untuk menarik stdout Anda perlu memanggil file yang dapat dieksekusi secara langsung seperti pada contoh ini.



Tidak mungkin memanggil executable di sepanjang jalur lengkap dan mendapatkan stdout dengan cara lain. Anda harus secara khusus pergi ke folder dengan file yang dapat dieksekusi dan menyebutnya dengan nama dari sana. Untuk ini, di blok Begin, skrip mengingat jalur dari mana ia dimulai, sehingga setelah menyelesaikan pekerjaannya tidak mengganggu pengguna.



  begin {
        $Location = Get-Location
    }
      
      





Fungsi ini akan terlihat bagus sebagai cmdlet terpisah, ini akan berguna, tetapi untuk masa depan.



2. Bergabung-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 
        }
    }
}

      
      



Salah satu lelucon favorit saya adalah tombol luar dalam. Tidak ada pola yang cocok di Powershell, tetapi konstruksi seperti itu menggantikannya, untuk sebagian besar.

Fungsi yang akan dijalankan ada di dalam tanda kurung. Dan jika hasil dari fungsi ini sama dengan kondisi pada sakelar, maka blok skrip dieksekusi di dalamnya.



3. Gabung-Potong



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
        }
    }
}
      
      





Fitur terbesar di pipeline. Fungsi yang ditulis dengan benar harus menunjukkan kepada pengguna tentang kesalahan, Anda harus mengasapi kode seperti ini.

Untuk kesederhanaan, diputuskan untuk tidak merangkum jalur ke file yang dapat dieksekusi di kelas, itulah sebabnya fungsi mengambil begitu banyak argumen.



Menampilkan objek baru



Agar skrip ini dapat disematkan di pipeline lain, Anda harus membuatnya agar mengembalikan sesuatu. Kami memiliki InputObject yang diambil dari Get-ChildItem, tetapi bidang Nama hanya-baca, Anda tidak bisa begitu saja mengubah nama file.



Untuk membuat keluaran dari perintah terlihat seperti keluaran sistem, Anda perlu menyimpan nama dari objek yang dikodekan dan menggunakan Get-Chilitem untuk menambahkannya ke larik dan menampilkannya.



1. Di blok Begin, nyatakan sebuah array



begin {
        $OutputArray = @()
}
      
      





2. Di blok Proses, masukkan file yang dikodekan:



Jangan lupa tentang pemeriksaan nol, bahkan dalam pemrograman fungsional mereka diperlukan.



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





3. Di blok End, kembalikan larik yang dihasilkan



end {
        return $OutputArray
    }
      
      





Hore, selesaikan blok akhir, saatnya menggunakan skrip dengan benar.



Kami menggunakan skrip



Contoh # 1



Perintah ini akan memilih semua file dalam folder, mengonversinya ke format mp4 dan segera mengirim file-file ini ke drive jaringan.



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





Contoh # 2



Mari kita mengodekan ulang semua video game kita di folder tertentu, dan menghapus sumbernya.



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





Contoh # 3



Menyandikan semua file dari folder dan memindahkan file baru ke folder lain.



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





Kesimpulan



Jadi kami memperbaiki ffmpeg, sepertinya kami tidak melewatkan sesuatu yang penting. Tapi ada apa, ffmpeg tidak bisa digunakan tanpa shell biasa?

Ternyata iya.

Namun masih banyak pekerjaan yang harus diselesaikan. Akan berguna untuk memiliki cmdlet seperti Measure-videoLenght sebagai modul, yang mengembalikan durasi video dalam bentuk Jangka Waktu, dengan bantuan mereka akan mungkin untuk menyederhanakan pipa dan membuat kode lebih ringkas.

Namun, Anda perlu membuat perintah ConvertTo-Webp dan semuanya dalam semangat ini. Juga perlu membuat folder untuk pengguna, jika tidak ada, secara rekursif. Dan memeriksa akses baca dan tulis akan menyenangkan juga.



Sementara itu, ikuti proyek di github .






All Articles