Rumble
Rumble

Reputation: 826

FFmpeg - feed raw frames via pipe - FFmpeg does not detect pipe closure

Im trying to follow these examples from C++ in Windows. Phyton Example C# Example

I have an application that produces raw frames that shall be encoded with FFmpeg. The raw frames are transfered via IPC pipe to FFmpegs STDIN. That is working as expected, FFmpeg even displays the number of frames currently available.

The problem occours when we are done sending frames. When I close the write end of the pipe I would expect FFmpeg to detect that, finish up and output the video. But that does not happen. FFmpeg stays open and seems to wait for more data.

I made a small test project in VisualStudio.

#include "stdafx.h" 
//// stdafx.h 
//#include "targetver.h"
//#include <stdio.h>
//#include <tchar.h>
//#include <iostream>

#include "Windows.h"
#include <cstdlib>

using namespace std;

bool WritePipe(void* WritePipe, const UINT8 *const Buffer, const UINT32 Length)
{
    if (WritePipe == nullptr || Buffer == nullptr || Length == 0)
    {
        cout << __FUNCTION__ << ": Some input is useless";
        return false;
    }

    // Write to pipe
    UINT32 BytesWritten = 0;
    UINT8 newline = '\n';
    bool bIsWritten = WriteFile(WritePipe, Buffer, Length, (::DWORD*)&BytesWritten, nullptr);
    cout << __FUNCTION__ << " Bytes written to pipe " << BytesWritten << endl;
    //bIsWritten = WriteFile(WritePipe, &newline, 1, (::DWORD*)&BytesWritten, nullptr); // Do we need this? Actually this should destroy the image.

    FlushFileBuffers(WritePipe); // Do we need this?

    return bIsWritten;
}

#define PIXEL 80 // must be multiple of 8. Otherwise we get warning: Bytes are not aligned

int main()
{
    HANDLE PipeWriteEnd = nullptr;
    HANDLE PipeReadEnd = nullptr;
    {
        // create us a pipe for inter process communication
        SECURITY_ATTRIBUTES Attr = { sizeof(SECURITY_ATTRIBUTES), NULL, true };
        if (!CreatePipe(&PipeReadEnd, &PipeWriteEnd, &Attr, 0))
        {
            cout << "Could not create pipes" << ::GetLastError() << endl;
            system("Pause");
            return 0;
        }
    }

    // Setup the variables needed for CreateProcess
    // initialize process attributes
    SECURITY_ATTRIBUTES Attr;
    Attr.nLength = sizeof(SECURITY_ATTRIBUTES);
    Attr.lpSecurityDescriptor = NULL;
    Attr.bInheritHandle = true;

    // initialize process creation flags
    UINT32 CreateFlags = NORMAL_PRIORITY_CLASS;
    CreateFlags |= CREATE_NEW_CONSOLE;

    // initialize window flags
    UINT32 dwFlags = 0;
    UINT16 ShowWindowFlags = SW_HIDE;

    if (PipeWriteEnd != nullptr || PipeReadEnd != nullptr)
    {
        dwFlags |= STARTF_USESTDHANDLES;
    }

    // initialize startup info
    STARTUPINFOA StartupInfo = {
        sizeof(STARTUPINFO),
        NULL, NULL, NULL,
        (::DWORD)CW_USEDEFAULT,
        (::DWORD)CW_USEDEFAULT,
        (::DWORD)CW_USEDEFAULT,
        (::DWORD)CW_USEDEFAULT,
        (::DWORD)0, (::DWORD)0, (::DWORD)0,
        (::DWORD)dwFlags,
        ShowWindowFlags,
        0, NULL,
        HANDLE(PipeReadEnd),
        HANDLE(nullptr),
        HANDLE(nullptr)
    };

    LPSTR ffmpegURL = "\"PATHTOFFMPEGEXE\" -y -loglevel verbose -f rawvideo -vcodec rawvideo -framerate 1 -video_size 80x80 -pixel_format rgb24 -i - -vcodec mjpeg -framerate 1/4 -an \"OUTPUTDIRECTORY\"";

    // Finally create the process
    PROCESS_INFORMATION ProcInfo;
    if (!CreateProcessA(NULL, ffmpegURL, &Attr, &Attr, true, (::DWORD)CreateFlags, NULL, NULL, &StartupInfo, &ProcInfo))
    {
        cout << "CreateProcess failed " << ::GetLastError() << endl;
    }
    //CloseHandle(ProcInfo.hThread);


        // Create images and write to pipe
#define MYARRAYSIZE (PIXEL*PIXEL*3) // each pixel has 3 bytes

    UINT8* Bitmap = new UINT8[MYARRAYSIZE];

    for (INT32 outerLoopIndex = 9; outerLoopIndex >= 0; --outerLoopIndex)   // frame loop
    {
        for (INT32 innerLoopIndex = MYARRAYSIZE - 1; innerLoopIndex >= 0; --innerLoopIndex) // create the pixels for each frame
        {
            Bitmap[innerLoopIndex] = (UINT8)(outerLoopIndex * 20); // some gray color
        }
        system("pause");
        if (!WritePipe(PipeWriteEnd, Bitmap, MYARRAYSIZE))
        {
            cout << "Failed writing to pipe" << endl;
        }
    }

    // Done sending images. Tell the other process. IS THIS NEEDED? HOW TO TELL FFmpeg WE ARE DONE?
    //UINT8 endOfFile = 0xFF; // EOF = -1 == 1111 1111 for uint8
    //if (!WritePipe(PipeWriteEnd, &endOfFile, 1))
    //{
    //  cout << "Failed writing to pipe" << endl;
    //}
    //FlushFileBuffers(PipeReadEnd); // Do we need this?

    delete Bitmap;


    system("pause");

    // clean stuff up
    FlushFileBuffers(PipeWriteEnd); // Do we need this?

    if (PipeWriteEnd != NULL && PipeWriteEnd != INVALID_HANDLE_VALUE)
    {
        CloseHandle(PipeWriteEnd);
    }

    // We do not want to destroy the read end of the pipe? Should not as that belongs to FFmpeg
    //if (PipeReadEnd != NULL && PipeReadEnd != INVALID_HANDLE_VALUE)
    //{
    //  ::CloseHandle(PipeReadEnd);
    //}


    return 0;

}

