ffmpeglibavlibavcodeclibavformat

FFMPEG- H.264 encoding BGR image data to YUP420P video file resulting in empty video


I'm new to FFMPEG and trying to use it to do some screen capture to a video file, but after a lot of online searching I am stumped as to what I'm doing wrong. Basically, I've already done the effort of capturing screen data via DirectX which stores in a BGR pixel format and I'm just trying to put each frame in a video file. There's two functions, setup which does all the ffmpeg initialization work, and addImage which is called in the main program loop and puts each buffer of BGR image data into a video file. The technique I'm doing for this is to make two frames, one with the BGR data and one with YUP420P (doesn't need to be the latter but after a lot of trial and error it was all I was able to get working with H.264), and use sws_scale to copy data between the two, and then send that frame to video.mp4. The file seems to be having data written to it successfully (the file size grows and grows as the program runs), but when I try and view it in VLC I see nothing- indeed, VLC fails to fetch a length of the video, and bringing up codec and media information both are empty. I turned on ffmpeg verbose logging but all that is spit out is the following:

Setting default whitelist 'Epu��'
Timestamps are unset in a packet for stream -1259342440. This is deprecated and will stop working in the future. Fix your code to set the timestamps properly
Encoder did not produce proper pts, making some up.

From what I am reading, I understand this to be warnings rather than errors that would totally corrupt my video file. I separately went through all the error codes being spit out and everything seems nominal to me (zero for success for most calls, -11 sometimes for avcodec_receive_packet but the docs indicate that's expected sometimes).

Based on my understanding of things as they are, this should be working, but isn't, and the logs and error codes give me nothing to go on, so someone with experience with this I reckon would save me a ton of time. The code is as follows:

VideoService.h

#ifndef VIDEO_SERVICE_H
#define VIDEO_SERVICE_H

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}

class VideoService {
    public:
        void setup();
        void addImage(unsigned char* data, int lineSize, int width, int height, int align);
    private:
        AVCodecContext* context;
        AVFormatContext* formatContext;
        AVFrame* bgrFrame;
        AVFrame* yuvFrame;
        AVStream* videoStream;
        SwsContext* swsContext;
};

#endif

VideoService.cpp

#include "VideoService.h"
#include <stdio.h>

void FfmpegLogCallback(void *ptr, int level, const char *fmt, va_list vargs)
{
    FILE* f = fopen("ffmpeg.txt", "a");
    fprintf(f, fmt, vargs);
    fclose(f);
}

void VideoService::setup() {
    int result = 0;
    av_log_set_level(AV_LOG_VERBOSE);
    av_log_set_callback(FfmpegLogCallback);
    bgrFrame = av_frame_alloc();
    bgrFrame->width = 1920;
    bgrFrame->height = 1080;
    bgrFrame->format = AV_PIX_FMT_BGRA;
    bgrFrame->time_base.num = 1;
    bgrFrame->time_base.den = 60;
    result = av_frame_get_buffer(bgrFrame, 1);
    yuvFrame = av_frame_alloc();
    yuvFrame->width = 1920;
    yuvFrame->height = 1080;
    yuvFrame->format = AV_PIX_FMT_YUV420P;
    yuvFrame->time_base.num = 1;
    yuvFrame->time_base.den = 60;
    result = av_frame_get_buffer(yuvFrame, 1);
    const AVOutputFormat* outputFormat = av_guess_format("mp4", "video.mp4", "video/mp4");
    result = avformat_alloc_output_context2(
        &formatContext,
        outputFormat,
        "mp4",
        "video.mp4"
    );
    formatContext->oformat = outputFormat;
    const AVCodec* codec = avcodec_find_encoder(AVCodecID::AV_CODEC_ID_H264);
    result = avio_open2(&formatContext->pb, "video.mp4", AVIO_FLAG_WRITE, NULL, NULL);
    videoStream = avformat_new_stream(formatContext, codec);
    AVCodecParameters* codecParameters = videoStream->codecpar;
    codecParameters->codec_type = AVMediaType::AVMEDIA_TYPE_VIDEO;
    codecParameters->codec_id = AVCodecID::AV_CODEC_ID_HEVC;
    codecParameters->width = 1920;
    codecParameters->height = 1080;
    codecParameters->format = AVPixelFormat::AV_PIX_FMT_YUV420P;
    videoStream->time_base.num = 1;
    videoStream->time_base.den = 60;
    result = avformat_write_header(formatContext, NULL);
    
    codec = avcodec_find_encoder(videoStream->codecpar->codec_id);
    context = avcodec_alloc_context3(codec);
    context->time_base.num = 1;
    context->time_base.den = 60;
    avcodec_parameters_to_context(context, videoStream->codecpar);
    result = avcodec_open2(context, codec, nullptr);
    swsContext = sws_getContext(1920, 1080, AV_PIX_FMT_BGRA, 1920, 1080, AV_PIX_FMT_YUV420P, 0, 0, 0, 0);
}

void VideoService::addImage(unsigned char* data, int lineSize, int width, int height, int align) {
    int result = 0;
    result = av_image_fill_arrays(bgrFrame->data, bgrFrame->linesize, data, AV_PIX_FMT_BGRA, 1920, 1080, 1);
    sws_scale(swsContext, bgrFrame->data, bgrFrame->linesize, 0, 1080, &yuvFrame->data[0], yuvFrame->linesize); 
    result = avcodec_send_frame(context, yuvFrame);
    AVPacket *packet = av_packet_alloc();
    result = avcodec_receive_packet(context, packet);
    if (result != 0) {
        return;
    }
    result = av_interleaved_write_frame(formatContext, packet);
}

My environment is windows 10, I'm building with clang++ 12.0.1, and using the FFMPEG 5.1 libs.


Solution

  • See the official sample, muxing.c. Fix you code like the following.

    1. Set fields of an AVCodecContext and call the avcodec_parameters_from_context(), instead of calling the avcodec_parameters_to_context(). You should set width, height, bit_rate, pix_fmt, framerate and time_base at least.(See the implementation of the add_stream() in the sample.)

    2. Specify an algorithm such as the SWS_BILINEAR when calling the sws_getContext().(Although a default algorithm will be selected, that's an undocumented feature.)

    3. Set the pts(presentation timestamp) field of an AVFrame.

    4. Implement a loop calling the avcodec_receive_packet() after calling the avcodec_send_frame(). See the write_frame() in the sample.(Single frame can result in multiple packets.)