C++ FFmpeg API を使用して動画を読み込み、フレームごとにOPENCVで処理して、動画で書き出しする

## C++でFFmpegのlibやdllを使いたい！
`_popen`で`ffmpeg.exe`を使っていましたが、Windowsは`_popen`を正式サポートしてないためか処理速度がでませんでした。
`_popen`の代わりになるものが、`CreateProcess` らしいですが、どこかでLinuxに比べてWindowsは遅いというような記載を見かけました。

https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/popen-wpopen?view=msvc-170

https://learn.microsoft.com/ja-jp/windows/win32/ProcThread/creating-a-child-process-with-redirected-input-and-output

なら、C++で直接ffmpegのlibやdllを操作するしかない！

ffmpegをC++で利用している記事はあまり見つからないのですが、幸いQiitaでこちらの記事を見つけました。

https://qiita.com/dugnutt/items/71c1b32cd574d93d972a

こちらの記事のコードを少し直して、使わせて頂きました。

## 環境
- Windows11pro 64bit 23H2
- VisualStudio2022 VC++
- OPENCV 4.10
- FFmpeg 7.1 LGPL
`ffmpeg-windows-build-helpers` で`--build-ffmpeg-shared=y --enable-gpl=n --build-intel-qsv=y`でビルドしました。
コマンドリストで使えるオプションを確認しながら使うのがよさそうです。

https://github.com/rdp/ffmpeg-windows-build-helpers/wiki/Command-Line-Options


## コード

https://github.com/calocenrieti/cpp_ffmpeg_opencv

