Search code examples
c++videoffmpeglibavlibx264

FFMPEG RTSP stream to MPEG4/H264 file using libx264


Heyo folks,

I'm attempting to transcode/remux an RTSP stream in H264 format into a MPEG4 container, containing just the H264 video stream. Basically, webcam output into a MP4 container.

I can get a poorly coded MP4 produced, using this code:

// Variables here for demo
AVFormatContext * video_file_output_format = nullptr;
AVFormatContext * rtsp_format_context = nullptr;
AVCodecContext * video_file_codec_context = nullptr;
AVCodecContext * rtsp_vidstream_codec_context = nullptr;
AVPacket packet = {0};
AVStream * video_file_stream = nullptr;
AVCodec * rtsp_decoder_codec = nullptr;
int errorNum = 0, video_stream_index = 0;
std::string outputMP4file = "D:\\somemp4file.mp4";

// begin
AVDictionary * opts = nullptr;
av_dict_set(&opts, "rtsp_transport", "tcp", 0);

if ((errorNum = avformat_open_input(&rtsp_format_context, uriANSI.c_str(), NULL, &opts)) < 0) {
    errOut << "Connection failed: avformat_open_input failed with error " << errorNum << ":\r\n" << ErrorRead(errorNum);
    TacticalAbort();
    return;
}

rtsp_format_context->max_analyze_duration = 50000;
if ((errorNum = avformat_find_stream_info(rtsp_format_context, NULL)) < 0) {
    errOut << "Connection failed: avformat_find_stream_info failed with error " << errorNum << ":\r\n" << ErrorRead(errorNum);
    TacticalAbort();
    return;
}

video_stream_index = errorNum = av_find_best_stream(rtsp_format_context, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);

if (video_stream_index < 0) {
    errOut << "Connection in unexpected state; made a connection, but there was no video stream.\r\n"
        "Attempts to find a video stream resulted in error " << errorNum << ": " << ErrorRead(errorNum);
    TacticalAbort();
    return;
}

rtsp_vidstream_codec_context = rtsp_format_context->streams[video_stream_index]->codec;

av_init_packet(&packet);

if (!(video_file_output_format = av_guess_format(NULL, outputMP4file.c_str(),  NULL))) {
    TacticalAbort();
    throw std::exception("av_guess_format");
}

if (!(rtsp_decoder_codec = avcodec_find_decoder(rtsp_vidstream_codec_context->codec_id))) {
    errOut << "Connection failed: connected, but avcodec_find_decoder returned null.\r\n"
        "Couldn't find codec with an AV_CODEC_ID value of " << rtsp_vidstream_codec_context->codec_id << ".";
    TacticalAbort();
    return;
}

video_file_format_context = avformat_alloc_context();
video_file_format_context->oformat = video_file_output_format;

if (strcpy_s(video_file_format_context->filename, sizeof(video_file_format_context->filename), outputMP4file.c_str())) {
    errOut << "Couldn't open video file: strcpy_s failed with error " << errno << ".";
    std::string log = errOut.str();
    TacticalAbort();
    throw std::exception("strcpy_s");
}

if (!(video_file_encoder_codec = avcodec_find_encoder(video_file_output_format->video_codec))) {
    TacticalAbort();
    throw std::exception("avcodec_find_encoder");
}

// MARKER ONE

if (!outputMP4file.empty() &&
    !(video_file_output_format->flags & AVFMT_NOFILE) &&
    (errorNum = avio_open2(&video_file_format_context->pb, outputMP4file.c_str(), AVIO_FLAG_WRITE, nullptr, &opts)) < 0) {
    errOut << "Couldn't open video file \"" << outputMP4file << "\" for writing : avio_open2 failed with error " << errorNum << ": " << ErrorRead(errorNum);
    TacticalAbort();
    return;
}

// Create stream in MP4 file
if (!(video_file_stream = avformat_new_stream(video_file_format_context, video_file_encoder_codec))) {
    TacticalAbort();
    return;
}

AVCodecContext * video_file_codec_context = video_file_stream->codec;

// MARKER TWO

// error -22/-21 in avio_open2 if this is skipped
if ((errorNum = avcodec_copy_context(video_file_codec_context, rtsp_vidstream_codec_context)) != 0) {
    TacticalAbort();
    throw std::exception("avcodec_copy_context");
}

//video_file_codec_context->codec_tag = 0;

