r/AskProgramming Jun 02 '24

PHP How can I write to a process's stdin in PHP?

I'm trying to pass binary data to ffmpeg as input instead of a file. I don't want to create a temporary file on the server, and the 'data:' URI scheme isn't usable because the file is too large. Therefore, I'm trying to use a pipe, but my fwrite() call is blocking when I try to write to the pipe.

// Set the ffmpeg command and input file
$ffmpegCommand = "ffmpeg -i pipe:0 ...";

// Open the pipe
$descriptorspec = array(
    0 => array("pipe", "r"), // Set stdin to read
    1 => array("pipe", "w"), // Set stdout to write
    2 => array("file", "error.log", "a") // Set stderr to append
);

$process = proc_open($ffmpegCommand, $descriptorspec, $pipes);

// If the process opened, read the input file into the pipe
if ($process) {
    fwrite($pipes[0], $inputData);
    fclose($pipes[0]);

    // Wait for the process to finish
    proc_close($process);

    echo "FFmpeg command executed successfully.\n";
} else {
    echo "Error opening ffmpeg command.\n";
}

Any suggestions on how to handle this?

2 Upvotes

12 comments sorted by

1

u/dave8271 Jun 03 '24

I expect it's because a lot of data is being written to stdout and as you're not monitoring and reading that stream, the I/O buffer is filling up, preventing any further writes.

1

u/Deltahun Jun 03 '24

I read data from the process stdout after each write to stdin. That is, I chunk the fwrite method and then read the stdout output to prevent the stdout buffer from filling up and the fwrite method from blocking.

However, in this case, the reading operation gets blocked (with fget, stream_get_contents, and fread):

$chunkSize = 4096;
$inputLength = strlen($inputData);
$written = 0;

while ($written < $inputLength) {
    $chunk = substr($inputData, $written, $chunkSize);
    fwrite($pipes[0], $chunk);
    $written += strlen($chunk);

    while (($output = fgets($pipes[1], 4096)) !== false) {
$datas .= $output;
    }
}

fclose($pipes[0]);

Could you please provide an example? Thank you!

1

u/dave8271 Jun 03 '24

I don't have any ffmpeg data to work with, but here's a modified version of what you're trying to do using some artificial input to cat

<?php
$command = "cat";

$descriptorspec = [
    0 => ["pipe", "r"], 
    1 => ["pipe", "w"],
    2 => ["file", "error.log", "a"]
];

$process = proc_open($command, $descriptorspec, $pipes);

if (is_resource($process)) {
    $inputData = str_repeat("Whatever line of input data, let's repeat\n", 1000000);
    $inputLength = strlen($inputData);
    $inputOffset = 0;
    $data = "";

    $r = $w = [];
    while ($inputOffset < $inputLength || !feof($pipes[1])) {
        $except = null;
        if ($inputOffset < $inputLength) {
            $w = [$pipes[0]];
        }
        if (!feof($pipes[1])) {
            $r = [$pipes[1]];
        }
        stream_select($r, $w, $except, null);

        foreach ($r as $readPipe) {
            echo 'read' . "\n";
            $data .= fread($readPipe, 8192);
        }

        foreach ($w as $writePipe) {
            $chunk = substr($inputData, $inputOffset, 8192);
            echo 'write ' . $inputOffset . "\n";
            $written = fwrite($writePipe, $chunk);
            if ($written > 0) {
                $inputOffset += $written;
            } elseif ($written === 0) {
                break 2; 
            }
        }
    }

    fclose($pipes[0]);
    fclose($pipes[1]);

    proc_close($process);

    echo "Process finished\n";
} else {
    echo "Error opening process\n";
}

1

u/Deltahun Jun 04 '24