```cpp:ffmpeg_opencv.cpp
#include <iostream>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/imgutils.h>
#include <libavutil/opt.h>
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
}
#include <opencv2/opencv.hpp>

void decode_and_encode_video(const char* output, const char* input);

void main() {
    const char* input = "test_sdr.mp4";
    const char* output = "output.mp4";
    decode_and_encode_video(output, input);
}


void decode_and_encode_video(const char* output, const char* input) {
    AVFormatContext* inputFmtContxt = NULL;
    AVFormatContext* outputFmtContxt = NULL;
    const AVCodec* encoder = NULL;
    const AVCodec* decoder = NULL;
    AVCodecContext* encoderContxt = NULL;
    AVCodecContext* decoderContxt = NULL;
    int ret = 0, video_stream_index = 0;
    ret = avformat_open_input(&inputFmtContxt, input, NULL, NULL);
    if (ret < 0) {
        std::cout << "Could not open input video" << std::endl;
    }
    ret = avformat_find_stream_info(inputFmtContxt, NULL);
    if (ret < 0) {
        std::cout << "Could not find the stream info" << std::endl;
    }
    const AVOutputFormat* outFmt = av_guess_format("mp4", NULL, NULL);
    avformat_alloc_output_context2(&outputFmtContxt, outFmt, NULL, NULL);
    //デコーダーとエンコーダーを設定
    for (int i = 0; i < (int)inputFmtContxt->nb_streams; ++i) {

        AVStream* in_stream = inputFmtContxt->streams[i];
        AVCodecParameters* in_par = in_stream->codecpar;
        AVStream* out_stream = avformat_new_stream(outputFmtContxt, NULL);
        if (in_par->codec_type == AVMEDIA_TYPE_VIDEO) {
            video_stream_index = i;
            //decoder = avcodec_find_decoder(in_par->codec_id);
            const AVCodec* decoder = avcodec_find_decoder_by_name("hevc_cuvid");
            if (!decoder) {
                fprintf(stderr, "Decoder not found\n");
                return;
            }
            decoderContxt = avcodec_alloc_context3(decoder);
            AVCodecContext* codec_ctx = avcodec_alloc_context3(NULL);
            avcodec_parameters_to_context(decoderContxt, in_par);
            decoderContxt->framerate = in_stream->r_frame_rate;
            decoderContxt->time_base = in_stream->time_base;
            ret=avcodec_open2(decoderContxt, decoder, NULL);
            if (ret < 0) {
                fprintf(stderr, "Could not open codec\n");
                return ;
            }
            //encoder = avcodec_find_encoder(in_par->codec_id);
            encoder = avcodec_find_encoder_by_name("hevc_nvenc");
            encoderContxt = avcodec_alloc_context3(encoder);
            encoderContxt->height = decoderContxt->height;
            encoderContxt->width = decoderContxt->width;
            //encoderContxt->pix_fmt = decoderContxt->pix_fmt;
            encoderContxt->pix_fmt = AV_PIX_FMT_YUV420P;
            //encoderContxt->qmax = 31;
            //encoderContxt->qmin = 2;
            //encoderContxt->qcompress = 0.6;
            //encoderContxt->max_qdiff = 4;
            //encoderContxt->gop_size = 250;
            //encoderContxt->keyint_min = 25;
            //encoderContxt->max_b_frames = 16;
            //encoderContxt->refs = 6;
            encoderContxt->framerate = in_stream->r_frame_rate;
            encoderContxt->time_base = in_stream->time_base;
            encoderContxt->bit_rate = decoderContxt->bit_rate;
            encoderContxt->flags = AV_CODEC_FLAG_GLOBAL_HEADER;
            av_opt_set(encoderContxt->priv_data, "preset", "fast", 0);
            //av_opt_set(encoderContxt->priv_data, "tune", "zerolatency", 0);
            avcodec_open2(encoderContxt, encoder, NULL);
            out_stream->time_base = encoderContxt->time_base;
            avcodec_parameters_from_context(out_stream->codecpar, encoderContxt);
        }
        else {
            ret = avcodec_parameters_copy(out_stream->codecpar, in_par);
        }
    }
    //出力ファイルを準備
    av_dump_format(outputFmtContxt, 0, output, 1);
    avio_open(&outputFmtContxt->pb, output, AVIO_FLAG_WRITE);
    ret = avformat_write_header(outputFmtContxt, NULL);
    //YUV と RGB 間の変換用
    enum AVPixelFormat bgr_pix_fmt = AV_PIX_FMT_BGR24;
    int HEIGHT = decoderContxt->height;
    int WIDTH = decoderContxt->width;
    SwsContext* yuv2bgr = sws_getContext(WIDTH, HEIGHT, decoderContxt->pix_fmt,
        WIDTH, HEIGHT, bgr_pix_fmt, SWS_BICUBIC, NULL, NULL, NULL);
    SwsContext* bgr2yuv = sws_getContext(WIDTH, HEIGHT, bgr_pix_fmt,
        WIDTH, HEIGHT, encoderContxt->pix_fmt, SWS_BICUBIC, NULL, NULL, NULL);
    //パケットとフレームの準備
    int res = 0;
    AVPacket* packet = av_packet_alloc();
    AVPacket* out_packet = av_packet_alloc();
    out_packet->data = NULL;
    out_packet->size = 0;
    // デコーダーから受け取るフレーム
    AVFrame* frame = av_frame_alloc();
    // RGBへの変換先のフレーム
    AVFrame* bgrframe = av_frame_alloc();
    bgrframe->width = decoderContxt->width;
    bgrframe->height = decoderContxt->height;
    bgrframe->format = bgr_pix_fmt;
    ret = av_frame_get_buffer(bgrframe, 0);
    if (ret < 0) {
		fprintf(stderr, "Could not allocate the video frame data\n");
		return;
	}

    //uint8_t* buf = (uint8_t*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_BGR24, decoderContxt->width, decoderContxt->height, 1));
    ////ret = av_image_fill_arrays(bgrframe->data, bgrframe->linesize, buf, AV_PIX_FMT_BGR24, decoderContxt->width, decoderContxt->height, 1);
    //ret = av_image_fill_arrays(bgrframe->data, bgrframe->linesize, NULL, bgr_pix_fmt, bgrframe->width, bgrframe->height, 1);
    //if (ret < 0) {
    //    fprintf(stderr, "Could not fill arrays for bgrframe\n");
    //    return;
    //}
    // エンコーダ―に渡すフレーム
    AVFrame* outframe = av_frame_alloc();
    outframe->width = decoderContxt->width;
    outframe->height = decoderContxt->height;
    //outframe->format = decoderContxt->pix_fmt;
    outframe->format = AV_PIX_FMT_YUV420P;
    ret = av_frame_get_buffer(outframe, 0);
    if (ret < 0) {
        fprintf(stderr, "Could not allocate the video frame data\n");
        return;
    }
    //uint8_t* outbuf = (uint8_t*)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_YUV420P, decoderContxt->width, decoderContxt->height, 1));
    //ret = av_image_fill_arrays(outframe->data, outframe->linesize, outbuf, AV_PIX_FMT_YUV420P, decoderContxt->width, decoderContxt->height, 1);
    //ret = av_image_fill_arrays(outframe->data, outframe->linesize, NULL, AV_PIX_FMT_YUV420P, outframe->width, outframe->height, 1);
    //if (ret < 0) {
    //    fprintf(stderr, "Could not fill arrays for outframe\n");
    //    return;
    //}
    //デコードとエンコードを開始
    while (true) {
        ret = av_read_frame(inputFmtContxt, packet);
        if (ret < 0) {
            break;
        }
        AVStream* input_stream = inputFmtContxt->streams[packet->stream_index];
        AVStream* output_stream = outputFmtContxt->streams[packet->stream_index];
        if (input_stream->codecpar->codec_type == video_stream_index) {
            res = avcodec_send_packet(decoderContxt, packet);
            while (res >= 0) {
                res = avcodec_receive_frame(decoderContxt, frame);
                if (res == AVERROR(EAGAIN) || res == AVERROR_EOF) {
                    break;
                }
                if (res >= 0) {
                    outframe->pict_type = frame->pict_type;
                    outframe->pts = frame->pts;
                    outframe->pkt_dts = frame->pkt_dts;
                    outframe->duration = frame->duration;
                    ret = av_frame_make_writable(outframe);
                    //YUVフレームをRGBフレームに変換
                    sws_scale(yuv2bgr, frame->data, frame->linesize, 0, frame->height, bgrframe->data, bgrframe->linesize);

                    ////ネガポジ反転
                    //int h = bgrframe->height;
                    //int l = bgrframe->linesize[0];
                    //for (int i = 0; i < h; ++i) {
                    //    for (int j = 0; j < l; ++j) {
                    //        bgrframe->data[0][i * l + j] = 255 - bgrframe->data[0][i * l + j];
                    //    }
                    //}

                    // ネガポジ反転の代わりに左右反転を行う部分
                    cv::Mat bgrMat(bgrframe->height, bgrframe->width, CV_8UC3, bgrframe->data[0], bgrframe->linesize[0]);
                    cv::Mat flippedMat;

                    cv::flip(bgrMat, flippedMat, 1); // 1は左右反転を意味します

                    // flippedMatをbgrframeにコピー
                    memcpy(bgrframe->data[0], flippedMat.data, flippedMat.total()* flippedMat.elemSize());


                    //RGBからYUVに戻す
                    sws_scale(bgr2yuv, bgrframe->data, bgrframe->linesize, 0, bgrframe->height, outframe->data, outframe->linesize);
                    res = avcodec_send_frame(encoderContxt, outframe);
                    //フレームの書き込み
                    while (res >= 0) {
                        res = avcodec_receive_packet(encoderContxt, out_packet);
                        if (res == AVERROR(EAGAIN) || res == AVERROR_EOF) {
                            break;
                        }
                        out_packet->pts = av_rescale_q_rnd(outframe->pts, input_stream->time_base, output_stream->time_base, AV_ROUND_NEAR_INF);
                        out_packet->dts = outframe->pts;
                        out_packet->duration = av_rescale_q(packet->duration, input_stream->time_base, output_stream->time_base);
                        res = av_interleaved_write_frame(outputFmtContxt, out_packet);
                    }
                    av_packet_unref(out_packet);
                }
                av_frame_unref(frame);
            }
            av_packet_unref(packet);
        }
        else {
            //音声データはそのまま書き込み
            packet->pts = av_rescale_q_rnd(packet->pts, input_stream->time_base, output_stream->time_base, AV_ROUND_NEAR_INF);
            packet->dts = av_rescale_q_rnd(packet->dts, input_stream->time_base, output_stream->time_base, AV_ROUND_NEAR_INF);
            packet->duration = av_rescale_q(packet->duration, input_stream->time_base, output_stream->time_base);
            res = av_interleaved_write_frame(outputFmtContxt, packet);
            av_packet_unref(packet);
        }
    }
    //各メモリの解放
    av_packet_free(&packet);
    av_frame_free(&frame);
    avformat_free_context(inputFmtContxt);
    avcodec_free_context(&decoderContxt);
    av_packet_free(&out_packet);
    av_write_trailer(outputFmtContxt);
    avformat_free_context(outputFmtContxt);
    avcodec_free_context(&encoderContxt);
    //av_freep(&buf);
    //av_freep(&outbuf);
    sws_freeContext(yuv2bgr);
    sws_freeContext(bgr2yuv);
}
```

## コードの補足
- デコーダ、エンコーダは、Nvidiaの`hevc_nvenc`、`hevc_cuvid`を指定しています。デコーダ、エンコーダは適宜変更ください
- エンコーダの設定である`encoderContxt-> `はnvencでは規定されていない値が多く、そのままでは
`[hevc_nvenc @ 000002058ca22cc0] No capable devices found `
とffmpegから、デバイスが見つからないといわれるので、コメントアウトしました。
（正確にはそんな設定になっているNVENCがない、というエラー）
- `uint8_t* outbuf = (uint8_t*)av_malloc...`や`av_image_fill_arrays`は不要ということだったのでコメントアウトしています
- OPENCVの処理は暫定で左右反転としています

## 終わりに
C++からFFmpegを利用している事例は少なく、@dugnutt さまの記事にはとても助けられました。ありがとうございます。
ただわたしが素人すぎて、そのままではうまく動かず、かなり試行錯誤したので、動いた例として投稿させていただきます。
まだ本当にやりたいところまでたどり着いていないので、私ももう少しがんばります！