ffmpeg Progress Bar - Encoding Percentage in PHP
Asked Answered
C

3

28

I've written a whole system in PHP and bash on the server to convert and stream videos in HTML5 on my VPS. The conversion is done by ffmpeg in the background and the contents is output to block.txt.

Having looked at the following posts:

Can ffmpeg show a progress bar?

and

ffmpeg video encoding progress bar

amongst others, I can't find a working example.

I need to grab the currently encoded progress as a percentage.

The first post I linked above gives:

$log = @file_get_contents('block.txt');

preg_match("/Duration:([^,]+)/", $log, $matches);
list($hours,$minutes,$seconds,$mili) = split(":",$matches[1]);
$seconds = (($hours * 3600) + ($minutes * 60) + $seconds);
$seconds = round($seconds);

$page = join("",file("$txt"));
$kw = explode("time=", $page);
$last = array_pop($kw);
$values = explode(' ', $last);
$curTime = round($values[0]);
$percent_extracted = round((($curTime * 100)/($seconds)));

echo $percent_extracted;

The $percent_extracted variable echoes zero, and as maths is not my strong point, I really don't know how to progress here.

Here's one line from the ffmpeg output from block.txt (if it's helpful)

time=00:19:25.16 bitrate= 823.0kbits/s frame=27963 fps= 7 q=0.0 size= 117085kB time=00:19:25.33 bitrate= 823.1kbits/s frame=27967 fps= 7 q=0.0 size= 117085kB time=00:19:25.49 bitrate= 823.0kbits/s frame=27971 fps= 7 q=0.0 size= 117126kB

Please help me output this percentage, once done I can create my own progress bar. Thanks.

Changeless answered 11/7, 2012 at 21:8 Comment(0)
C
50

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. :)

Changeless answered 12/7, 2012 at 15:57 Comment(14)
Thanks! I'm developing a site which streams videos in HTML5 too. Your solution saved my day.Orpine
As a note, you should probably specify stdin to /dev/null or something so the process doesnt anticipate user input. You can do this with </dev/null This is good stuff though! Helped me figure out a solution to my problem!Wangle
Where would you put </dev/null ?Reprehension
What is the purpose of the 2>&1?Bakelite
@Bakelite From a two second google search: Here's what 2>&1 meansChangeless
How are you getting ffmpeg to output block.txt in the style you show it? Have you done any other processing to make it look like that? Have you changed this at all for recent ffmpeg versions?Senskell
@Senskell No, I haven't altered the output at all nor have I changed this for recent ffmpeg versions. Are you getting a different output?Changeless
thank you. this is exactly what i am looking for. however how do i convert the sec to mins?Ideomotor
What is the $matches value?Athanasia
@Drupalist If you look at the documentation for preg_match, you'll see it has a third optional (see the [ square brackets?) array parameter, $matches. If you pass it an array, because it's passed by reference (&), this array will get overwritten with the result. If you put in a variable that doesn't exist yet, it'll get created with that value. Another idiosyncrasy with PHP, why can't the return value be what we want? Oh well...Changeless
@Changeless thank you very much. I am building an online video editor (cut piece of video), and I was in need of a progress bar. Your code worked well.Athanasia
@Changeless Great Job .Here i am stacked with creating flock or pid for ffmpeg process is it possible to get pid for processing each file in a folder by running bash script to convert the video filesSubcutaneous
Thanks man. You just did what exactly i am trying to do. now i just need to create a js to show it every x seconds in real time.Elison
@Elison Such an old question here :D but even now, for a quick and dirty solution I would output to a text file and poll it just like you're going to do. You can try regexpal.com for simpler regexes.Changeless
E
8

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

Envious answered 5/1, 2015 at 17:10 Comment(1)
Thanks for this addition! eval() is inherently insecure in PHP, but if you could format your code (see format faqs) and add what version of ffmpeg is required, I'd happily up-vote you.Changeless
H
1

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);
Hypersensitize answered 26/6, 2013 at 8:9 Comment(4)
Cool, so mine is a PHP solution and yours is a JavaScript only solution then?Changeless
well - both would work but the js saves php some work... If you run a long video encoding process in php you would need a second process to read the logfiles (for each encoding). But if you read the logfile directly with javascript, you don't have to care about php processes.Hypersensitize
Well I like it, I think you should state clearly at the top that it requires Dojo, but here's a +1! :)Changeless
@Hypersensitize You still need to explain some things more, for novice people on this topic it will not be easy for them to understand the example you are setting, for example how to add the dojo library, in this line I get an error: request.post (logfile) .then (function (content) {.... Tell me that request is not defined, how do I solve that?Ewaewald

© 2022 - 2024 — McMap. All rights reserved.