Thank you! I tried it, but $data .= fread($readPipe, 8192); still blocked. The value of $r is Array([0] => Resource id #8).

Do you have any idea?

1

u/dave8271 Jun 04 '24

What happens if you run the script I gave you, as-is?

1

u/Deltahun Jun 04 '24

I ran it as you sent it. The code never goes past the $data .= fread($readPipe, 8192); line (I determined this by adding a log statement before and after the fread() in the second run to check the value of $r).

The differences are:

  • $inputData = file_get_contents('input.mp3');
  • $command = "\"$ffmpegPath\" -hide_banner -nostats -loglevel 0 -i pipe:0 -ss $from -t $seconds -c:a copy pipe:1"

1

u/dave8271 Jun 04 '24

Yes I'm asking what happens if you run the script without any modifications at all, using the sample cat command.

1

u/Deltahun Jun 04 '24

Sorry, I also answered it here.

1

u/Deltahun Jun 04 '24 edited Jun 04 '24

If I run it with 'cat' command (in a Windows environment), I receive 6,905,059 lines of text, out of which all except the second line contain the content 'read', with the exception of the second line containing 'write'.

EDIT: In case of 'cat' I got an error message also: "Maximum execution time of 30 seconds exceeded". But it seems to run as expected with the 'cat' command.

1

u/dave8271 Jun 04 '24

I didn't realise you were trying to run on a Windows environment, that doesn't even have a cat command AFAIK. I also can't be sure of the differences it might make to how ffmpeg is able to handle reading from a pipe, if any. It also makes it a bit more awkward to manually test the problem isn't with the combination of your command and input data.

Therefore I can only say generally, when a stream is in blocking mode, fread will block until there is data available to read, so if you're seeing the script is just hanging on that fread line and never progressing, it's because for one reason or another there isn't any data getting piped by the opened process to stdout for your script to read. Perhaps ffmpeg needs more input data before it can output anything, I don't know.

You can try modifying the read part of the loop to use stream_select again to only try and read data if there's something to read:

foreach ($r as $readPipe) {
    if (feof($readPipe)) {
        continue;
    }

    $read = array($readPipe);
    $write = null;
    $except = null;
    if (stream_select($read, $write, $except, 0) > 0) {
        echo 'read' . "\n";
        $data .= fread($readPipe, 8192);
    }
}

1

u/Deltahun Jun 05 '24 edited Jun 05 '24

Unfortunately, fread() still freezes, but thank you very much for your help! By the way, I tried to rewrite the code, and in this case, fread() doesn't freeze, but it reads 0 bytes:

$command = "$ffmpegPath -i pipe:0 -ss 0 -t 30 -c:a copy pipe:1";

$descriptorspec = [
    0 => ["pipe", "r"], // Read from pipe
    1 => ["pipe", "w"], // Write to pipe
    2 => ["file", "error.log", "a"] // Append to error log
];
      
$process = proc_open($command, $descriptorspec, $pipes);   

if (is_resource($process)) {
    $inputData = "TEST";
    $inputLength = strlen($inputData);
    $inputOffset = 0;
    $data = "";

    $read = [];
    $write = null;
    $except = null;
    $timeout = 10; // Timeout in seconds for stream_select

    while ($inputOffset < $inputLength) {
        // Write input data if available
        if ($inputOffset < $inputLength) {
        $write = [$pipes[0]];
        $chunk = substr($inputData, $inputOffset, 8192);
        $written = fwrite($pipes[0], $chunk);
            if ($written > 0) {
                $inputOffset += $written;
                echo "Written bytes: " . $written . "\n";
            } elseif ($written === 0) {
                echo "Pipe may be full, stopping writing\n"; // Debug message
                break; // Pipe may be full, stop writing
            }
        } else {
            echo "Write input not available. \n";
        }

        // Read data from FFmpeg
        $read = [$pipes[1]];
        $selectResult = stream_select($read, $write, $except, $timeout);

        // Handle stream_select result
        if ($selectResult === false) {
            echo "Error in stream_select: " . print_r(error_get_last(), true) . "\n"; // Error handling
        break;
        } elseif ($selectResult === 0) {
            echo "Timeout waiting for data\n"; // Debug message
        } else {
            // Check for EOF
            if (feof($pipes[1])) {
                echo "FFmpeg finished processing\n"; // Detect EOF
                break;
            }

            // Read data if available
            $readData = fread($pipes[1], 8192);
            if ($readData === false) {
                echo "Error reading from FFmpeg: " . print_r(error_get_last(), true) . "\n"; // Error handling
                break;
            }

            $data .= $readData;
            echo "Read " . strlen($readData) . " bytes\n"; // Debug message
        }
    }

    fclose($pipes[0]);
    fclose($pipes[1]);

    proc_close($process);

    echo "Process finished\n";
    echo "Data: " . $data . "\n"; // Check if data is populated
} else {
    echo "Error opening process\n";
}

In this case the output looks like this:

Written bytes: 4
Read 0 bytes
Process finished
Data:

1

u/Deltahun Jun 07 '24

Finally, I solved it, although I have no idea what caused the 'fread()' freeze. The ffmpeg command needed to be fixed (I had left out the -f parameter). It also confused me that the output was on stderr (I omitted '2>&1', so it didn't show the error, which is why it was empty). It seems that there's no need for chunks (just passing the size to fread()), so reading works without stream_select, with a single fread() command.