ffmpeg视频编码原理和实战-(5)对编码过程进行封装并解决丢帧问题

时间:2024-06-13 13:36:55

头文件:

xencode.h

#pragma once
#include <mutex>
#include<vector>
struct AVCodecContext;
struct AVPacket;
struct AVFrame;
class XEncode
{
public:
    //
    /// 创建编码上下文
    /// @para codec_id 编码器ID号,对应ffmpeg
    /// @return 编码上下文 ,失败返回nullptr
    static AVCodecContext* Create(int codec_id);

    //
    /// 设置对象的编码器上下文 上下文传递到对象中,资源由XEncode维护
    /// 加锁 线程安全
    /// @para c 编码器上下文 如果c_不为nullptr,则先清理资源
    void set_c(AVCodecContext* c);

    /
    /// 设置编码参数,线程安全
    bool SetOpt(const char* key, const char* val);
    bool SetOpt(const char* key, int val);

    //
    /// 打开编码器 线程安全
    bool Open();

    //
    /// 编码数据 线程安全 每次新创建AVPacket
    /// @para frame 空间由用户维护
    /// @return 失败范围nullptr 返回的AVPacket用户需要通过av_packet_free 清理
    AVPacket* Encode(const AVFrame* frame);

    ///
    //根据AVCodecContext 创建一个AVFrame,需要调用者释放av_frame_free
    AVFrame* CreateFrame();
    


    //返回所有编码缓存中的AVPacket,解决丢帧问题
    std::vector<AVPacket*> End();

private:
    AVCodecContext* c_ = nullptr;  //编码器上下文
    std::mutex mux_;               //编码器上下文锁
};

源文件:

xencode.cpp


#include "xencode.h"
#include <iostream>
using namespace std;
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
}
//预处理指令导入库
#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"avutil.lib")

static void PrintErr(int err)
{
    char buf[1024] = { 0 };
    av_strerror(err, buf, sizeof(buf) - 1);
    cerr << buf << endl;
}

//
/// 创建编码上下文
/// @para codec_id 编码器ID号,对应ffmpeg
/// @return 编码上下文 ,失败返回nullptr
AVCodecContext* XEncode::Create(int codec_id)
{
    //1 找到编码器
    auto codec = avcodec_find_encoder((AVCodecID)codec_id);
    if (!codec)
    {
        cerr << "avcodec_find_encoder failed!" << codec_id << endl;
        return nullptr;
    }
    //创建上下文
    auto c = avcodec_alloc_context3(codec);
    if (!c)
    {
        cerr << "avcodec_alloc_context3 failed!" << codec_id << endl;
        return nullptr;
    }
    //设置参数默认值
    c->time_base = { 1,25 };
    c->pix_fmt = AV_PIX_FMT_YUV420P;
    c->thread_count = 16;
    return c;
}

//
/// 设置对象的编码器上下文 上下文传递到对象中,资源由XEncode维护
/// 加锁 线程安全
/// @para c 编码器上下文 如果c_不为nullptr,则先清理资源
void XEncode::set_c(AVCodecContext* c)
{
    unique_lock<mutex>lock(mux_);
    if (c_)
    {
        avcodec_free_context(&c_);
    }
    this->c_ = c;
}


bool XEncode::SetOpt(const char* key, const char* val)
{
    unique_lock<mutex>lock(mux_);
    if (!c_)return false;
    auto re = av_opt_set(c_->priv_data, key, val, 0);
    if (re != 0)
    {
        cerr << "av_opt_set failed!";
        PrintErr(re);
    }
    return true;
}

bool XEncode::SetOpt(const char* key, int val)
{
    unique_lock<mutex>lock(mux_);
    if (!c_)return false;
    auto re = av_opt_set_int(c_->priv_data, key, val, 0);
    if (re != 0)
    {
        cerr << "av_opt_set failed!";
        PrintErr(re);
    }
    return true;
}


