FFmpeg 6.0 实战:用 C++ 封装摄像头采集与 RTSP 推流

FFmpeg 6.0 实战:用 C++ 封装摄像头采集与 RTSP 推流(附完整代码)

引言

在 Windows 平台上,使用 FFmpeg 进行摄像头采集和 RTSP 推流是常见的需求。然而,官方示例多以 C 语言为主,且涉及多线程、图像格式转换、编码器等复杂流程,容易踩坑。本文分享一个基于 Qt 的 CVideoCapTask 类,它独立于 Qt 的 UI 部分,封装了 摄像头采集 → 解码 → 预览(QImage)→ H.264 编码 → RTSP 推流 的完整流程,并特别解决了在 FFmpeg 6.0 环境下 avio_open 导致 Protocol not found 的典型问题。

代码已在 Windows 10 + MSVC 2019 + FFmpeg 6.0.2(win32 shared)验证通过,可作为直播推流、视频监控终端的基础组件。


一、类设计与功能概览

CVideoCapTask 继承自 QThread,将采集和推流放在独立线程中,通过信号 SigFrameCaptured(QImage) 将预览画面传递给 UI 线程,避免界面卡顿。

主要接口:

  • SetDeviceName(const QString&):设置 DirectShow 设备名,如 "video=Integrated Camera"
  • SetFramerate(int):目标帧率(默认30)
  • SetTargetSize(int,int):预览缩放尺寸(可选)
  • SetRtspUrl(const QString&):推流地址,如 "rtsp://127.0.0.1:8554/live"
  • EnablePush(bool):是否启用推流
  • StartCapture() / StopCapture():启停采集

内部工作流:

复制代码
摄像头(DShow) → av_read_frame → 解码(MJPEG/H.264) → AVFrame
                                              ├→ 缩放→ RGB32 → 发送预览
                                              └→ 色彩转换(YUV420P) → H.264编码 → RTMP/RTSP

二、FFmpeg 初始化与设备打开

2.1 全局初始化

cpp 复制代码
avdevice_register_all();   // 注册设备(dshow/v4l2)
avformat_network_init();   // 初始化网络(后续RTSP推流需要)

2.2 打开 DirectShow 摄像头

这里使用 带 const 修饰 的 API,因为 FFmpeg 6.0 中 av_find_input_format 返回 const AVInputFormat*

cpp 复制代码
const AVInputFormat* pInputFmt = av_find_input_format("dshow");
AVDictionary* opts = nullptr;
av_dict_set(&opts, "framerate", "30", 0);
av_dict_set(&opts, "rtbufsize", "512M", 0);
avformat_open_input(&m_pFormatCtx, "video=摄像头名", pInputFmt, &opts);

查找视频流后,根据 codecpar->codec_id(通常为 AV_CODEC_ID_MJPEG)打开解码器:

cpp 复制代码
const AVCodec* pDecoder = avcodec_find_decoder(pCodecPar->codec_id);
m_pCodecCtx = avcodec_alloc_context3(pDecoder);
avcodec_parameters_to_context(m_pCodecCtx, pCodecPar);
avcodec_open2(m_pCodecCtx, pDecoder, nullptr);

三、图像格式转换:解码 → 预览(QImage)

摄像头输出格式可能是 MJPEG、YUYV 等,而 Qt 显示需要 BGRA/RGB32 。我们使用 sws_scale 进行颜色空间和尺寸转换。

cpp 复制代码
QImage AVFrameToQImage(AVFrame* frame) {
    int dstW = m_targetWidth ? m_targetWidth : frame->width;
    int dstH = m_targetHeight ? m_targetHeight : frame->height;
    SwsContext* sws = sws_getContext(frame->width, frame->height, (AVPixelFormat)frame->format,
                                     dstW, dstH, AV_PIX_FMT_BGRA, SWS_FAST_BILINEAR, ...);
    uint8_t* dstData[1] = { (uint8_t*)av_malloc(dstW * dstH * 4) };
    sws_scale(sws, frame->data, frame->linesize, 0, frame->height, dstData, dstStride);
    QImage img(dstData[0], dstW, dstH, dstW*4, QImage::Format_ARGB32);
    QImage ret = img.copy(); // 深拷贝
    av_free(dstData[0]);
    return ret;
}

