Salida normal de ffmpeg
Usted, como yo, ha oído hablar de ffmpeg, pero tenía miedo de usarlo. Respeta a tipos así, todo el programa está escrito en C (si, no # y ++).
A pesar de la funcionalidad extremadamente alta del programa, los argumentos terribles, gigantescos, inconvenientes, extraños valores predeterminados, la falta de autocompletar y la sintaxis implacable, junto con errores que no siempre son detallados y comprensibles para el usuario, hacen que este excelente programa sea inconveniente.
No encontré cmdlets listos para usar en Internet para interactuar con ffmpeg, así que finalicemos lo que se necesita mejorar y hagámoslo todo para que no sea una pena publicarlo en PowershellGallery.
Hacer un objeto para una pipa
class VideoFile {
$InputFileLiteralPath
$OutFileLiteralPath
$Arguments
}
Todo comienza con un objeto. El programa FFmpeg es bastante simple, todo lo que necesitamos saber es dónde trabajamos, cómo trabajamos con él y dónde ponemos todo.
Comenzar, procesar, terminar
En el bloque Begin, no puede trabajar con los argumentos recibidos de ninguna manera, es decir, no puede concatenar inmediatamente una cadena por argumentos, en el bloque Begin todos los parámetros son ceros.
Sin embargo, aquí puede cargar ejecutables, importar los módulos necesarios e inicializar contadores para todos los archivos que serán procesados, trabajar con constantes y variables del sistema.
Piense en la construcción Begin-Process como un foreach, donde begin se ejecuta antes de que se llame a la función y se establezcan los parámetros, y End se ejecuta en último lugar después de foreach.
Así es como se vería el código si no hubiera construcciones Begin, Process, End. Este es un ejemplo de código incorrecto, no debería escribirlo.
# 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
¿Qué se debe poner en el bloque Begin?
Contadores, componga rutas a archivos ejecutables y haga un saludo. Así es como se ve el bloque Begin para mí:
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
}
Quiero llamar su atención sobre la línea, este es un truco de la vida real:
$PathToModule = Split-Path (Get-Module -ListAvailable ConvertTo-MP4).Path
Usando Get-Module, obtenemos la ruta a la carpeta con el módulo, y Split-Path toma el valor de entrada y devuelve la carpeta un nivel por debajo. Por lo tanto, puede almacenar archivos ejecutables junto a la carpeta de módulos, pero no en esta carpeta.
Me gusta esto:
PSffmpeg/
├── ConvertTo-MP4/
│ ├── ConvertTo-MP4.psm1
│ ├── ConvertTo-MP4.psd1
│ ├── Readme.md
└── ffmpeg/
├── ffmpeg.exe
├── ffplay.exe
└── ffprobe.exe
Y con la ayuda de Split-Path, puede diseñar hasta el nivel inferior.
Set-Location ( Get-Location | Split-Path )
¿Cómo hacer un bloque de Param correcto?
Inmediatamente después de Begin, hay Process junto con el bloque Param. El bloque Param en sí mismo contiene comprobaciones nulas y valida los argumentos. Por ejemplo:
Validación de lista:
[ValidateSet("libx264", "libx265")]
$Encoder
Aquí todo es sencillo. Si el valor no se parece a uno de la lista, se devuelve False y se lanza una excepción.
Validación de rango:
[ValidateRange(0, 51)]
[UInt16]$Quality = 21
Puede validar en un rango especificando números desde y hasta. El crf de Ffmpeg admite números del 0 al 51, por lo que este rango se especifica aquí.
Validación por script:
[ValidateScript( { $_ -match "(?:(?:([01]?\d|2[0-3]):)?([0-5]?\d):)?([0-5]?\d)" })]
[timespan]$TrimStart
La entrada compleja se puede validar con scripts regulares o completos. Lo principal es que el script de validación devuelve verdadero o falso.
Soportes Debe Procesar y forzar
Por lo tanto, debe volver a codificar los archivos con un códec diferente, pero con el mismo nombre. La interfaz clásica de ffmpeg solicita a los usuarios que presionen y / N para sobrescribir el archivo. Y así para cada archivo.
La mejor opción es el estándar Sí a todos, Sí, No, No a todos.
Elegí "Sí a todo" y puede reescribir archivos en lotes y ffmpeg no se detendrá y preguntará nuevamente si desea reemplazar este archivo o no.
function ConvertTo-WEBM {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'high')]
param (
#
[switch]$Force
)
Así es como se ve el bloque Param desnudo de una persona sana. Con SupportsShouldProcess, la función podrá preguntar antes de realizar una acción destructiva y el interruptor de fuerza lo ignorará por completo.
En nuestro caso, estamos trabajando con un archivo de video y antes de sobrescribir el archivo, queremos asegurarnos de que el usuario comprende lo que está haciendo la función.
# Si se especifica el parámetro Force, todos los archivos se sobrescriben silenciosamente
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
}
Hacer una pipa normal
En estilo funcional, una tubería normal se vería así:
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
}
Pero esto es horrible, todo parece fideos, ¿no puedes hacer que todo esté más limpio?
Por supuesto que puede, pero necesita usar funciones anidadas para esto. Pueden mirar la declaración de variables en la función padre, lo cual es muy conveniente. He aquí un ejemplo:
function Invoke-FunctionName {
$ParentVar = "Hello"
function Invoke-NetstedFunctionName {
Write-Host $ParentVar
}
Invoke-NetstedFunctionName
}
Pero al mismo tiempo, si tiene muchas de las mismas funciones, tendrá que copiar y pegar el mismo código en cada función cada vez. En el caso de ConvertTo-Mp4, ConvertTo-Webp, etc. más fácil de hacer como yo.
Si usara funciones anidadas, se vería así:
$VideoFile = $InputObject
| Join-InputFileLiterallPath
| Join-Preset
| Join-ConstantRateFactor
| Join-VideoScale
| Join-Loglevel
| Join-Trim
| Join-Codec
| Join-OutFileLiterallPath
Pero nuevamente, esto reduce en gran medida la intercambiabilidad de código.
Realización de funciones normales
Necesitamos componer argumentos para ffmpeg.exe, y para esto no hay nada mejor que un pipeline. ¡Cómo amo las tuberías!
En lugar de interpolación o un generador de cadenas, usamos una tubería que puede corregir argumentos o escribir un error relevante. Viste la tubería en sí arriba.
Ahora, sobre cómo se ven las funciones más interesantes de la tubería :
1. Medir-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 guarda una tasa de bits a partir de 1080 y superior, a una resolución de video más baja no es tan importante, por lo tanto, para codificar videos grandes, debe especificar h265 como predeterminado.
El retorno en Foreach-Object se ve muy extraño. Pero no hay nada que puedas hacer al respecto. FFmpeg escribe todo en stdout y esta es la forma más fácil de extraer un valor de dichos programas. Use este truco si necesita sacar algo de la salida estándar. No use Start-Process, para sacar stdout necesita llamar al archivo ejecutable directamente como en este ejemplo.
Es imposible llamar al ejecutable a lo largo de la ruta completa y obtener stdout de otra manera. Debe ir específicamente a la carpeta con el archivo ejecutable y llamarlo por su nombre desde allí. Para ello, en el bloque Comenzar, el script recuerda la ruta desde la que partió, para que luego de terminar su trabajo no moleste al usuario.
begin {
$Location = Get-Location
}
Esta función se vería bien como un cmdlet independiente, sería útil, pero para el futuro.
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
}
}
}
Uno de mis chistes favoritos es el interruptor de adentro hacia afuera. No hay un patrón coincidente en Powershell, pero tales construcciones lo reemplazan, en su mayor parte.
La función a ejecutar está entre paréntesis. Y si el resultado de esta función es igual a la condición en el conmutador, entonces el bloque de script se ejecuta en él.
3. Unir-Recortar
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 característica más importante en la tubería. Una función escrita correctamente debería mostrarle al usuario acerca de los errores, tienes que inflar el código de esta manera.
Por simplicidad, se decidió no encapsular las rutas a los archivos ejecutables en la clase, razón por la cual las funciones toman tantos argumentos.
Visualización de nuevos objetos
Para que este script pueda incrustarse en otras canalizaciones, debe configurarlo para que devuelva algo. Tenemos un InputObject tomado de Get-ChildItem, pero el campo Nombre es de solo lectura, no puede simplemente cambiar los nombres de los archivos.
Para que la salida del comando se vea como la salida del sistema, necesita guardar los nombres de los objetos recodificados y usar Get-Chilitem para agregarlos a la matriz y mostrarlos.
1. En el bloque Begin, declare una matriz
begin {
$OutputArray = @()
}
2. En el bloque Proceso, ingrese los archivos recodificados:
No se olvide de las verificaciones nulas, incluso en programación funcional son necesarias.
process {
if (Test-Path $Arguments.OutFileLiteralPath) {
$OutputArray += Get-Item -Path $Arguments.OutFileLiteralPath
}
}
3. En el bloque Final, devuelve la matriz resultante
end {
return $OutputArray
}
Hurra, terminó el bloque final, es hora de usar el script correctamente.
Usamos el guion
Ejemplo # 1
Este comando seleccionará todos los archivos en una carpeta, los convertirá al formato mp4 y enviará inmediatamente estos archivos a una unidad de red.
Get-ChildItem | ConvertTo-MP4 -Width 320 -Preset Veryslow | Copy-Item –Destination '\\local.smb.server\videofiles'
Ejemplo # 2
Recodifiquemos todos nuestros videos de juegos en la carpeta especificada y eliminemos las fuentes.
ConvertTo-MP4 -Path "C:\Users\Administrator\Videos\Escape From Tarkov\" | Remove-Item -Exclude $_
Ejemplo # 3
Codificar todos los archivos de una carpeta y mover archivos nuevos a otra carpeta.
Get-ChildItem | ConvertTo-WEBM | Move-Item -Destination D:\OtherFolder
Conclusión
Así que arreglamos ffmpeg, parece que no nos perdimos nada crítico. Pero, ¿qué es, ffmpeg no podría usarse sin un shell normal?
Resulta que sí.
Pero aún queda mucho trabajo por delante. Sería útil tener cmdlets como Measure-videoLenght como módulos, que devuelvan la duración de un video en forma de Timespan, con su ayuda sería posible simplificar la tubería y hacer el código más compacto.
Aún así, necesita hacer comandos ConvertTo-Webp y todo con este espíritu. También sería necesario crear una carpeta para el usuario, si no existe, de forma recursiva. Y comprobar el acceso de lectura y escritura también sería bueno.
Mientras tanto, sigue el proyecto en github .