//
/// 打开编码器 线程安全
bool XEncode::Open()
{
    unique_lock<mutex>lock(mux_);
    if (!c_)return false;
    auto re = avcodec_open2(c_, NULL, NULL);
    if (re != 0)
    {
        PrintErr(re);
        return false;
    }
    return true;
}
//
/// 编码数据 线程安全 每次新创建AVPacket
/// @para frame 空间由用户维护
/// @return 失败范围nullptr 返回的AVPacket用户需要通过av_packet_free 清理
AVPacket* XEncode::Encode(const AVFrame* frame)
{
    if (!frame)return nullptr;
    unique_lock<mutex>lock(mux_);
    if (!c_)return nullptr;
    //发送到编码线程
    auto re = avcodec_send_frame(c_, frame);
    if (re != 0)return nullptr;
    auto pkt = av_packet_alloc();
    //接收编码线程数据
    re = avcodec_receive_packet(c_, pkt);
    if (re == 0)
    {
        return pkt;
    }
    av_packet_free(&pkt);
    if (re == AVERROR(EAGAIN) || re == AVERROR_EOF)
    {
        return nullptr;
    }
    if (re < 0)
    {
        PrintErr(re);
    }
    return nullptr;
}


//返回所有编码缓存中的AVPacket,解决丢帧问题
std::vector<AVPacket*> XEncode::End()
{
    std::vector<AVPacket*> res;//类似一个数组,存的是AVPacket 对象指针
    unique_lock<mutex>lock(mux_);
    if (!c_)return res;
    auto re = avcodec_send_frame(c_, NULL);//发送NULL到编码器中,获取编码器中的缓存区
    if (re != 0)return res;
    while (re >= 0)
    {
        auto pkt = av_packet_alloc();
        re = avcodec_receive_packet(c_,pkt);//avpacket从编码器c_中读取编码数据。
        if (re != 0)
        {
            av_packet_free(&pkt);
            break;
        }
        res.push_back(pkt);
    }
    return res;

}


///
//根据AVCodecContext 创建一个AVFrame,需要调用者释放av_frame_free
AVFrame* XEncode::CreateFrame()
{
    unique_lock<mutex>lock(mux_);
    if (!c_)return nullptr;
    auto frame = av_frame_alloc();
    frame->width = c_->width;
    frame->height = c_->height;
    frame->format = c_->pix_fmt;
    auto re = av_frame_get_buffer(frame, 0);
    if (re != 0)
    {
        av_frame_free(&frame);
        PrintErr(re);
        return nullptr;
    }
    return frame;
}

main.cpp


#include <iostream>
#include <fstream>
#include "xencode.h"
using namespace std;
extern "C" { //指定函数是c语言函数,函数名不包含重载标注
//引用ffmpeg头文件
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
}


int main(int argc, char* argv[])
{
    string filename = "400_300_25_preset";
    AVCodecID codec_id = AV_CODEC_ID_H264;
    if (argc > 1)
    {
        string codec = argv[1];
        if (codec == "h265" || codec == "hevc")
        {
            codec_id = AV_CODEC_ID_HEVC;
        }
    }
    if (codec_id == AV_CODEC_ID_H264)
    {
        filename += ".h264";
    }
    else if (codec_id == AV_CODEC_ID_HEVC)
    {
        filename += ".h265";
    }
    //输出文件
    ofstream ofs;
    ofs.open(filename, ios::binary);

    XEncode en;
    auto c = en.Create(codec_id);
    c->width = 400;
    c->height = 300;
    en.set_c(c);
    en.SetOpt("crf", 18); //恒定速率因子(CRF)
    en.Open();
    auto frame = en.CreateFrame();
    int count = 0;//写入文件的帧数,看是否有500,SPS PPS IDR放在一帧中
    for (int i = 0; i < 500; i++)
    {
        //生成AVFrame 数据 每帧数据不同
        //Y
        for (int y = 0; y < c->height; y++)
        {
            for (int x = 0; x < c->width; x++)
            {
                frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
            }
        }
        //UV
        for (int y = 0; y < c->height / 2; y++)
        {
            for (int x = 0; x < c->width / 2; x++)
            {
                frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
                frame->data[2][y * frame->linesize[2] + x] = 64 + x + i * 5;
            }
        }
        frame->pts = i;//显示的时间
        auto pkt = en.Encode(frame);
        if (pkt)
        {
            count++;
            ofs.write((char*)pkt->data, pkt->size);
            av_packet_free(&pkt);
        }
    }
   auto missing_pkt =  en.End();
   for (auto pkt : missing_pkt)
   {
       count++;
       ofs.write((char*)pkt->data, pkt->size);
       av_packet_free(&pkt);
   }
    ofs.close();
    en.set_c(nullptr);
    cout << "encode" << count << endl;

    getchar();
    return 0;
}

讨论:

1.上一篇文章提到的丢帧问题得到了解决,原因是编码器缓存不够

2.封装使得代码更加简洁