注意: 每次调用都会重新创建 SwsContext,但代码中缓存的 m_pSwsCtx 只在尺寸/格式变化时重建,避免重复分配。


四、RTSP 推流:编码与封装

4.1 推流初始化(关键!)

在早期的代码中,手动调用 avio_open 打开网络连接,导致 FFmpeg 内部协议识别错乱(RTSP protocol found: file)。参考 CLiveTask 的成功经验,让 RTSP muxer 自动管理 IO ,即不手动调用 avio_open ,直接调用 avformat_write_header 即可。

正确做法:

cpp 复制代码
bool InitPushStream() {
    // 1. 分配输出上下文
    avformat_alloc_output_context2(&m_pOutFormatCtx, nullptr, "rtsp", rtspUrl.c_str());
    
    // 2. 添加视频流,配置 H.264 编码器
    const AVCodec* encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
    AVStream* outStream = avformat_new_stream(m_pOutFormatCtx, nullptr);
    m_pOutCodecCtx = avcodec_alloc_context3(encoder);
    // ... 设置宽高、比特率、gop等 ...
    avcodec_open2(m_pOutCodecCtx, encoder, nullptr);
    avcodec_parameters_from_context(outStream->codecpar, m_pOutCodecCtx);
    
    // 3. 关键:不调用 avio_open,让 muxer 自己处理
    m_pOutFormatCtx->pb = nullptr;
    
    // 4. 添加 RTSP 传输选项(TCP模式,避免丢包)
    AVDictionary* opts = nullptr;
    av_dict_set(&opts, "rtsp_transport", "tcp", 0);
    ret = avformat_write_header(m_pOutFormatCtx, &opts);
    av_dict_free(&opts);
    
    // 5. 颜色转换上下文(解码帧 -> YUV420P,供编码器使用)
    m_pOutSwsCtx = sws_getContext(width, height, m_pCodecCtx->pix_fmt,
                                  width, height, AV_PIX_FMT_YUV420P, ...);
    return true;
}

4.2 编码与推送(PushFrame)

每一帧经历:

  1. 使用 m_pOutSwsCtx 将解码后的 AVFrame(可能是 MJPEG 解码后的 YUVJ 或其它格式)转为 YUV420P
  2. 填充 pts(按帧计数,简单递增)。
  3. 调用 avcodec_send_frameavcodec_receive_packet 获得 H.264 编码包。
  4. 调整时间基后调用 av_interleaved_write_frame 发送。
cpp 复制代码
bool PushFrame(AVFrame* frame) {
    AVFrame* yuv = av_frame_alloc();
    // ... 分配 buffer,sws_scale ...
    yuv->pts = m_ptsCounter++;
    avcodec_send_frame(m_pOutCodecCtx, yuv);
    while (avcodec_receive_packet(m_pOutCodecCtx, &pkt) == 0) {
        av_packet_rescale_ts(&pkt, m_pOutCodecCtx->time_base, outStream->time_base);
        av_interleaved_write_frame(m_pOutFormatCtx, &pkt);
    }
    av_frame_free(&yuv);
}

五、线程主循环(run)

cpp 复制代码
void run() override {
    OpenDevice();
    if (m_pushEnabled) InitPushStream();
    AVPacket packet;
    AVFrame* frame = av_frame_alloc();
    while (m_running) {
        if (av_read_frame(m_pFormatCtx, &packet) < 0) {
            msleep(1); continue;
        }
        if (packet.stream_index == m_videoStreamIndex) {
            avcodec_send_packet(m_pCodecCtx, &packet);
            while (avcodec_receive_frame(m_pCodecCtx, frame) == 0) {
                emit SigFrameCaptured(AVFrameToQImage(frame));  // 预览
                if (m_pushEnabled) PushFrame(frame);            // 推流
                av_frame_unref(frame);
            }
        }
        av_packet_unref(&packet);
    }
    // 清理资源...
}

注意预览帧率: 如果 UI 显示卡顿,可以在 emit 之前加入帧数控制(例如每 2 帧发送一次),因为预览不需要和推流同频。


六、解决推流失败的心路历程

