1

I have a PowerShell script that calls ffmpeg to do a two-pass encode of all .mp4 video files in the current folder.

foreach ($i in Get-ChildItem . | Where { $_.extension -like ".mp4" }) {
  ffmpeg -y -i "$i" -c:v libx265 -b:v 2.2M -preset medium -x265-params pass=1 -an -f mkv NUL
  ffmpeg -i "$i" -c:v libx265 -b:v 2.2M -x265-params pass=2 -c:a libopus -ac 1 -b:a 64k -preset medium "small\$i.mkv"
}

It works perfectly but brings my system to a halt when in use, so I want to give the ffmpeg processes a low priority and an affinity that only lets them use two CPU cores. So I try

foreach ($i in Get-ChildItem . | Where { $_.extension -like ".mp4" }) {
  start "x265ify" /wait /low /b /affinity C ffmpeg -y -i "$i" -c:v libx265 -b:v 2.1M -preset medium -x265-params pass=1 -an -f mkv NUL
  start "x265ify" /wait /low /b /affinity C ffmpeg -i "$i" -c:v libx265 -b:v 2.1M -x265-params pass=2 -c:a libopus -ac 1 -b:a 64k -preset medium "small\$i.mkv"
}

This doesn't work, because the -i is considered to be ambiguous. So I try putting the entire command ("ffmpeg -y -i..." onward) in a string, with the quotation marks escaped. That doesn't work because a positional parameter can't be found that accepts /low. I try omitting that, passing the command as a separate variable, nothing I try seems to work.

Yet I have an Ruby script invoking ffmpeg the exact same way that does work. It reads

start "x265-ifier" /wait /low /b /affinity #{AFFINITY} ffmpeg \
              -hide_banner \
              -i "#{v}" \
              -vf scale=-2:#{HEIGHT} \
              -c:a libopus \
              -b:a #{AUDIO_BITRATE}k \
              -ac 1 \
              -c:v libx265 \
              -x265-params vbv-maxrate=#{MAX_VIDEO_BITRATE}:vbv-bufsize=7000 \
              -preset #{PRESET} \
              -crf #{CRF} \
              "#{dest}" \
              -y

So what am I doing wrong? Why does PowerShell not want to run this command, which runs okay if Ruby is the middleman? Why is /low not a parameter when it's listed in the documentation as one, and how can I make -i any less ambiguous when the entire string passed to start is supposed to be the command it's running, with -i as a valid argument of that command?

GreenTriangle
  • 2,382
  • 2
  • 21
  • 35

1 Answers1

0

In PowerShell, start is the built-in alias for the Start-Process cmdlet[1], whose syntax and behavior differ from the internal start command of cmd.exe.

In order to call the latter[1], you must invoke cmd.exe explicitly, as follows:

cmd /c start `"x265ify`" /wait /low /b /affinity C ffmpeg -y -i $i -c:v libx265 -b:v 2.1M -preset medium -x265-params pass=1 -an -f mkv NUL

Note:

  • The " chars. around x265ify are escaped as `" to ensure that they're passed through to start, which (awkwardly) uses them to distinguish between the first argument representing a window title vs. the command to invoke.

  • $i doesn't have to be double-quoted, because PowerShell will do that on demand.

  • Using /wait means that your second start command won't be invoked until the first one has completed.

As an aside: PowerShell's handling of empty arguments ("") and arguments with embedded " chars. passed to external programs is fundamentally broken as of PowerShell [Core] 7.0, requiring awkward workarounds - see this answer for more information.


[1] Use Get-Command start to discover this fact.

[2] which is necessary in your case, because Start-Process doesn't offer control over the launched process' priority or CPU affinity.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • Glad to hear it, @GreenTriangle. Please see my update, which describes how to identify a command as an alias, and links to a post with potential problems when passing arguments to external programs. – mklement0 Feb 20 '20 at 17:53