ffmpeg Progress Bar - Encoding Percentage in PHP

ffmpeg now has a progress option, which gives output more easily parsed.

ffmpeg -progress block.txt -i path/to/input.mov -vcodec videocodec -acodec audiocodec path/to/output.flv 2>&1

Before you start encoding you can get the total frames, and a lot of other info with this (this is what would be done with bash. I'm a Perl programmer so I don't know how you'd get the info into your PHP script).

eval $(ffprobe -of flat=s=_ -show_entries stream=height,width,nb_frames,duration,codec_name path/to/input.mov);
width=${streams_stream_0_width};
height=${streams_stream_0_height};
frames=${streams_stream_0_nb_frames};
videoduration=${streams_stream_0_duration};
audioduration=${streams_stream_1_duration};
codec=${streams_stream_0_codec_name};
echo $width,$height,$frames,$videoduration,$audioduration,$codec;

-of flate=s=_ says to put each name=value on a separate line. -show_entries tells it to show the entries from what follows (stream for -show_streams, format for -show_format, etc.) stream=... says to show those items from the -show_streams output. Try the following to see what is available:

ffprobe -show_streams path/to/input.mov

The output to the progress file is added to approximately once a second. Content, after the encoding is finished, looks like the following. In my script, once a second I am putting the file into an array, and traversing the array in reverse order, using only what is between the first [last before reversal] two "progress" lines I find, so that I am using the most recent info from the end of the file. There may be better ways. This is from an mp4 with no audio so there is only one stream.

frame=86
fps=0.0
stream_0_0_q=23.0
total_size=103173
out_time_ms=1120000
out_time=00:00:01.120000
dup_frames=0
drop_frames=0
progress=continue
frame=142
fps=140.9
stream_0_0_q=23.0
total_size=415861
out_time_ms=3360000
out_time=00:00:03.360000
dup_frames=0
drop_frames=0
progress=continue
frame=185
fps=121.1
stream_0_0_q=23.0
total_size=1268982
out_time_ms=5080000
out_time=00:00:05.080000
dup_frames=0
drop_frames=0
progress=continue
frame=225
fps=110.9
stream_0_0_q=23.0
total_size=2366000
out_time_ms=6680000
out_time=00:00:06.680000
dup_frames=0
drop_frames=0
progress=continue
frame=262
fps=103.4
stream_0_0_q=23.0
total_size=3810570
out_time_ms=8160000
out_time=00:00:08.160000
dup_frames=0
drop_frames=0
progress=continue
frame=299
fps=84.9
stream_0_0_q=-1.0
total_size=6710373
out_time_ms=11880000
out_time=00:00:11.880000
dup_frames=0
drop_frames=0
progress=end


if javascript updates your progress bar, javascript could perform step 2 "directly" :

[this example requires dojo ]


1 php: start conversion and write status to a textfile - example syntax:

exec("ffmpeg -i path/to/input.mov path/to/output.flv 1>path/to/output.txt 2>&1");

For the second part we need just javascript to read the file. The following example uses dojo.request for AJAX, but you could use jQuery or vanilla or whatever as well :

[2] js: grab the progress from the file:

var _progress = function(i){
    i++;
    // THIS MUST BE THE PATH OF THE .txt FILE SPECIFIED IN [1] : 
    var logfile = 'path/to/output.txt';

/* (example requires dojo) */

request.post(logfile).then( function(content){
// AJAX success
    var duration = 0, time = 0, progress = 0;
    var result = {};

    // get duration of source
    var matches = (content) ? content.match(/Duration: (.*?), start:/) : [];
    if( matches.length>0 ){
        var rawDuration = matches[1];
        // convert rawDuration from 00:00:00.00 to seconds.
        var ar = rawDuration.split(":").reverse();
        duration = parseFloat(ar[0]);
        if (ar[1]) duration += parseInt(ar[1]) * 60;
        if (ar[2]) duration += parseInt(ar[2]) * 60 * 60;

        // get the time 
        matches = content.match(/time=(.*?) bitrate/g);
        console.log( matches );

        if( matches.length>0 ){
            var rawTime = matches.pop();
            // needed if there is more than one match
            if (lang.isArray(rawTime)){ 
                rawTime = rawTime.pop().replace('time=','').replace(' bitrate',''); 
            } else {
                rawTime = rawTime.replace('time=','').replace(' bitrate','');
            }

            // convert rawTime from 00:00:00.00 to seconds.
            ar = rawTime.split(":").reverse();
            time = parseFloat(ar[0]);
            if (ar[1]) time += parseInt(ar[1]) * 60;
            if (ar[2]) time += parseInt(ar[2]) * 60 * 60;

            //calculate the progress
            progress = Math.round((time/duration) * 100);
        }

        result.status = 200;
        result.duration = duration;
        result.current  = time;
        result.progress = progress;

        console.log(result);

        /* UPDATE YOUR PROGRESSBAR HERE with above values ... */

        if(progress==0 && i>20){
            // TODO err - giving up after 8 sec. no progress - handle progress errors here
            console.log('{"status":-400, "error":"there is no progress while we tried to encode the video" }'); 
            return;
        } else if(progress<100){ 
            setTimeout(function(){ _progress(i); }, 400);
        }
    } else if( content.indexOf('Permission denied') > -1) {
        // TODO - err - ffmpeg is not executable ...
        console.log('{"status":-400, "error":"ffmpeg : Permission denied, either for ffmpeg or upload location ..." }');    
    } 
},
function(err){
// AJAX error
    if(i<20){
        // retry
        setTimeout(function(){ _progress(0); }, 400);
    } else {
        console.log('{"status":-400, "error":"there is no progress while we tried to encode the video" }');
        console.log( err ); 
    }
    return; 
});
}
setTimeout(function(){ _progress(0); }, 800);

Okay, I've found what I needed - and hopefully this helps someone else as well!

First and foremost, you want to output the ffmpeg data to a text file on the server.

ffmpeg -i path/to/input.mov -vcodec videocodec -acodec audiocodec path/to/output.flv 1> block.txt 2>&1

So, the ffmpeg output is block.txt. Now in PHP, let's do this!

$content = @file_get_contents('../block.txt');

if($content){
    //get duration of source
    preg_match("/Duration: (.*?), start:/", $content, $matches);

    $rawDuration = $matches[1];

    //rawDuration is in 00:00:00.00 format. This converts it to seconds.
    $ar = array_reverse(explode(":", $rawDuration));
    $duration = floatval($ar[0]);
    if (!empty($ar[1])) $duration += intval($ar[1]) * 60;
    if (!empty($ar[2])) $duration += intval($ar[2]) * 60 * 60;

    //get the time in the file that is already encoded
    preg_match_all("/time=(.*?) bitrate/", $content, $matches);

    $rawTime = array_pop($matches);

    //this is needed if there is more than one match
    if (is_array($rawTime)){$rawTime = array_pop($rawTime);}

    //rawTime is in 00:00:00.00 format. This converts it to seconds.
    $ar = array_reverse(explode(":", $rawTime));
    $time = floatval($ar[0]);
    if (!empty($ar[1])) $time += intval($ar[1]) * 60;
    if (!empty($ar[2])) $time += intval($ar[2]) * 60 * 60;

    //calculate the progress
    $progress = round(($time/$duration) * 100);

    echo "Duration: " . $duration . "<br>";
    echo "Current Time: " . $time . "<br>";
    echo "Progress: " . $progress . "%";

}

This outputs the percentage of time left.

You can have this as the only piece of text echoed out to a page, and from another page you can perform an AJAX request using jQuery to grab this piece of text and output it into a div, for example, to update on your page every 10 seconds. :)