现象

  • 执行 avio_open 时返回 Protocol not found,虽然 avio_find_protocol_name("rtsp") 返回 "file"
  • 同一个 FFmpeg 库,另一个 CLiveTask 类却能正常推流。

对比分析

CLiveTask 的推流初始化流程:

cpp 复制代码
avformat_alloc_output_context2(...);
// 配置编码器,添加流,不调用 avio_open
avformat_write_header(m_pOutFormatCtx, &opts);   // 内部自动打开网络连接

CVideoCapTask 早期版本:

cpp 复制代码
avio_open(&m_pOutFormatCtx->pb, url, AVIO_FLAG_WRITE);  // 手动打开
avformat_write_header(...);

结论: 在某些 FFmpeg 版本或编译配置中,手动调用 avio_open 会干扰 RTSP muxer 的协议注册 ,导致 muxer 无法识别 rtsp 协议。解决方法就是 移除手动 avio_open,完全信任 muxer 的自动处理。


七、使用示例、结果

cpp 复制代码
// 在主线程中
CVideoCapTask* task = new CVideoCapTask(this);
task->SetDeviceName("video=Integrated Camera");
task->SetTargetSize(640, 360);            // 预览缩放
task->SetRtspUrl("rtsp://192.168.1.100:8554/cam");
task->EnablePush(true);
task->StartCapture();

// 连接预览信号到 UI 控件
connect(task, &CVideoCapTask::SigFrameCaptured, this, [=](const QImage& img){
    ui->label->setPixmap(QPixmap::fromImage(img).scaled(ui->label->size()));
});

结果:

使用window rtsp 服务器演示

推流到rtsp服务器 ,使用vlc 播放

八、注意事项与优化建议

  1. 帧率控制 :当前推流的 pts 使用递增计数器,不依赖实际时间。若需严格同步,可基于系统时间计算。
  2. CPU 占用 :MJPEG 解码 + H.264 编码对 CPU 有一定要求。可调低编码质量(bit_rate 设为 500k ~ 1000k)或使用硬件编码器(如 h264_mfh264_qsv)。
  3. 容错处理 :当 RTSP 服务器断开时,av_interleaved_write_frame 会返回错误,代码中仅警告。生产环境应加入重连逻辑。
  4. 内存泄漏 :确保 AVFrameAVPacket 正确释放,示例代码已全部使用 av_frame_freeav_packet_unref

九、完整源码获取

cpp 复制代码
#include "../Include/VideoCapTask.h"
#include <QDebug>
#include <chrono>
#include <thread>

CVideoCapTask::CVideoCapTask(QObject* parent)
    : QThread(parent)
    , m_framerate(30)
    , m_targetWidth(0)
    , m_targetHeight(0)
    , m_srcWidth(0)
    , m_srcHeight(0)
    , m_dstWidth(0)
    , m_dstHeight(0)
    , m_running(false)
    , m_pFormatCtx(nullptr)
    , m_pCodecCtx(nullptr)
    , m_pSwsCtx(nullptr)
    , m_videoStreamIndex(-1)
    , m_pushEnabled(false)
    , m_pOutFormatCtx(nullptr)
    , m_pOutCodecCtx(nullptr)
    , m_pOutSwsCtx(nullptr)
    , m_outStreamIndex(-1)
    , m_ptsCounter(0)
{
    InitFFmpeg();
}

CVideoCapTask::~CVideoCapTask()
{
    StopCapture();
    wait();
    CloseDevice();
    ClosePushStream();
}

void CVideoCapTask::SetDeviceName(const QString& deviceName)
{
    QMutexLocker locker(&m_mutex);
    m_deviceName = deviceName;
}

void CVideoCapTask::SetFramerate(int fps)
{
    QMutexLocker locker(&m_mutex);
    if (fps > 0) m_framerate = fps;
}

void CVideoCapTask::SetTargetSize(int width, int height)
{
    QMutexLocker locker(&m_mutex);
    m_targetWidth = width;
    m_targetHeight = height;
}

void CVideoCapTask::SetRtspUrl(const QString& url)
{
    QMutexLocker locker(&m_mutex);
    m_rtspUrl = url;
    qDebug() << m_rtspUrl;
}