/*
// MARKER 3 - is this not needed? Examples suggest not.
if ((errorNum = avcodec_open2(video_file_codec_context, video_file_encoder_codec, &opts)) < 0)
{
    errOut << "Couldn't open video file codec context: avcodec_open2 failed with error " << errorNum << ": " << ErrorRead(errorNum);
    std::string log = errOut.str();
    TacticalAbort();
    throw std::exception("avcodec_open2, video file");
}*/

//video_file_format_context->flags |= AVFMT_FLAG_GENPTS;
if (video_file_format_context->oformat->flags & AVFMT_GLOBALHEADER)
{
    video_file_codec_context->flags |= CODEC_FLAG_GLOBAL_HEADER;
}

if ((errorNum = avformat_write_header(video_file_format_context, &opts)) < 0) {
    errOut << "Couldn't open video file: avformat_write_header failed with error " << errorNum << ":\r\n" << ErrorRead(errorNum);
    std::string log = errOut.str();
    TacticalAbort();
    return;
}

However, there are several issues:

  1. I can't pass any x264 options to the output file. The output H264 matches the input H264's profile/level - switching cameras to a different model switches H264 level.
  2. The timing of the output file is off, noticeably.
  3. The duration of the output file is off, massively. A few seconds of footage becomes hours, although playtime doesn't match. (FWIW, I'm using VLC to play them.)

Passing x264 options

If I manually increment PTS per packet, and set DTS equal to PTS, it plays too fast, ~2-3 seconds' worth of footage in one second playtime, and duration is hours long. The footage also blurs past several seconds, about 10 seconds' footage in a second.

If I let FFMPEG decide (with or without GENPTS flag), the file has a variable frame rate (probably as expected), but it plays the whole file in an instant and has a long duration too (over forty hours for a few seconds). The duration isn't "real", as the file plays in an instant.

At Marker One, I try to set the profile by passing options to avio_open2. The options are simply ignored by libx264. I've tried:

av_dict_set(&opts, "vprofile", "main", 0);
av_dict_set(&opts, "profile", "main", 0); // error, missing '('
// FF_PROFILE_H264_MAIN equals 77, so I also tried
av_dict_set(&opts, "vprofile", "77", 0); 
av_dict_set(&opts, "profile", "77", 0);

It does seem to read the profile setting, but it doesn't use them. At Marker Two, I tried to set it after the avio_open2, before avformat_write_header .

// I tried all 4 av_dict_set from earlier, passing it to avformat_write_header.
// None had any effect, they weren't consumed.
av_opt_set(video_file_codec_context, "profile", "77", 0);
av_opt_set(video_file_codec_context, "profile", "main", 0);
video_file_codec_context->profile = FF_PROFILE_H264_MAIN;
av_opt_set(video_file_codec_context->priv_data, "profile", "77", 0);
av_opt_set(video_file_codec_context->priv_data, "profile", "main", 0);

Messing with privdata made the program unstable, but I was trying anything at that point. I'd like to solve issue 1 with passing settings, since I imagine it'd bottleneck any attempt to solve issues 2 or 3.

I've been fiddling with this for the better part of a month now. I've been through dozens of documentation, Q&As, examples. It doesn't help that quite a few are outdated.

Any help would be appreciated.

Cheers