And here the output of FFmpeg

ffmpeg version 3.4.1 Copyright (c) 2000-2017 the FFmpeg developers
  built with gcc 7.2.0 (GCC)
  configuration: --enable-gpl --enable-version3 --enable-sdl2 --enable-bzlib --enable-fontconfig --enable-gnutls --enable-iconv --enable-libass --enable-libbluray --enable-libfreetype --enable-libmp3lame --enable-libopenjpeg --enable-libopus --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libtheora --enable-libtwolame --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libzimg --enable-lzma --enable-zlib --enable-gmp --enable-libvidstab --enable-libvorbis --enable-cuda --enable-cuvid --enable-d3d11va --enable-nvenc --enable-dxva2 --enable-avisynth --enable-libmfx
  libavutil      55. 78.100 / 55. 78.100
  libavcodec     57.107.100 / 57.107.100
  libavformat    57. 83.100 / 57. 83.100
  libavdevice    57. 10.100 / 57. 10.100
  libavfilter     6.107.100 /  6.107.100
  libswscale      4.  8.100 /  4.  8.100
  libswresample   2.  9.100 /  2.  9.100
  libpostproc    54.  7.100 / 54.  7.100
[rawvideo @ 00000221ff992120] max_analyze_duration 5000000 reached at 5000000 microseconds st:0
Input #0, rawvideo, from 'pipe:':
  Duration: N/A, start: 0.000000, bitrate: 153 kb/s
    Stream #0:0: Video: rawvideo, 1 reference frame (RGB[24] / 0x18424752), rgb24, 80x80, 153 kb/s, 1 fps, 1 tbr, 1 tbn, 1 tbc
Stream mapping:
  Stream #0:0 -> #0:0 (rawvideo (native) -> mjpeg (native))
[graph 0 input from stream 0:0 @ 00000221ff999c20] w:80 h:80 pixfmt:rgb24 tb:1/1 fr:1/1 sar:0/1 sws_param:flags=2
[auto_scaler_0 @ 00000221ffa071a0] w:iw h:ih flags:'bicubic' interl:0
[format @ 00000221ffa04e20] auto-inserting filter 'auto_scaler_0' between the filter 'Parsed_null_0' and the filter 'format'
[swscaler @ 00000221ffa0a780] deprecated pixel format used, make sure you did set range correctly
[auto_scaler_0 @ 00000221ffa071a0] w:80 h:80 fmt:rgb24 sar:0/1 -> w:80 h:80 fmt:yuvj444p sar:0/1 flags:0x4
Output #0, mp4, to 'c:/users/vr3/Documents/Guenni/sometest.mp4':
  Metadata:
    encoder         : Lavf57.83.100
    Stream #0:0: Video: mjpeg, 1 reference frame (mp4v / 0x7634706D), yuvj444p(pc), 80x80, q=2-31, 200 kb/s, 1 fps, 16384 tbn, 1 tbc
    Metadata:
      encoder         : Lavc57.107.100 mjpeg
    Side data:
      cpb: bitrate max/min/avg: 0/0/200000 buffer size: 0 vbv_delay: -1