void CVideoCapTask::EnablePush(bool enable)
{
    QMutexLocker locker(&m_mutex);
    m_pushEnabled = enable;
}

void CVideoCapTask::StartCapture()
{
    if (m_running) return;
    if (m_deviceName.isEmpty()) {
        qWarning() << "Device name not set";
        return;
    }
    m_running = true;
    start();
}

void CVideoCapTask::StopCapture()
{
    if (!m_running) return;
    m_running = false;
}

bool CVideoCapTask::InitFFmpeg()
{
    static bool bRegistered = false;
    if (!bRegistered) {
        avdevice_register_all();
        avformat_network_init();
        bRegistered = true;
    }
    return true;
}

bool CVideoCapTask::OpenDevice()
{
    // ***** const 修饰 *****
    const AVInputFormat* pInputFmt = nullptr;
#ifdef _WIN32
    pInputFmt = av_find_input_format("dshow");
#else
    pInputFmt = av_find_input_format("v4l2");
#endif
    if (!pInputFmt) {
        qCritical() << "Cannot find input format";
        return false;
    }

    AVDictionary* pOptions = nullptr;
    av_dict_set(&pOptions, "framerate", QString::number(m_framerate).toUtf8().constData(), 0);
    av_dict_set(&pOptions, "rtbufsize", "512M", 0);

    int ret = avformat_open_input(&m_pFormatCtx, m_deviceName.toUtf8().constData(),
        pInputFmt, &pOptions);
    av_dict_free(&pOptions);
    if (ret < 0) {
        char errbuf[128];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qCritical() << "Cannot open device:" << m_deviceName << errbuf;
        return false;
    }

    if (avformat_find_stream_info(m_pFormatCtx, nullptr) < 0) {
        qCritical() << "Cannot find stream info";
        return false;
    }

    m_videoStreamIndex = -1;
    for (unsigned i = 0; i < m_pFormatCtx->nb_streams; ++i) {
        if (m_pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            m_videoStreamIndex = i;
            break;
        }
    }
    if (m_videoStreamIndex == -1) {
        qCritical() << "No video stream found";
        return false;
    }

    AVCodecParameters* pCodecPar = m_pFormatCtx->streams[m_videoStreamIndex]->codecpar;
    // ***** const 修饰 *****
    const AVCodec* pDecoder = avcodec_find_decoder(pCodecPar->codec_id);
    if (!pDecoder) {
        qCritical() << "Decoder not found";
        return false;
    }

    m_pCodecCtx = avcodec_alloc_context3(pDecoder);
    if (avcodec_parameters_to_context(m_pCodecCtx, pCodecPar) < 0) {
        qCritical() << "Failed to copy codec parameters";
        return false;
    }

    if (avcodec_open2(m_pCodecCtx, pDecoder, nullptr) < 0) {
        qCritical() << "Cannot open decoder";
        return false;
    }

    qDebug() << "Opened device:" << m_deviceName
        << "size:" << pCodecPar->width << "x" << pCodecPar->height
        << "codec:" << avcodec_get_name(pCodecPar->codec_id);
    return true;
}

void CVideoCapTask::CloseDevice()
{
    if (m_pCodecCtx) {
        avcodec_free_context(&m_pCodecCtx);
        m_pCodecCtx = nullptr;
    }
    if (m_pFormatCtx) {
        avformat_close_input(&m_pFormatCtx);
        m_pFormatCtx = nullptr;
    }
    if (m_pSwsCtx) {
        sws_freeContext(m_pSwsCtx);
        m_pSwsCtx = nullptr;
    }
    m_videoStreamIndex = -1;
}

