Sortie normale de ffmpeg
Vous, comme moi, avez entendu parler de ffmpeg, mais vous aviez peur de l'utiliser. Respectez les gars comme ça, tout le programme est écrit en C (si, no # et ++).
MalgrĂ© les fonctionnalitĂ©s extrĂȘmement Ă©levĂ©es du programme, des arguments terribles, verbeux et gĂȘnants, des dĂ©fauts Ă©tranges, un manque de syntaxe de saisie semi-automatique et impitoyable, associĂ©s Ă des erreurs qui ne sont pas toujours dĂ©taillĂ©es et comprĂ©hensibles pour l'utilisateur, rendent cet excellent programme peu pratique.
Je n'ai pas trouvĂ© d'applets de commande prĂȘtes Ă l'emploi sur Internet pour interagir avec ffmpeg, alors finalisons ce qui doit ĂȘtre amĂ©liorĂ© et faisons tout cela pour qu'il ne soit pas dommage de le publier sur PowershellGallery.
Créer un objet pour un tuyau
class VideoFile {
$InputFileLiteralPath
$OutFileLiteralPath
$Arguments
}
Tout commence par un objet. Le programme FFmpeg est assez simple, tout ce que nous devons savoir est oĂč nous travaillons, comment nous travaillons avec et oĂč nous mettons tout.
Commencer, traiter, terminer
Dans le bloc Begin, vous ne pouvez en aucun cas travailler avec les arguments reçus, c'est-à -dire que vous ne pouvez pas concaténer immédiatement une chaßne par des arguments, dans le bloc Begin tous les paramÚtres sont des zéros.
Cependant, ici, vous pouvez charger des exécutables, importer les modules nécessaires et initialiser les compteurs pour tous les fichiers qui seront traités, travailler avec des constantes et des variables systÚme.
ConsidĂ©rez la construction Begin-Process comme un foreach, oĂč begin est exĂ©cutĂ© avant l'appel de la fonction et les paramĂštres dĂ©finis, et End est exĂ©cutĂ© en dernier aprĂšs foreach.
Voici à quoi ressemblerait le code s'il n'y avait pas de constructions Begin, Process, End. Ceci est un exemple de mauvais code, vous ne devriez pas écrire ça.
# 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
Que faut-il mettre dans le bloc Begin?
Compteurs, composez les chemins vers les fichiers exécutables et faites un message d'accueil. Voici à quoi ressemble le bloc Begin pour moi:
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
}
Je veux attirer votre attention sur la ligne, c'est un vrai hack:
$PathToModule = Split-Path (Get-Module -ListAvailable ConvertTo-MP4).Path
En utilisant Get-Module, nous obtenons le chemin du dossier avec le module, et Split-Path prend la valeur d'entrĂ©e et renvoie le dossier un niveau en dessous. Ainsi, vous pouvez stocker des fichiers exĂ©cutables Ă cĂŽtĂ© du dossier modules, mais pas dans ce dossier lui-mĂȘme.
Comme ça:
PSffmpeg/
âââ ConvertTo-MP4/
â âââ ConvertTo-MP4.psm1
â âââ ConvertTo-MP4.psd1
â âââ Readme.md
âââ ffmpeg/
âââ ffmpeg.exe
âââ ffplay.exe
âââ ffprobe.exe
Et avec l'aide de Split-Path, vous pouvez styliser jusqu'au niveau inférieur.
Set-Location ( Get-Location | Split-Path )
Comment créer un bloc Param correct?
ImmĂ©diatement aprĂšs Begin, il y a Process avec le bloc Param. Le bloc Param lui-mĂȘme contient des vĂ©rifications nulles et valide les arguments. Par exemple:
Validation de liste:
[ValidateSet("libx264", "libx265")]
$Encoder
Tout est simple ici. Si la valeur ne ressemble pas à celle de la liste, False est renvoyé et une exception est levée.
Validation de la plage:
[ValidateRange(0, 51)]
[UInt16]$Quality = 21
Vous pouvez valider sur une plage en spécifiant des nombres de et à . Le crf de Ffmpeg prend en charge les nombres de 0 à 51, donc cette plage est spécifiée ici.
Validation par script:
[ValidateScript( { $_ -match "(?:(?:([01]?\d|2[0-3]):)?([0-5]?\d):)?([0-5]?\d)" })]
[timespan]$TrimStart
Les entrĂ©es complexes peuvent ĂȘtre validĂ©es avec des scripts normaux ou entiers. L'essentiel est que le script de validation renvoie vrai ou faux.
SoutientShouldProcess et force
Vous devez donc rĂ©-encoder les fichiers avec un codec diffĂ©rent, mais avec le mĂȘme nom. L'interface classique ffmpeg invite les utilisateurs Ă appuyer sur y / N pour Ă©craser le fichier. Et donc pour chaque fichier.
La meilleure option est la norme Oui Ă tous, Oui, Non, Non Ă tous.
J'ai choisi «Oui Ă tous» et vous pouvez réécrire les fichiers par lots et ffmpeg ne s'arrĂȘtera pas et vous demandera Ă nouveau si vous souhaitez remplacer ce fichier ou non.
function ConvertTo-WEBM {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'high')]
param (
#
[switch]$Force
)
Voici à quoi ressemble le bloc Param nu d'une personne en bonne santé. Avec SupportsShouldProcess, la fonction pourra demander avant d'effectuer une action destructive, et le commutateur de force l'ignore complÚtement.
Dans notre cas, nous travaillons avec un fichier vidéo et avant d'écraser le fichier, nous voulons nous assurer que l'utilisateur comprend ce que fait la fonction.
# Si le paramÚtre Force est spécifié, tous les fichiers sont écrasés en silence
si ($ 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
}
Faire une pipe normale
Dans un style fonctionnel, un tuyau normal ressemblerait Ă ceci:
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
}
Mais c'est tout simplement horrible, tout ressemble Ă des nouilles, ne pouvez-vous pas vraiment tout rendre plus propre?
Bien sûr, vous pouvez, mais vous devez utiliser des fonctions imbriquées pour cela. Ils peuvent consulter la déclaration des variables dans la fonction parent, ce qui est trÚs pratique. Voici un exemple:
function Invoke-FunctionName {
$ParentVar = "Hello"
function Invoke-NetstedFunctionName {
Write-Host $ParentVar
}
Invoke-NetstedFunctionName
}
Mais en mĂȘme temps, si vous avez beaucoup des mĂȘmes fonctions, vous devrez copier et coller le mĂȘme code dans chaque fonction Ă chaque fois. Dans le cas de ConvertTo-Mp4, ConvertTo-Webp, etc. plus facile Ă faire comme moi.
Si j'utilisais des fonctions imbriquées, cela ressemblerait à ceci:
$VideoFile = $InputObject
| Join-InputFileLiterallPath
| Join-Preset
| Join-ConstantRateFactor
| Join-VideoScale
| Join-Loglevel
| Join-Trim
| Join-Codec
| Join-OutFileLiterallPath
Mais encore une fois, cela réduit considérablement l'interchangeabilité du code.
Faire des fonctions normales
Nous devons composer des arguments pour ffmpeg.exe, et pour cela il n'y a rien de mieux qu'un pipeline. Comme j'aime les pipelines!
Au lieu d'une interpolation ou d'un gĂ©nĂ©rateur de chaĂźnes, nous utilisons un tube qui peut corriger des arguments ou Ă©crire une erreur pertinente. Vous avez vu le tuyau lui-mĂȘme au-dessus.
Maintenant à quoi ressemblent les fonctions les plus intéressantes du pipeline :
1. Mesure-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 enregistre le débit à partir de 1080 et plus, à une résolution vidéo inférieure, ce n'est pas si important, par conséquent, pour l'encodage de grandes vidéos, vous devez spécifier h265 par défaut.
Le retour dans Foreach-Object semble trÚs étrange. Mais vous ne pouvez rien y faire. FFmpeg écrit tout sur stdout et c'est le moyen le plus simple d'extraire une valeur de ces programmes. Utilisez cette astuce si vous devez extraire quelque chose de stdout. N'utilisez pas Start-Process, pour extraire stdout, vous devez appeler directement le fichier exécutable comme dans cet exemple.
Il est impossible d'appeler l'exĂ©cutable le long du chemin complet et d'obtenir stdout d'une autre maniĂšre. Vous devez spĂ©cifiquement aller dans le dossier avec le fichier exĂ©cutable et l'appeler par son nom Ă partir de lĂ . Pour cela, dans le bloc Begin, le script se souvient du chemin Ă partir duquel il a commencĂ©, de sorte qu'aprĂšs avoir terminĂ© son travail, il ne gĂȘne pas l'utilisateur.
begin {
$Location = Get-Location
}
Cette fonction aurait l'air bien en tant qu'applet de commande distincte, ce serait utile, mais pour le futur.
2. Join-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
}
}
}
L'un de mes gags préférés est l'interrupteur à l'envers. Il n'y a pas de modÚle de correspondance dans Powershell, mais de telles constructions le remplacent, pour la plupart.
La fonction à exécuter est entre parenthÚses. Et si le résultat de cette fonction est égal à la condition du commutateur, alors le bloc de script y est exécuté.
3. Join-Trim
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
}
}
}
La plus grande fonctionnalité du pipeline. Une fonction correctement écrite devrait montrer à l'utilisateur les erreurs, vous devez gonfler le code comme ça.
Par souci de simplicité, il a été décidé de ne pas encapsuler les chemins vers les fichiers exécutables dans la classe, c'est pourquoi les fonctions prennent autant d'arguments.
Affichage de nouveaux objets
Pour que ce script puisse ĂȘtre intĂ©grĂ© dans d'autres pipelines, vous devez faire en sorte qu'il renvoie quelque chose. Nous avons un InputObject extrait de Get-ChildItem, mais le champ Name est en lecture seule, vous ne pouvez pas simplement changer les noms de fichiers.
Pour que la sortie de la commande ressemble à la sortie systÚme, vous devez enregistrer les noms des objets recodés et utiliser Get-Chilitem pour les ajouter au tableau et l'afficher.
1. Dans le bloc Begin, déclarez un tableau
begin {
$OutputArray = @()
}
2. Dans le bloc Processus, entrez les fichiers recodés:
N'oubliez pas les vĂ©rifications nulles, mĂȘme en programmation fonctionnelle, elles sont nĂ©cessaires.
process {
if (Test-Path $Arguments.OutFileLiteralPath) {
$OutputArray += Get-Item -Path $Arguments.OutFileLiteralPath
}
}
3. Dans le bloc End, renvoyez le tableau résultant
end {
return $OutputArray
}
Hourra, fini le bloc de fin, il est temps d'utiliser le script correctement.
Nous utilisons le script
Exemple # 1
Cette commande sélectionne tous les fichiers dans un dossier, les convertit au format mp4 et envoie immédiatement ces fichiers sur un lecteur réseau.
Get-ChildItem | ConvertTo-MP4 -Width 320 -Preset Veryslow | Copy-Item âDestination '\\local.smb.server\videofiles'
Exemple # 2
Recodons toutes nos vidéos de jeu dans le dossier spécifié et supprimons les sources.
ConvertTo-MP4 -Path "C:\Users\Administrator\Videos\Escape From Tarkov\" | Remove-Item -Exclude $_
Exemple # 3
Encodage de tous les fichiers d'un dossier et déplacement de nouveaux fichiers vers un autre dossier.
Get-ChildItem | ConvertTo-WEBM | Move-Item -Destination D:\OtherFolder
Conclusion
Nous avons donc corrigĂ© ffmpeg, il semble que nous n'ayons rien manquĂ© de critique. Mais qu'est-ce que c'est, ffmpeg ne pourrait pas ĂȘtre utilisĂ© sans un shell normal?
Il s'avĂšre que oui.
Mais il reste encore beaucoup de travail à faire. Il serait utile d'avoir des applets de commande telles que Measure-videoLenght comme modules, qui renvoient la durée d'une vidéo sous la forme d'un Timespan, avec leur aide, il serait possible de simplifier le tube et de rendre le code plus compact.
Pourtant, vous devez créer des commandes ConvertTo-Webp et tout dans cet esprit. Il serait également nécessaire de créer un dossier pour l'utilisateur, s'il n'existe pas, de maniÚre récursive. Et vérifier l'accÚs en lecture et en écriture serait bien aussi.
En attendant, suivez le projet sur github .