frame=   10 fps=6.3 q=1.6 size=       0kB time=00:00:09.00 bitrate=   0.0kbits/s speed=5.63x

As you can see in the last line of te FFmpeg output, the images got trough. 10 frames are available. But after closing the pipe, FFmpeg does not close, still expecting input.

As the linked examples show, this should be a valid method.

Trying for a week now...

Upvotes: 4

Views: 3956

Answers (2)

d7samurai
d7samurai

Reputation: 3216

When you create your pipe it produces two handles, one for reading and one for writing:

HANDLE readPipe;
HANDLE writePipe;

SECURITY_ATTRIBUTES sa  = { sizeof(sa) };
sa.lpSecurityDescriptor = nullptr;
sa.bInheritHandle       = TRUE;

CreatePipe(&readPipe, &writePipe, &sa, sizeof(buffer));

Notice the bInheritHandle = TRUE flag. This allows the child process that you spawn (ffmpeg) to inherit the handles, which is necessary for it to access your pipe (there's a similar parameter in the CreateProcess function signature below, bInheritHandles, also set to TRUE).

You then start up the child process and map its stdin to readPipe:

STARTUPINFOA si = { sizeof(si) };
si.dwFlags      = STARTF_USESTDHANDLES;
si.hStdInput    = readPipe;
si.hStdOutput   = GetStdHandle(STD_OUTPUT_HANDLE);
si.hStdError    = GetStdHandle(STD_ERROR_HANDLE);

PROCESS_INFORMATION pi = {};

CreateProcessA(nullptr, cmd, nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi);

At this point you can begin pumping your frames to ffmpeg via WriteFile, which (if told to consume input from stdin) will encode away. The problem is that when you are done and call..

CloseHandle(readPipe);
CloseHandle(writePipe);

..ffmpeg keeps waiting for more data instead of closing the file.

Here's why:

When you create the child process, it will inherit all your handles, not just the one you specified in the STARTUPINFO struct (i.e. readPipe). So it will also inherit the writePipe handle. For reads from readPipe to produce an EOF, signalling ffmpeg to finish and close, writePipe has to be closed first. But since the child process has a copy of both handles, even closing the pair at your end won't trigger an EOF on the client because writePipe is still kept alive there.

The fix is to prevent writePipe from being inherited. Like so (before spawning the child process):

SetHandleInformation(writePipe, HANDLE_FLAG_INHERIT, 0);

Here's a complete working source code example (outputs a short test movie):

#include <windows.h>

char* cmd = "ffmpeg -f rawvideo -pix_fmt rgba -video_size 1920x1080 -r 30 -i pipe:0 -preset fast -y -pix_fmt yuv420p output.mp4";
int   buffer[1920 * 1080];

void main()
{
    HANDLE readPipe;
    HANDLE writePipe;

    SECURITY_ATTRIBUTES sa  = { sizeof(sa) };
    sa.lpSecurityDescriptor = nullptr;
    sa.bInheritHandle       = TRUE;

    CreatePipe(&readPipe, &writePipe, &sa, sizeof(buffer));

    SetHandleInformation(writePipe, HANDLE_FLAG_INHERIT, 0);

    STARTUPINFOA si = { sizeof(si) };
    si.dwFlags      = STARTF_USESTDHANDLES;
    si.hStdInput    = readPipe;
    si.hStdOutput   = GetStdHandle(STD_OUTPUT_HANDLE);
    si.hStdError    = GetStdHandle(STD_ERROR_HANDLE);

    PROCESS_INFORMATION pi = {};

    CreateProcessA(nullptr, cmd, nullptr, nullptr, TRUE, 0, nullptr, nullptr, &si, &pi);

    for (int frame = 0; frame < 1080; frame++)
    {
        for (int x = 0; x < 1920; x++)
        {
            buffer[frame * 1920 + x] = 0xffffff00 | (frame * 255 / 1079);
        }

        DWORD bytesWritten;
        WriteFile(writePipe, buffer, sizeof(buffer), &bytesWritten, nullptr);
    }

    CloseHandle(readPipe);
    CloseHandle(writePipe);
}

Upvotes: 11

Rumble
Rumble

Reputation: 826

I finally gave up on it. No idea whats the issue. Switched to use sockets and TCP instead. Below the commandline for TCP connection.

LPSTR ffmpegURL = "\"PATHTOFFMPEGEXE\" -y -loglevel verbose -f rawvideo -vcodec rawvideo -framerate 1 -video_size 80x80 -pixel_format rgb24 -i tcp://127.0.0.1:PORT_NR -vcodec mjpeg -framerate 1/4 -an \"OUTPUTDIRECTORY\"";

In this case FFmpeg is the client and tries to connect to a server.

Upvotes: 1

Related Questions