Solution

  • Okay, firstly, I wasn't using ffmpeg, but a fork of ffmpeg called libav. Not to be confused, ffmpeg is more recent, and libav was used in some distributions of Linux.

    Compiling for Visual Studio

    Once I upgraded to the main branch, I had to compile it manually again, since I was using it in Visual Studio and the only static libraries are G++, so linking doesn't work nicely.

    The official guide is https://trac.ffmpeg.org/wiki/CompilationGuide/MSVC.
    First, compiling as such works fine:
    Ensure VS is in PATH. Your PATH should read in this order:

    C:\Program Files (x86)\Microsoft Visual Studio XX.0\VC\bin
    D:\MinGW\msys64\mingw32\bin  
    D:\MinGW\msys64\usr\bin  
    D:\MinGW\bin
    

    Then run Visual Studio x86 Native Tools prompt. Should be in your Start Menu.
    In the CMD, run
    (your path to MinGW)\msys64\msys2_shell.cmd -full-path
    In the created MinGW window, run:
    $ cd /your dev path/
    $ git clone https://git.ffmpeg.org/ffmpeg.git ffmpeg
    After about five minutes you'll have the FFMPEG source in the subfolder ffmpeg.
    Access the source via:
    $ cd ffmpeg
    Then run:
    $ which link
    if it doesn't provide the VS path from PATH, but usr/link or usr/bin/link, rename similarly:
    $ mv /usr/bin/link.exe /usr/bin/msys-link.exe
    If it does skip the $ mv step.

    Finally, run this command:
    $ ./configure --toolchain=msvc and whatever other cmdlines you want
    (you can see commandlines via ./configure --help)
    It may appear inactive for a long time. Once it's done you'll get a couple pages of output.

    Then run:
    $ make
    $ make install

    Note for static builds (configure with --enable-static), although you get Windows static lib files, it'll produce them with extension *.a files. Just rename to .lib.
    (you can use cmd: ren *.a *.lib)

    Using FFMPEG

    To just copy from FFMPEG RTSP to a file, using source profile, level etc, just use:

    1. read network frame av_read_frame(rtsp_format_context)
    2. pass to MP4 av_write_frame(video_file_format_context)

    You don't need to open a AVCodecContext, a decoder or encoder; just avformat_open_input, and the video file AVFormatContext and AVIOContext.

    If you want to re-encode, you have to:

    1. read network frame
      av_read_frame(rtsp_format_context)
    2. pass packet to decoder
      avcodec_send_packet(rtsp_decoder_context)
    3. read frames from decoder (in loop)
      avcodec_receive_frame(rtsp_decoder_context)
    4. send each decoded frame to encoder
      avcodec_send_frame(video_file_encoder_context)
    5. read packets from encoder (in loop)
      avcodec_receive_packet(video_file_encoder_context)
    6. send each encoded packet to output video av_write_frame(video_file_format_context)

    Some gotchas

    • Copy out the width, height, and pixel format manually. For H264 it's YUV420P.
      As example, for level 3.1, profile high:

        AVCodecParameters * video_file_codec_params = video_file_stream->codecpar;
        video_file_codec_params->profile = FF_PROFILE_H264_HIGH;
        video_file_codec_params->format = AV_PIX_FMT_YUV420P;
        video_file_codec_params->level = 31;
        video_file_codec_params->width = rtsp_vidstream->codecpar->width;
        video_file_codec_params->height = rtsp_vidstream->codecpar->height;
      
    • libx264 accepts a H264 preset via the opts parameter in avcodec_open2. Example for "veryfast" preset:

        AVDictionary * mydict;
        av_dict_set(&mydict, "preset", "veryfast", 0);
        avcodec_open2(video_file_encoder_context, video_file_encoder_codec, &opts)
        // avcodec_open2 returns < 0 for errors.
        // Recognised options will be removed from the mydict variable.
        // If all are recognised, mydict will be NULL.
      
    • Output timing is a volatile thing. Use this before ``.

        video_file_stream->avg_frame_rate = rtsp_vidstream->avg_frame_rate;
        video_file_stream->r_frame_rate = rtsp_vidstream->r_frame_rate;
        video_file_stream->time_base = rtsp_vidstream->time_base;
        video_file_encoder_context->time_base = rtsp_vidstream_codec_context->time_base;
        // Decreasing GOP size for more seek positions doesn't end well.
        // libx264 forces the new GOP size.
        video_file_encoder_context->gop_size = rtsp_vidstream_codec_context->gop_size;
        if ((errorNum = avcodec_open2(video_file_encoder_context,...)) < 0) {
            // an error...
        }
      
    • H264 may write at double-speed to the file, so playback is doubly fast. To change this, go manual with encoded packets' timing:

        packet->pts = packet->dts = frameNum++;
        av_packet_rescale_ts(packet, video_file_encoder_context->time_base, video_file_stream->time_base);
        packet->pts *= 2;
        packet->dts *= 2;
        av_interleaved_write_frame(video_file_format_context, packet)
        // av_interleaved_write_frame returns < 0 for errors.
      

      Note we switch av_write_frame to av_interleaved_write_frame, and set both PTS and DTS. frameNum should be a int64_t, and should start from 0 (although that's not required).

      Also note the av_rescale_ts call's parameters are the video file encoder context, and the video file stream - RTSP isn't involved.

    • VLC media player won't play H.264 streams encoded with an FPS of 4 or lower. So if your RTSP streams are showing the first decoded frame and never progressing, or showing pure green until the video ends, make sure your FPS is high enough. (that's VLC v2.2.4)