FFmpeg实战:录制Windows屏幕并用FFmpeg压缩成H.264/H.265并保存

一、概述

  前面讲过【Qt+Windows录制屏幕并将屏幕录制的BGRA数据保存到本地】。本节就在这一篇的基础上加上【利用ffmpeg的SwsContext将BGRA数据转YUV420P】,并将转换好的YUV420P数据

通过H264/H265编码压缩,然后再存储到本地。

  工具截图: 

  

   功能介绍:

    1.视频格式可以选择要编码视频的编码格式,目前只能选择H264和H265

    2.输入框中代表录制文件的存储路径

    3.下面两个按钮是开始录制和停止录制

    4.录制的宽高为全屏,也就是电脑屏幕的宽高

二、代码示例

  1. 选择编码格式
    void GenerateH26XWindow::onVideoFormatChanged(int pos)
    {
        switch (pos)
        {
        case 0:
            codecId = AV_CODEC_ID_H264;
            break;
        case 1:
            codecId = AV_CODEC_ID_HEVC;
            break;
        }
    }

     

  2. 选择输出文件路径
    void GenerateH26XWindow::choiceOutputFilePathFunc()
    {
        QString filter = "所有文件(*.*)"; //文件过滤器
        QString filePath = QFileDialog::getSaveFileName(this, "选择文件输出目录", "", filter);
        this->outFilePath = filePath;
        this->ui.outputPathEdit->setText(this->outFilePath);
    }

     

  3. 开始录制
    1. 开始录屏
      //开始录屏,开启后可得到屏幕宽高
      TonyCaptureScreen::Get()->outFilePath = filePath;
      TonyCaptureScreen::Get()->Start();

       

    2. 实例化编码器,创建AVCodecContext并设置参数
      int screenWidth = TonyCaptureScreen::Get()->getWidth();
      int screenHeight = TonyCaptureScreen::Get()->getHeight();
      //编码器
      encoder = new YEncoder();
      auto mContext = encoder->Create(codecId);
      mContext->width = screenWidth;
      mContext->height = screenHeight;
      encoder->set_c(mContext);
      encoder->SetOpt("crf", 18); //恒定速率因子(CRF)

       

    3. 打开编码器并初始化像素格式转换上下文
      1.   
        encoder->Open();
        encoder->InitSwsCtx();

          

    4. 初始化文件写入磁盘工具,并开启写入线程
      //文件写入工具
      writer = BinaryFileWriter::Create();
      writer->Init(filePath.toStdString());
      // 创建并启动后台线程
      renderThread = std::make_unique<std::thread>(&GenerateH26XWindow::Save, this);

       

      • 保存方法的代码如下
        void GenerateH26XWindow::Save()
        {
            int count = 0;
            auto frame = encoder->CreateFrame();
            while (isSaveing)
            {
                std::list<char*> mList = TonyCaptureScreen::Get()->getScreenDataList();
                if (!mList.empty())
                {
                    char* data = mList.front();
                    frame->pts = count;
                    encoder->scale((uint8_t*)data, frame);
                    auto pkt = encoder->Encode(frame);
                    if (pkt)
                    {
                        writer->Write((char*)pkt->data, pkt->size);
                        av_packet_free(&pkt);
                    }
                    mList.pop_front();
                    count++;
                }
            }
            //取出编码缓冲区中的数据,也就是最后几帧
            auto pkts = encoder->End();
            for (auto pkt : pkts)
            {
                writer->Write((char*)pkt->data, pkt->size);
                av_packet_free(&pkt);
            }
            encoder->set_c(nullptr);
            if (frame)
            {
                av_frame_free(&frame);
            }
            qDebug() << "数据保存结束";
        
        }
  4. 停止录制
    void GenerateH26XWindow::closeEvent(QCloseEvent* event) {
        event->accept();  // 接受关闭事件(允许关闭)
        if (isClickedRecord) {
            TonyCaptureScreen::Get()->Stop();
            isSaveing = false;
            if (renderThread && renderThread->joinable()) {
                renderThread->join();
            }
            if (writer) {
                writer->Close();
                writer = nullptr;
            }
            if (encoder) {
                encoder->Release();
                delete encoder;
                encoder = nullptr;
            }
    
        }
        qDebug() << "窗口已关闭";
    }

    5.编解码封装工具类

    #include "YEncoder.h"
    #include <QDebug>
    extern "C"
    {
    #include "libavcodec/avcodec.h"
    #include "libswscale/swscale.h"
    #include "libavutil/avutil.h"
    #include "libavutil/opt.h"
    }
    
    /// <summary>
    /// 输出错误日志
    /// </summary>
    /// <param name="err"></param>
    static void PrintErr(int err)
    {
        char buf[1024] = { 0 };
        av_strerror(err, buf, sizeof(buf) - 1);
        qDebug() << buf;
    }
    
    //////////////////////////////////////////
    /// 创建编码上下文
    /// @para codec_id 编码器ID号,对应ffmpeg
    /// @return 编码上下文 ,失败返回nullptr
    AVCodecContext* YEncoder::Create(int codec_id)
    {
        //1 找到编码器
        auto codec = avcodec_find_encoder((AVCodecID)codec_id);
        if (!codec)
        {
            qDebug() << "avcodec_find_encoder failed!" << codec_id << endl;
            return nullptr;
        }
        //创建上下文
        auto c = avcodec_alloc_context3(codec);
        if (!c)
        {
            qDebug() << "avcodec_alloc_context3 failed!" << codec_id << endl;
            return nullptr;
        }
        //设置参数默认值
        c->time_base = { 1,25 };
        c->pix_fmt = AV_PIX_FMT_YUV420P;
        //c->pix_fmt = AV_PIX_FMT_RGBA;
        c->thread_count = 16;
        return c;
    }
    
    //////////////////////////////////////////
    /// 设置对象的编码器上下文 上下文传递到对象中,资源由YEncoder维护
    /// 加锁 线程安全
    /// @para c 编码器上下文 如果mContext不为nullptr,则先清理资源
    void YEncoder::set_c(AVCodecContext* c)
    {
        std::unique_lock<std::mutex>lock(mMutex);
        if (mContext)
        {
            avcodec_free_context(&mContext);
        }
        this->mContext = c;
    }
    
    bool YEncoder::SetOpt(const char* key, const char* val)
    {
        std::unique_lock<std::mutex>lock(mMutex);
        if (!mContext)return false;
        auto re = av_opt_set(mContext->priv_data, key, val, 0);
        if (re != 0)
        {
            qDebug() << "av_opt_set failed!";
            PrintErr(re);
        }
        return true;
    }
    
    bool YEncoder::SetOpt(const char* key, int val)
    {
        std::unique_lock<std::mutex>lock(mMutex);
        if (!mContext)return false;
        auto re = av_opt_set_int(mContext->priv_data, key, val, 0);
        if (re != 0)
        {
            qDebug() << "av_opt_set failed!";
            PrintErr(re);
        }
        return true;
    }
    
    //////////////////////////////////////////////////////////////
    /// 打开编码器 线程安全
    bool YEncoder::Open()
    {
        std::unique_lock<std::mutex>lock(mMutex);
        if (!mContext)return false;
        auto re = avcodec_open2(mContext, NULL, NULL);
        if (re != 0)
        {
            PrintErr(re);
            return false;
        }
        return true;
    }
    
    //////////////////////////////////////////////////////////////
    /// 编码数据 线程安全 每次新创建AVPacket
    /// @para frame 空间由用户维护
    /// @return 失败范围nullptr 返回的AVPacket用户需要通过av_packet_free 清理
    AVPacket* YEncoder::Encode(const AVFrame* frame)
    {
        if (!frame)return nullptr;
        std::unique_lock<std::mutex>lock(mMutex);
        if (!mContext)return nullptr;
    
        //发送到编码线程
        auto re = avcodec_send_frame(mContext, frame);
        if (re != 0)return nullptr;
        auto pkt = av_packet_alloc();
        //接收编码线程数据
        re = avcodec_receive_packet(mContext, 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*> YEncoder::End()
    {
        std::vector<AVPacket*> res;
        std::unique_lock<std::mutex>lock(mMutex);
        if (!mContext)return res;
        auto re = avcodec_send_frame(mContext, NULL); //发送NULL 获取缓冲
        if (re != 0)return res;
        while (re >= 0)
        {
            auto pkt = av_packet_alloc();
            re = avcodec_receive_packet(mContext, pkt);
            if (re != 0)
            {
                av_packet_free(&pkt);
                break;
            }
            res.push_back(pkt);
        }
        return res;
    }
    
    ///////////////////////////////////////////////////////////////
    //根据AVCodecContext 创建一个AVFrame,需要调用者释放av_frame_free
    AVFrame* YEncoder::CreateFrame()
    {
        std::unique_lock<std::mutex>lock(mMutex);
        if (!mContext)return nullptr;
        auto frame = av_frame_alloc();
        frame->width = mContext->width;
        frame->height = mContext->height;
        frame->format = mContext->pix_fmt;
        auto re = av_frame_get_buffer(frame, 0);
        if (re != 0)
        {
            av_frame_free(&frame);
            PrintErr(re);
            return nullptr;
        }
        return frame;
    }
    
    //像素格式转换
    void YEncoder::InitSwsCtx()
    {
        std::unique_lock<std::mutex>lock(mMutex);
        if (!mContext)return;
        swsCtx = sws_getCachedContext(swsCtx,
            mContext->width, mContext->height, AV_PIX_FMT_BGRA,//输入的宽高及像素格式
            mContext->width, mContext->height,//输出的宽高
            AV_PIX_FMT_YUV420P,//输出的像素格式
            SWS_BILINEAR,//双线性插值算法
            0, 0, 0
        );
        if (!swsCtx)
        {
            qDebug() << "sws_getCachedContext failed!";
        }
    }
    
    void YEncoder::scale(uint8_t* inData, AVFrame* frame)
    {
        std::unique_lock<std::mutex>lock(mMutex);
        if (!mContext)return;
    
        const uint8_t* srcData[AV_NUM_DATA_POINTERS];
        srcData[0] = inData;
        int srcLines[AV_NUM_DATA_POINTERS];
        srcLines[0] = mContext->width * 4;
        int ret = sws_scale(swsCtx
            , srcData, srcLines, 0, mContext->height,
            frame->data, frame->linesize
        );
        if (ret <= 0)
        {
            PrintErr(ret);
        }
    }
    void YEncoder::Release()
    {
        if (swsCtx)
        {
            sws_freeContext(swsCtx);
            swsCtx = nullptr;
        }
    }
    
    YEncoder::YEncoder()
    {
    
    }
    
    YEncoder::~YEncoder()
    {
    }

     

posted on 2025-05-21 16:35  飘杨......  阅读(158)  评论(0)    收藏  举报