FFmpeg encode percentage progress bar with PHP

Using an HTML progress bar to view FFmpeg video encode progress percentage, With the usage of Ajax and PHP to parse the FFMpeg output file.

This method enables a web-based form to encode video with FFmpeg along with being able to see its live progress.

The main page is index.php which displays the form for encoding settings along with the progress bar once the form is submitted.

The 3 pages are:

  • index.php
  • run.php
  • progress.php

With a style file (style.css) that is minimal Bootstrap 4.

run.php builds the FFmpeg command from the form parameters and executes it.

progress.php parses the FFmpeg output log to calculate the task completion percentage. This gets called every second with AJAX to have the ‘live’ progress.

index.php :

<?php
if (file_exists('output.txt')) {
    unlink('output.txt');
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>FFmpeg encode</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
    <script type="text/javascript">
        $(function () {
            setInterval(function () {
                $.get("progress.php", function (data) {
                    if (data === '') {
                        $('#progress-string').html(data);
                    } else {
                        $('#progress-string').html(`${data}%`);
                    }
                    $('#progressbar').attr('aria-valuenow', data).css('width', `${data}%`);
                });
            }, 1000);//1000 milliseconds = 1 second
        });
    </script>
    <script>
        $(document).ready(function () {
            $('#make_smaller').on('submit', function (e) {
                e.preventDefault();
                $.ajax({
                    type: "POST",
                    url: 'run.php',
                    data: {
                        input: $('#input').val(),
                        size: $('#size').val(),
                        codec: $('#codec').val(),
                        preset: $('#preset').val(),
                        crf: $('#crf').val(),
                        output: $('#output').val()
                    },
                });
            });
        });
    </script>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-12">
            <div class="card">
                <div class="card-body">
                    <form id="make_smaller" method="post">
                        <div class="form-row">
                            <div class="form-group col-12">
                                <label for="input">Input</label>
                                <input type="text" class="form-control" name="input" id="input" aria-describedby="input"
                                       placeholder="video.mp4">
                            </div>
                        </div>
                        <div class="form-row">
                            <div class="form-group col-12">
                                <label for="size">Resize to</label>
                                <select class="form-control" name="size" id="size">
                                    <option value="0">No resize</option>
                                    <option value="1">240p</option>
                                    <option value="2">360p</option>
                                    <option value="3">480p</option>
                                    <option value="4">720p</option>
                                    <option value="5">1080p</option>
                                    <option value="6">1440p</option>
                                    <option value="7">2160p</option>
                                </select>
                            </div>
                        </div>
                        <div class="form-row">
                            <div class="form-group col-12 col-lg-6">
                                <label for="codec">Codec</label>
                                <select class="form-control" id="codec" name="codec">
                                    <option value="1" selected>x264</option>
                                    <option value="2">x265</option>
                                </select>
                            </div>
                            <div class="form-group col-12 col-lg-6">
                                <label for="preset">Speed</label>
                                <select class="form-control" id="preset" name="preset">
                                    <option value="ultrafast">Ultrafast</option>
                                    <option value="superfast">Superfast</option>
                                    <option value="veryfast">Veryfast</option>
                                    <option value="faster">Faster</option>
                                    <option value="fast" selected>Fast</option>
                                    <option value="medium">Medium</option>
                                    <option value="slow">Slow</option>
                                    <option value="slower">Slower</option>
                                    <option value="veryslow">Veryslow</option>
                                </select>
                            </div>
                        </div>
                        <div class="form-row">
                            <div class="form-group col-12 col-lg-6">
                                <label for="crf">crf</label>
                                <input type="number" class="form-control" name="crf" id="crf" aria-describedby="crf"
                                       placeholder="23" value="23">
                            </div>
                            <div class="form-group col-12 col-lg-6">
                                <label for="output">Output</label>
                                <input type="text" class="form-control" name="output" id="output"
                                       aria-describedby="output"
                                       placeholder="out.mp4">
                            </div>
                        </div>
                        <div class="form-group">
                            <button type="submit" class="btn-green">Run</button>
                        </div>
                    </form>
                    <div<?php include_once('progress.php'); ?>
                    <div class="progress">
                        <div id="progressbar" class="progress-bar bg-info" role="progressbar" aria-valuenow="0"
                             aria-valuemin="0" aria-valuemax="100"><span id="progress-string"></span></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
</body>
</html>

This includes JQuery. The important 2 parts for index.php are as follows:

$(function () {
    setInterval(function () {
        $.get("progress.php", function (data) {
            if (data === '') {
                $('#progress-string').html(data);
            } else {
                $('#progress-string').html(`${data}%`);
            }
            $('#progressbar').attr('aria-valuenow', data).css('width', `${data}%`);
        });
    }, 1000);//1000 milliseconds = 1 second
});

This function continuously executes every 1 second (1000 milliseconds) calling progress.php getting its output which is the progress. Video duration / encode progress time. This output is used for the progress bar percentage number and fill amount.

$(document).ready(function () {
    $('#make_smaller').on('submit', function (e) {
        e.preventDefault();
        $.ajax({
            type: "POST",
            url: 'run.php',
            data: {
                input: $('#input').val(),
                size: $('#size').val(),
                codec: $('#codec').val(),
                preset: $('#preset').val(),
                crf: $('#crf').val(),
                output: $('#output').val()
            },
        });
    });
});

This is an Ajax POST request which enables form submission without a redirect. The parameters are passed through to run.php from the form.

run.php :

<?php
if (isset($_POST['input'])) {
    $input = $_POST['input'];
    $output = $_POST['output'];
    $preset = $_POST['preset'];
    $crf = $_POST['crf'];
    $size_int = $_POST['size'];
    if ($size_int == 0) {
        $scale = "";//No resize
    } elseif ($size_int == 1) {
        $scale = "-vf scale=352x240:flags=lanczos";
    } elseif ($size_int == 2) {
        $scale = "-vf scale=480x360:flags=lanczos";
    } elseif ($size_int == 3) {
        $scale = "-vf scale=640x480:flags=lanczos";
    } elseif ($size_int == 4) {
        $scale = "-vf scale=1280x720:flags=lanczos";
    } elseif ($size_int == 5) {
        $scale = "-vf scale=1920x1080:flags=lanczos";
    } elseif ($size_int == 6) {
        $scale = "-vf scale=2560x1440:flags=lanczos";
    } elseif ($size_int == 7) {
        $scale = "-vf scale=3840x2160:flags=lanczos";
    }
    if ($_POST['codec'] == 1){
        $codec = "libx264";
    } else {
        $codec = "libx265";
    }
    $command = "ffmpeg -i $input $scale -c:v $codec -preset $preset -crf $crf $output -y 1> output.txt 2>&1";
    shell_exec($command);
} else {
    echo "Did not come from the form";
    exit;
}

 

This builds the FFmpeg command from the submitted form parameters. If it is accessed externally ‘Did not come from the form’ will be displayed.

Progress.php is adapted from jimbo at StackOverflow from here.

progress.php :

<?php
$ffmpeg_output = @file_get_contents('output.txt');
if ($ffmpeg_output) {
    preg_match("/Duration: (.*?), start:/", $ffmpeg_output, $a_match);
    $duration_as_time = $a_match[1];
    $time_array = array_reverse(explode(":", $duration_as_time));
    $duration = floatval($time_array[0]);
    if (!empty($time_array[1])) $duration += intval($time_array[1]) * 60;
    if (!empty($time_array[2])) $duration += intval($time_array[2]) * 60 * 60;
    preg_match_all("/time=(.*?) bitrate/", $ffmpeg_output, $a_match);
    $raw_time = array_pop($a_match);
    if (is_array($raw_time)) {
        $raw_time = array_pop($raw_time);
    }
    $time_array = array_reverse(explode(":", $raw_time));
    $encode_at_time = floatval($time_array[0]);
    if (!empty($time_array[1])) $encode_at_time += intval($time_array[1]) * 60;
    if (!empty($time_array[2])) $encode_at_time += intval($time_array[2]) * 60 * 60;
    echo round(($encode_at_time / $duration) * 100);
}

This is the script that gets called every 1 second, it outputs the percentage of the encode task being complete as seen by round(($encode_at_time / $duration) * 100). You could also build on video duration and encode task time elapsed.

style.css (optional, gives form style):

:root{--breakpoint-xs:0;--breakpoint-sm:576px;--breakpoint-md:768px;--breakpoint-lg:992px;--breakpoint-xl:1200px;--font-family-sans-serif:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}*,::after,::before{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;text-align:left;background-color:#36393A;color:#FDF1E7}.card-body{background-color:#0F2417;-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card{position:relative;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#252625;background-clip:border-box;border:1px solid #31724B75;border-radius:.2rem}.btn-green{font-weight:400;text-align:center;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;padding:.375rem .75rem .375rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;color:#fff;background-color:#5f886d;border:1px solid transparent;border-color:#73976b;display:block;width:100%}.btn-green:hover{background-color:#51775c;border-color:#7fa477}.btn-green:not(:disabled):not(.disabled).active,.btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{background-color:#54a060;border-color:#00bf42}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#fff;background-color:#858881;background-clip:padding-box;border:1px solid #4d6851;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control:focus{color:#fff;background-color:#575757;border-color:#80ff8870;outline:0;box-shadow:0 0 0 .2rem rgba(68,255,0,.2)}.progress{display:-ms-flexbox;display:flex;height:1.2rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem;margin-top:1.8rem;text-align:center}
label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}select{word-wrap:normal}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width:576px){.container{max-width:540px}}@media (min-width:768px){.container{max-width:720px}}@media (min-width:992px){.container{max-width:960px}}@media (min-width:1200px){.container{max-width:1140px}}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}.row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.col-12,.col-lg-6{position:relative;width:100%;padding-right:15px;padding-left:15px}.col-12{-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}@media (min-width:992px){.col-lg-6{-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}}.form-control:focus{color:#495057;background-color:#fff;border-color:#80bdff;outline:0;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.form-control::-webkit-input-placeholder{color:#6c757d;opacity:1}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control:-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::-ms-input-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-group{margin-bottom:1rem}.form-row{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*=col-]{padding-right:5px;padding-left:5px}.progress-bar{background-color:#17a2b8!important;text-align: center;}

ffmpeg encode progress with php 1
ffmpeg encode progress with php 2