QImage CVideoCapTask::AVFrameToQImage(AVFrame* frame)
{
    if (!frame || frame->width <= 0 || frame->height <= 0)
        return QImage();

    int dstWidth = m_targetWidth > 0 ? m_targetWidth : frame->width;
    int dstHeight = m_targetHeight > 0 ? m_targetHeight : frame->height;
    AVPixelFormat dstFormat = AV_PIX_FMT_BGRA;

    if (!m_pSwsCtx ||
        m_srcWidth != frame->width || m_srcHeight != frame->height ||
        m_dstWidth != dstWidth || m_dstHeight != dstHeight) {
        if (m_pSwsCtx) sws_freeContext(m_pSwsCtx);
        m_pSwsCtx = sws_getContext(frame->width, frame->height, (AVPixelFormat)frame->format,
            dstWidth, dstHeight, dstFormat,
            SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
        if (!m_pSwsCtx) {
            qWarning() << "Cannot create SwsContext";
            return QImage();
        }
        m_srcWidth = frame->width;
        m_srcHeight = frame->height;
        m_dstWidth = dstWidth;
        m_dstHeight = dstHeight;
    }

    int dstStride = dstWidth * 4;
    uint8_t* dstData = (uint8_t*)av_malloc(dstStride * dstHeight);
    if (!dstData) return QImage();

    uint8_t* dstPlane[1] = { dstData };
    int dstLinesize[1] = { dstStride };
    sws_scale(m_pSwsCtx, frame->data, frame->linesize, 0, frame->height,
        dstPlane, dstLinesize);

    QImage image(dstData, dstWidth, dstHeight, dstStride, QImage::Format_ARGB32);
    QImage result = image.copy();
    av_free(dstData);
    return result;
}

bool CVideoCapTask::InitPushStream()
{
    if (m_rtspUrl.isEmpty()) {
        qWarning() << "RTSP URL is empty, cannot push";
        return false;
    }

    // 1. 分配输出上下文(与之前相同)
    int ret = avformat_alloc_output_context2(&m_pOutFormatCtx, nullptr, "rtsp", m_rtspUrl.toStdString().c_str());
    if (!m_pOutFormatCtx || ret < 0) {
        qCritical() << "Failed to allocate output context for RTSP";
        return false;
    }

    qDebug() << "Output format name:" << m_pOutFormatCtx->oformat->name;

    // 2. 查找编码器、创建流、配置编码器(保持原样)
    const AVCodec* encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!encoder) {
        qCritical() << "H.264 encoder not found";
        return false;
    }

    AVStream* outStream = avformat_new_stream(m_pOutFormatCtx, nullptr);
    if (!outStream) {
        qCritical() << "Failed to create output stream";
        return false;
    }
    m_outStreamIndex = outStream->index;

    m_pOutCodecCtx = avcodec_alloc_context3(encoder);
    if (!m_pOutCodecCtx) {
        qCritical() << "Failed to allocate encoder context";
        return false;
    }

    int width = m_pCodecCtx->width;
    int height = m_pCodecCtx->height;
    m_pOutCodecCtx->width = width;
    m_pOutCodecCtx->height = height;
    m_pOutCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    m_pOutCodecCtx->time_base = AVRational{ 1, m_framerate };
    m_pOutCodecCtx->framerate = AVRational{ m_framerate, 1 };
    m_pOutCodecCtx->bit_rate = 2 * 1024 * 1024;
    m_pOutCodecCtx->gop_size = m_framerate;
    m_pOutCodecCtx->max_b_frames = 0;
    if (m_pOutFormatCtx->oformat->flags & AVFMT_GLOBALHEADER)
        m_pOutCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

    ret = avcodec_open2(m_pOutCodecCtx, encoder, nullptr);
    if (ret < 0) {
        char errbuf[128];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qCritical() << "Cannot open encoder:" << errbuf;
        return false;
    }

    ret = avcodec_parameters_from_context(outStream->codecpar, m_pOutCodecCtx);
    if (ret < 0) {
        qCritical() << "Failed to copy encoder parameters to stream";
        return false;
    }
    outStream->time_base = m_pOutCodecCtx->time_base;

    av_dump_format(m_pOutFormatCtx, 0, m_rtspUrl.toStdString().c_str(), 1);

    // 3. 关键修改:不要手动调用 avio_open,让 muxer 自己处理
    // 确保 pb 为 nullptr(默认就是)
    m_pOutFormatCtx->pb = nullptr;

    // 可选:设置 RTSP 传输选项(与 CLiveTask 一致)
    AVDictionary* opts = nullptr;
    av_dict_set(&opts, "rtsp_transport", "tcp", 0);
    // 如果仍然有问题,可以加上协议白名单(虽然不是必须)
    // av_dict_set(&opts, "protocol_whitelist", "file,udp,tcp,rtp,rtsp", 0);

    // 4. 写头部:这一步内部会自动调用 avio_open 并正确处理 RTSP 协议
    ret = avformat_write_header(m_pOutFormatCtx, &opts);
    av_dict_free(&opts);
    if (ret < 0) {
        char errbuf[128];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qCritical() << "Failed to write header:" << errbuf;
        return false;
    }

    // 5. 初始化颜色空间转换(保持不变)
    m_pOutSwsCtx = sws_getContext(width, height, m_pCodecCtx->pix_fmt,
        width, height, AV_PIX_FMT_YUV420P,
        SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
    if (!m_pOutSwsCtx) {
        qCritical() << "Cannot create SwsContext for encoder conversion";
        return false;
    }

    m_ptsCounter = 0;
    qDebug() << "RTSP push stream initialized, URL:" << m_rtspUrl;
    return true;
}

void CVideoCapTask::ClosePushStream()
{
    if (m_pOutCodecCtx) {
        avcodec_free_context(&m_pOutCodecCtx);
        m_pOutCodecCtx = nullptr;
    }
    if (m_pOutFormatCtx) {
        if (m_pOutFormatCtx->pb) {
            av_write_trailer(m_pOutFormatCtx);
            avio_close(m_pOutFormatCtx->pb);
        }
        avformat_free_context(m_pOutFormatCtx);
        m_pOutFormatCtx = nullptr;
    }
    if (m_pOutSwsCtx) {
        sws_freeContext(m_pOutSwsCtx);
        m_pOutSwsCtx = nullptr;
    }
    m_outStreamIndex = -1;
    m_ptsCounter = 0;
}

bool CVideoCapTask::PushFrame(AVFrame* frame)
{
    if (!m_pOutFormatCtx || !m_pOutCodecCtx || !m_pOutSwsCtx)
        return false;

    // 1. 转换像素格式到 YUV420P(编码器需要)
    AVFrame* yuvFrame = av_frame_alloc();
    yuvFrame->format = AV_PIX_FMT_YUV420P;
    yuvFrame->width = frame->width;
    yuvFrame->height = frame->height;
    if (av_frame_get_buffer(yuvFrame, 0) < 0) {
        av_frame_free(&yuvFrame);
        qWarning() << "Failed to allocate buffer for YUV frame";
        return false;
    }

    sws_scale(m_pOutSwsCtx, frame->data, frame->linesize, 0, frame->height,
        yuvFrame->data, yuvFrame->linesize);

    // 2. 设置时间戳
    yuvFrame->pts = m_ptsCounter++;
    yuvFrame->pkt_dts = yuvFrame->pts;

    // 3. 发送原始帧到编码器
    int ret = avcodec_send_frame(m_pOutCodecCtx, yuvFrame);
    if (ret < 0) {
        av_frame_free(&yuvFrame);
        char errbuf[128];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qWarning() << "Error sending frame to encoder:" << errbuf;
        return false;
    }

    // 4. 接收编码后的包并写入输出
    AVPacket pkt;
    av_init_packet(&pkt);
    pkt.data = nullptr;
    pkt.size = 0;

    while (true) {
        ret = avcodec_receive_packet(m_pOutCodecCtx, &pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        }
        else if (ret < 0) {
            char errbuf[128];
            av_strerror(ret, errbuf, sizeof(errbuf));
            qWarning() << "Error receiving packet from encoder:" << errbuf;
            break;
        }

        // 转换时间基
        pkt.stream_index = m_outStreamIndex;
        av_packet_rescale_ts(&pkt, m_pOutCodecCtx->time_base, m_pOutFormatCtx->streams[m_outStreamIndex]->time_base);
        pkt.pos = -1;

        ret = av_interleaved_write_frame(m_pOutFormatCtx, &pkt);
        if (ret < 0) {
            char errbuf[128];
            av_strerror(ret, errbuf, sizeof(errbuf));
            qWarning() << "Error writing packet to RTSP:" << errbuf;
        }
        av_packet_unref(&pkt);
    }

    av_frame_free(&yuvFrame);
    return true;
}

void CVideoCapTask::run()
{
    if (!OpenDevice()) {
        qCritical() << "Failed to open device, cannot run";
        m_running = false;
        return;
    }

    if (m_pushEnabled && !m_rtspUrl.isEmpty()) {
        if (!InitPushStream()) {
            qWarning() << "Failed to initialize RTSP push stream, continuing without push";
            m_pushEnabled = false;
        }
    }

    AVPacket packet;
    AVFrame* pFrame = av_frame_alloc();
    if (!pFrame) {
        qCritical() << "Failed to allocate frame";
        CloseDevice();
        ClosePushStream();
        m_running = false;
        return;
    }

    while (m_running) {
        int ret = av_read_frame(m_pFormatCtx, &packet);
        if (ret < 0) {
            if (ret == AVERROR(EAGAIN)) {
                std::this_thread::sleep_for(std::chrono::milliseconds(1));
                continue;
            }
            else {
                char errbuf[128];
                av_strerror(ret, errbuf, sizeof(errbuf));
                qWarning() << "av_read_frame error:" << errbuf;
                break;
            }
        }

        if (packet.stream_index == m_videoStreamIndex) {
            ret = avcodec_send_packet(m_pCodecCtx, &packet);
            if (ret < 0) {
                av_packet_unref(&packet);
                continue;
            }

            while (true) {
                ret = avcodec_receive_frame(m_pCodecCtx, pFrame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
                    break;
                }
                else if (ret < 0) {
                    char errbuf[128];
                    av_strerror(ret, errbuf, sizeof(errbuf));
                    qWarning() << "Decode error:" << errbuf;
                    break;
                }

                //QImage img = AVFrameToQImage(pFrame);
                //if (!img.isNull()) {
                //    emit SigFrameCaptured(img);
                //}

                static int previewFrameCount = 0;
                if (++previewFrameCount % 3 == 0) {
                    QImage img = AVFrameToQImage(pFrame);
                    if (!img.isNull()) {
                        emit SigFrameCaptured(img);
                    }
                }

                if (m_pushEnabled && m_pOutFormatCtx) {
                    PushFrame(pFrame);
                }

                av_frame_unref(pFrame);
            }
        }
        av_packet_unref(&packet);
    }

    av_frame_free(&pFrame);
    CloseDevice();
    ClosePushStream();
    qDebug() << "CVideoCapTask stopped";
}

十、总结

通过封装 CVideoCapTask 类,我们实现了:

  • Windows DirectShow 摄像头采集
  • 跨平台(可迁移到 Linux v4l2)
  • 多线程无阻塞预览
  • RTSP 推流(H.264)
  • 常见 FFmpeg API 误用问题的回避

希望本文能帮助到正在使用 FFmpeg 开发视频应用的开发者。如果觉得有用,请点赞、收藏、转发!


参考链接

上一篇文章:
QT开发技术【ffmpeg EVideo录屏软件 一】

相关推荐
天天进步20151 小时前
Python全栈项目实战:基于深度学习的语音合成(TTS)系统
开发语言·python·深度学习
OctShop大型商城源码2 小时前
.NET线上商城源码_C#商城源码_技术赋能下的电商新生态
开发语言·c#·.net·商城系统源码
光电笑映2 小时前
从环境变量到进程虚拟地址空间——Linux 内存管理的底层脉络
linux·服务器·c++·c
IT猿手2 小时前
光伏模型参数估计:基于山羊优化算法(GOA )的光伏模型参数辨识问题求解研究,免费提供完整MATLAB代码链接
开发语言·算法·matlab·群智能优化算法·智能优化算法·光伏模型参数估计·光伏模型参数辨识
xrgs_shz2 小时前
【高光谱数据处理实战】基于Python的ENVI图像交互式裁剪与光谱数据预处理
开发语言·图像处理·python
MATLAB代码顾问2 小时前
麻雀搜索算法(SSA)原理详解与Python实现
开发语言·python
sparEE2 小时前
c++字符串和自定义字面量
开发语言·c++
赏金术士3 小时前
Kotlin 从入门到进阶 之作用域函数 & 优雅写法(五)
android·开发语言·kotlin
openKaka_3 小时前
从 scheduleUpdateOnFiber 到 Root 微任务调度:React 如何把更新交给调度系统
开发语言·前端·javascript