0

I'm trying to write my first PowerShell GUI. Basically I'm trying to run a ffmpeg command which is fine and works, but I cannot get the progress bar to run. (I'm brand new to this.) Here is my attempt.

cd C:\Users\brett\Documents\convert
$cmd = 'ffmpeg.exe'
$arg0 = '-i'
$arg1 = 'MASH_01.ts'
$arg2 = '-c:v'
$arg3 = '-c:a'
$arg4 = 'MASH_01.mp4'
$cf = 'copy'
$isFile = 'MASH_01.mp4'
if (-not(Test-Path -Path $isFile) -eq $false) {
  echo "Shit Go"
  del $isFile

      & $cmd $arg0 $arg1 $arg2 $cf $arg3 $cf $arg4
    for ($i = 1; $i -le 100; $i++) {
    Start-Sleep -m 100
    Write-Progress -Activity 'Progress Of The Coversion' -Status "$i Percent Complete" -PercentComplete $i;
    }
} else {
& $cmd $arg0 $arg1 $arg2 $cf $arg3 $cf $arg4
    for ($i = 1; $i -le 100; $i++) {
    Start-Sleep -m 100
    Write-Progress -Activity 'Progress Of The Coversion' -Status "$i Percent Complete" -PercentComplete $i;
    }
}

Update: There is no error output I can see, but the progress bar runs after the file has been processed, not during.

here is my latest attempt.. but now i get ffmpeg saying "m" is not a valid switch

cd C:\Users\brett\Documents\convert
$oldVideo = Get-ChildItem -Include @("*.ts")
Write-Host -ForegroundColor Green -Object $ArgumentList;
# Pause the script until user hits enter
$isFile = 'MASH_01.mp4'
if( -not(Test-Path -Path $isFile) -eq $false) {
  echo "Shit Go"
  del $isFile
  }a
  $tool = ffmpeg.exe
$ArgumentList = '`-i'+' '+'MASH_01.ts'+' '+'-c:v'+' '+'copy'+' '+'-c:a'+' '+'copy'+' '+'MASH_01.mp4';
Invoke-Expression $tool $ArgumentList
for($i = 1; $i -le 100; $i++){
    ffmpeg $ArgumentList -m 100

    Write-Progress -Activity 'Progress Of The Coversion' -Status "$i Percent Complete" -PercentComplete $i 
   `-SecondsRemaining $a -CurrentOperation
   "$i% complete" `

    }
Brett
  • 376
  • 7
  • 24

2 Answers2

4

I know that this post is now 7+ years old, but it still shows up near the top of searches for "ffmpeg display progress bar powershell" and such, so I thought I'd contribute a working example.

Environment

Show me the code

$theFile = "path\to\file\video.mp4"
$frames = Invoke-Expression "ffprobe.exe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 ""$($theFile)"""
$ffmpeg_params = "-i  ""$($theFile)"" ""$($theFile)-processed.mkv"""

Write-Progress -Activity "ffmpeg" -Status "$theFile" -PercentComplete 0

$p = New-Object System.Diagnostics.Process
$p.StartInfo.Filename = "ffmpeg.exe"
$p.StartInfo.Arguments = "$ffmpeg_params"
$p.StartInfo.UseShellExecute = $false
$p.StartInfo.RedirectStandardError = $true
$p.StartInfo.RedirectStandardOutput = $true
$p.StartInfo.CreateNoWindow = $true
$p.Start() | Out-Null

while (-not $p.HasExited) {
  if ($p.StandardError.Peek()) {
    $line = $p.StandardError.ReadLineAsync().Result
    if ($line) {
      $info = $line.Trim();
      if ($info.StartsWith("frame=")) {
        $progperc = ([int]($info.split(" fps")[0]).split("=")[1] / $frames) * 100;
        if ($progperc -gt $PrevProgress) {
          Write-Progress "ffmpeg" -Status "$theFile" -PercentComplete $progperc
          $PrevProgress = $progperc
        }
      }
    }
  }
}

$p.WaitForExit();

Write-Progress  "ffmpeg" -Status "$theFile" -PercentComplete 100 -Completed

Let's tear this apart

An important note first

By default, ffmpeg sends its text output to stderr and not the expected stdout. This is because ffmpeg is capable of outputting streams on stdout for other consumption. This is non-obvious since most shells redirect stderr to stdout silently, but when trying to capture output from a process this is important. Note that you could redirect stderr to stdout with 2>&1, but the method presented here works just as well (e.g. capturing and reading stderr). Note that ffprobe does not, which also makes sense when you consider what the utility does.

Here we go

I won't explain the first line, which brings us to:

$frames = Invoke-Expression "ffprobe.exe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 ""$($theFile)"""

Since displaying a progress bar depends on knowing the final value to count to, we get the number of frames in the video. ffmpeg's output during processing meshes well with this (it's also possible to deal with duration in time since the output has that too and calculate that way, but it's significantly more annoying.)

$ffmpeg_params = "-i ""$($theFile)"" ""$($theFile)-processed.mkv"""

This is just setting up the parameters to pass to ffmpeg. Note that this particular set of parameters basically doesn't do anything except cross-encode from whatever your .mp4 is to a .mkv format with defaults, which is mostly useless except as a demo. Replace this with whatever parameters you need to pass to ffmpeg.

Write-Progress -Activity "ffmpeg" -Status "$theFile" -PercentComplete 0

This throws up the actual progress bar. Play with the -Activity and -Status parameters however you see fit.

$p = New-Object System.Diagnostics.Process
$p.StartInfo.Filename = "ffmpeg.exe"
$p.StartInfo.Arguments = "$ffmpeg_params"
$p.StartInfo.UseShellExecute = $false
$p.StartInfo.RedirectStandardError = $true
$p.StartInfo.RedirectStandardOutput = $true
$p.StartInfo.CreateNoWindow = $true
$p.Start() | Out-Null

This block is fairly boilerplate; it creates a child process for ffmpeg, passes its arguments, and (here's the important part) sets the parent process (PowerShell) up to read both stderr and stdout from the child process it will create (ffmpeg). It then starts that child process with the passed in arguments and command line.

Important

A common problem with the above block is that your command, in this case ffmpeg.exe, isn't in your path. You should do more work to ensure that ffmpeg.exe is present and probably pass it a fully-qualified path.

while (-not $p.HasExited) {
  if ($p.StandardError.Peek()) {
    $line = $p.StandardError.ReadLineAsync().Result
    if ($line) {
      $info = $line.Trim();
      if ($info.StartsWith("frame=")) {
        $progperc = ([int]($info.split(" fps")[0]).split("=")[1] / $frames) * 100;
        if ($progperc -gt $PrevProgress) {
          Write-Progress "ffmpeg" -Status "$theFile" -PercentComplete $progperc
          $PrevProgress = $progperc
        }
      }
    }
  }
}

This while loop is checking if the process, running ffmpeg, has exited. If it hasn't, then it "Peeks" (that is, returns the next available character in the stream but doesn't consume it) at the StandardError stream to see if there's something there. If there is, it reads the line in in an asynchronous fashion (non-blocking to the process), trims it, and then the real magic starts.

For completeness, it's important to know that ffmpeg's process output (once it's done giving you a ton of information on what you told it to do) looks a lot like this:

frame=    1 fps=0.0 q=0.0 size=       5kB time=00:00:00.00 bitrate=N/A speed=   0x
frame=    1 fps=0.0 q=0.0 size=       5kB time=00:00:00.00 bitrate=N/A speed=   0x
frame=    2 fps=0.0 q=0.0 size=       5kB time=00:00:00.00 bitrate=N/A speed=   0x
frame=    2 fps=0.0 q=0.0 size=       5kB time=00:00:00.00 bitrate=N/A speed=   0x
frame=    2 fps=1.9 q=0.0 size=       5kB time=00:00:00.00 bitrate=N/A speed=   0x
frame=    2 fps=1.9 q=0.0 size=       5kB time=00:00:00.00 bitrate=N/A speed=   0x
frame=   16 fps= 10 q=0.0 size=       5kB time=00:00:00.59 bitrate=  63.5kbits/s speed=0.379x
frame=   16 fps= 10 q=0.0 size=       5kB time=00:00:00.59 bitrate=  63.5kbits/s speed=0.379x
frame=   31 fps= 15 q=0.0 size=       5kB time=00:00:01.09 bitrate=  34.6kbits/s speed=0.528x
frame=   31 fps= 15 q=0.0 size=       5kB time=00:00:01.09 bitrate=  34.6kbits/s speed=0.528x
frame=   46 fps= 18 q=0.0 size=       5kB time=00:00:01.59 bitrate=  23.8kbits/s speed=0.616x
frame=   46 fps= 18 q=0.0 size=       5kB time=00:00:01.59 bitrate=  23.8kbits/s speed=0.616x
frame=   61 fps= 20 q=0.0 size=       5kB time=00:00:02.09 bitrate=  18.1kbits/s speed=0.678x
frame=   61 fps= 20 q=0.0 size=       5kB time=00:00:02.09 bitrate=  18.1kbits/s speed=0.678x
frame=   76 fps= 21 q=29.0 size=       5kB time=00:00:02.59 bitrate=  14.7kbits/s speed=0.718x
frame=   76 fps= 21 q=29.0 size=       5kB time=00:00:02.59 bitrate=  14.7kbits/s speed=0.718x
frame=   91 fps= 22 q=29.0 size=       5kB time=00:00:03.09 bitrate=  12.3kbits/s speed=0.75x
frame=   91 fps= 22 q=29.0 size=       5kB time=00:00:03.09 bitrate=  12.3kbits/s speed=0.75x
frame=  106 fps= 23 q=29.0 size=       5kB time=00:00:03.59 bitrate=  10.6kbits/s speed=0.775x
frame=  106 fps= 23 q=29.0 size=       5kB time=00:00:03.59 bitrate=  10.6kbits/s speed=0.775x

... which, in recent versions of ffmpeg at least, only shows up on a continuously changing single line about once per second. From the perspective of our child process though, it streams an entire line each time it updates, which is what we'll be using to calculate where our progress is on the progress bar. You can probably see the important information right at the start of the line: frame= 2 and so on.

Therefore, back to our code; so long as the line out output starts with frame=, we grab that line, do some splitting on it (this is probably inefficient and is definitely prone to breaking should ffmpeg's output format change; I'm certain someone out there could do this much more elegantly than I have, but what I have works), and compare that number to the total number of frames we have to process. Multiply that number by 100 to get a percent, and we have our progress, so, as long as the current progress is greater than the last one, we update the progress bar.

$p.WaitForExit();

Again, this is boilerplate child process stuff and just waits for the process to exit before anything else happens in the script.

You should also do some validation here to see if it exited with a regular exit code or not (if ($p.ExitCode -ne 0)) and so something valuable with that.

The last line just completes the progress bar and makes it go away from the UI.

That's it! Modify as needed.

Unimportant to the question--but relevant to the answer--information

ffmpeg and its siblings (e.g. ffprobe) require double-quoted (") filenames if the filename or path has spaces in it, which causes a lot of script developers heartburn. To deal with this in PowerShell, we double-double quote, so the double-double quotes you see in the commands are to protect those quotations in the actual commands which ensures that if your filename has spaces in it that it'll get processed properly.

Sources

https://stackoverflow.com/a/28376817/13902318 for the ffprobe command.

Most of the progress bar code has been adapted from https://github.com/JoJoBond/Powershell-YouTube-Upload.

RyanJ
  • 63
  • 7
  • 2
    Had to change the way to detect frames, as the split method uses a `char[]` as parameter, therefore it was splitting on either space, f, p, or s instead of " fps". So i changed `$progperc = ([int]($info.split(" fps")[0]).split("=")[1] / $frames) * 100;` with `$progperc = ([int](($info -split " fps")[0] -split "=")[1] / $frames) * 100;` – Muldec Sep 04 '21 at 08:47
1

To see the progress functioning use:

cls
for($i =1; $i -le 100 ; $i++){
Start-Sleep -m 100
Write-Progress -Activity 'Progress Of The Conversion' -Status "$i Percent Complete" -PercentComplete $i
}

replace start-sleep cmdlet with your own code, ideally have the percent calculating on something:

http://blogs.technet.com/b/kpalmvig/archive/2013/10/29/easy-progress-bar-in-your-powershell-scripts.aspx

Example: 1. Put your commands in a txt file - one per line - you don't need the & in the text file 2. Use this code to run:

    cls
    $file = get-content 'c:\yourFolder\yourTextFile.txt'

    $i = 1
    foreach($line in $file) {

      $pct = (($i / $file.count) * 100)
      Write-Progress -activity "Processing...$i" -status "Progress: $([int]$pct)%" -PercentComplete $pct

      &"$line"

      $i++

}
Jimbo
  • 2,529
  • 19
  • 22
  • That seems to be my problem, im unsure what to replace Start-Sleep with, It still runs after the file has processed. Do i need to use Get-Process to have the progress bar show as it runs? And how would i do that? – Brett Jul 23 '14 at 13:06
  • replace start-sleep with the code for ffmpeg however, if it is just one file then the progress bar is not going to tell you much since you are not receiving events back from ffmpeg so, as far as I can see, a progress bar makes sense if you are doing a batch of ffmpeg commands, then you can report on progress as each one finishes. – Jimbo Jul 23 '14 at 13:22
  • That is the intended goal this is only the first command i have 4 others to come after this – Brett Jul 23 '14 at 13:43
  • 1
    create an array with the values that change for each call to ffmpeg then loop through that array, updating progress for each call – Jimbo Jul 23 '14 at 13:53
  • we're not supposed to write code but if you post, say, the values for 3 of the ffmpeg calls I will show you how to do it... – Jimbo Jul 23 '14 at 17:00
  • ok cool here are my commands ffmpeg -i MASH_01.ts -c:v copy -c:a copy MASH_01.mp4 &ffmpeg -i MASH_01.mp4 -vcodec libxvid -vtag XVID -b:v 2693k -bf 2 -g 300 -s 1024x768 -pass 1 -an -threads 0 -f rawvideo -y NULL & ffmpeg -i MASH_01.mp4 -vcodec libxvid -vtag XVID -b:v 2693k -bf 2 -g 300 -s 1024x768 -acodec libmp3lame -ab 128k -ar 48000 -ac 2 -pass 2 -threads 0 -f avi MASH_01.avi & ffmpeg -i MASH_01.avi -c:v libx264 -g 1 -crf 18 -c:a copy -preset slower LAST_RECORDED.avi – Brett Jul 23 '14 at 17:02
  • Was that closer Jimbo ? – Brett Jul 23 '14 at 17:23
  • It keeps telling me ffmpeg is not a cmdlet or recognized function – Brett Jul 23 '14 at 19:29
  • The term 'ffmpeg.exe is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again. At line:12 char:5 + & "$line" – Brett Jul 23 '14 at 19:31
  • add the path to ffmpeg in your commands inside the txt file, e.g. c:\ffmegFolder\ffmeg.exe etc – Jimbo Jul 23 '14 at 19:31
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/57871/discussion-between-brett-and-jimbo). – Brett Jul 23 '14 at 19:45