Interfacing PHP with FFmpeg via Standard IO

Interfacing PHP with the FFmpeg binary via standard input, output, and error streams (I/O) allows you to process media files dynamically in memory without writing temporary files to disk. This article provides a straightforward guide on using PHP’s proc_open function to pass video data directly to FFmpeg via stdin, retrieve the processed output through stdout, and capture diagnostic logs using stderr.

1. Understanding the FFmpeg Pipeline

To redirect standard I/O streams, you must instruct FFmpeg to read from and write to the system pipes instead of physical files.

2. Setting Up proc_open in PHP

PHP’s proc_open execution function allows fine-grained control over execution pipes. You define a descriptor specification array to map PHP file pointers to FFmpeg’s standard streams.

$descriptors = [
    0 => ['pipe', 'r'], // stdin (write to FFmpeg)
    1 => ['pipe', 'w'], // stdout (read from FFmpeg)
    2 => ['pipe', 'w']  // stderr (read FFmpeg errors/logs)
];

3. Implementation Code

Below is a complete, non-blocking PHP script that reads a source video file, streams it into FFmpeg to convert it, and captures the output.

<?php

// Define the FFmpeg command
// This example converts an incoming stream to an MP4 with streaming-compatible flags
$cmd = 'ffmpeg -i pipe:0 -f mp4 -movflags frag_keyframe+empty_moov pipe:1';

$descriptors = [
    0 => ['pipe', 'r'], // stdin
    1 => ['pipe', 'w'], // stdout
    2 => ['pipe', 'w']  // stderr
];

$process = proc_open($cmd, $descriptors, $pipes);

if (is_resource($process)) {
    // Set all stream pipes to non-blocking mode to prevent deadlocks
    stream_set_blocking($pipes[0], 0);
    stream_set_blocking($pipes[1], 0);
    stream_set_blocking($pipes[2], 0);

    // Load sample input data (e.g., a local video file)
    $inputData = file_get_contents('input.avi');
    $inputLength = strlen($inputData);
    $bytesWritten = 0;
    
    $outputData = '';
    $errorData = '';

    // Loop until the process finishes and all buffers are cleared
    while (true) {
        $read = [$pipes[1], $pipes[2]];
        $write = ($bytesWritten < $inputLength) ? [$pipes[0]] : null;
        $except = null;

        // Wait for activity on the streams
        if (stream_select($read, $write, $except, 1) === false) {
            break;
        }

        // Write chunk of input data to FFmpeg's stdin
        if ($write && in_array($pipes[0], $write)) {
            $chunk = substr($inputData, $bytesWritten, 65536);
            $written = fwrite($pipes[0], $chunk);
            if ($written > 0) {
                $bytesWritten += $written;
            }
            // Close stdin once all input data has been sent
            if ($bytesWritten >= $inputLength) {
                fclose($pipes[0]);
            }
        }

        // Read chunk of output data from FFmpeg's stdout
        if (in_array($pipes[1], $read)) {
            $chunk = fread($pipes[1], 65536);
            if ($chunk !== false) {
                $outputData .= $chunk;
            }
        }

        // Read chunk of logs/errors from FFmpeg's stderr
        if (in_array($pipes[2], $read)) {
            $chunk = fread($pipes[2], 65536);
            if ($chunk !== false) {
                $errorData .= $chunk;
            }
        }

        // Terminate loop if the process has stopped
        $status = proc_get_status($process);
        if (!$status['running'] && feof($pipes[1]) && feof($pipes[2])) {
            break;
        }
    }

    // Clean up remaining open pipes and close the process
    if (is_resource($pipes[0])) fclose($pipes[0]);
    fclose($pipes[1]);
    fclose($pipes[2]);
    
    $exitCode = proc_close($process);

    if ($exitCode === 0) {
        // Save the processed standard output data to a file
        file_put_contents('output.mp4', $outputData);
        echo "Conversion successful!";
    } else {
        echo "Error during conversion (Exit code: $exitCode):\n";
        echo $errorData;
    }
}

4. Key Considerations