一、前言
本文主要记录通过 Qt 结合 FFmpeg 实现从摄像头采集视频数据,并将采集到的数据保存为 YUV 格式文件的过程。涉及 FFmpeg 设备操作、Qt 多线程编程等核心知识点,适合有一定 Qt 和 FFmpeg 基础的开发者学习参考。
二、环境准备
- 开发框架:Qt 5/6(本文以 Qt 为例,核心逻辑不依赖具体版本)
- FFmpeg 库 :需包含
libavdevice、libavformat、libavutil、libavcodec等核心库 - 系统环境:本文以 Windows 系统为例,使用 dshow(DirectShow)作为设备输入格式
三、核心代码解析
3.1 线程类头文件(audiothread.h)
由于视频采集是耗时操作,需放在子线程中执行,避免阻塞主线程(UI 线程)。
cpp
#ifndef AUDIOTHREAD_H
#define AUDIOTHREAD_H
#include <QThread>
class audioThread : public QThread
{
Q_OBJECT
private:
void run(); // 线程执行函数,重写QThread的run方法
public:
explicit audioThread(QObject *parent = nullptr);
~audioThread();
};
#endif // AUDIOTHREAD_H
3.2 线程实现文件(audiothread.cpp)
核心逻辑集中在该文件,包含 FFmpeg 设备打开、数据采集、文件写入、资源释放等步骤。
3.2.1 头文件与宏定义
cpp
#include "audiothread.h"
#include <qdebug.h>
#include <qfile.h>
// 引入FFmpeg的C语言接口
extern "C"{
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libavcodec/avcodec.h>
}
// Windows系统下的设备配置
#ifdef Q_OS_WIN
#define FMT_NAME "dshow" // 设备输入格式(dshow)
#define DEVICE_NAME "video=Integrated Webcam" // 摄像头设备名称
#define FILENAME "D:/out.yuv" // YUV文件保存路径
#endif
// 错误信息格式化宏
#define ERROR_BUF(ret) \
char errbuf[1024]; \
av_strerror(ret,errbuf,sizeof(errbuf));
3.2.2 构造与析构函数
cpp
audioThread::audioThread(QObject *parent):QThread{parent}
{
// 线程结束时自动回收内存
connect(this,&audioThread::finished,
this,&audioThread::deleteLater);
}
audioThread::~audioThread()
{
disconnect(); // 断开所有信号槽连接
requestInterruption(); // 请求线程中断
quit(); // 退出事件循环
wait(); // 等待线程结束
qDebug() << this << "析构(内存被回收)";
}
3.2.3 核心采集逻辑(run 方法)
cpp
void audioThread::run()
{
qDebug() << this << "开始执行------";
// 1. 获取输入格式对象(dshow)
AVInputFormat *fmt = av_find_input_format(FMT_NAME);
if(!fmt){
qDebug() << "av_find_input_format error" << FMT_NAME;
return;
}
// 2. 初始化格式上下文(操作设备的核心上下文)
AVFormatContext *ctx = nullptr;
AVDictionary *options = nullptr;
// 设置采集参数:分辨率、像素格式、帧率
av_dict_set(&options,"video_size","1280x720",0);
av_dict_set(&options,"pixel_format","yuyv422",0);
av_dict_set(&options,"framerate","10",0);
// 3. 打开摄像头设备
int ret = avformat_open_input(&ctx,DEVICE_NAME,fmt,&options);
if(ret < 0){
ERROR_BUF(ret);
qDebug() << "avformat_open_input error" << errbuf;
return;
}
// 4. 打开YUV文件用于写入
QFile file(FILENAME);
if(!file.open(QFile::WriteOnly)){
qDebug() << "file open error" << FILENAME;
avformat_close_input(&ctx); // 失败时关闭设备
return;
}
// 5. 计算一帧视频数据的大小
AVCodecParameters *params = ctx->streams[0]->codecpar;
AVPixelFormat pixFmt = (AVPixelFormat)params->format;
int imageSize = av_image_get_buffer_size(
pixFmt,
params->width,
params->height,
1);
// 6. 循环采集视频数据
AVPacket *pkt = av_packet_alloc(); // 分配数据包
while(!isInterruptionRequested()){ // 检查是否需要中断
ret = av_read_frame(ctx,pkt); // 读取一帧数据
if(ret == 0){ // 读取成功
// 将数据写入YUV文件
file.write((const char*)pkt->data,imageSize);
av_packet_unref(pkt); // 释放数据包引用
}else if(ret == AVERROR(EAGAIN)){ // 资源临时不可用,继续循环
continue;
}else{ // 其他错误,退出循环
ERROR_BUF(ret);
qDebug() << "av_read_frame error" << errbuf << ret;
break;
}
}
// 7. 释放资源
av_packet_free(&pkt); // 释放数据包
file.close(); // 关闭文件
avformat_close_input(&ctx); // 关闭设备
qDebug() << this << "正常结束------";
}
3.3 主窗口逻辑(mainwindow 相关)
3.3.1 头文件(mainwindow.h)
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "audiothread.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_playBtn_clicked(); // 按钮点击槽函数
private:
Ui::MainWindow *ui;
audioThread *_audioThread = nullptr; // 采集线程指针
};
#endif // MAINWINDOW_H
3.3.2 实现文件(mainwindow.cpp)
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_playBtn_clicked()
{
if(!_audioThread){ // 开始采集
_audioThread = new audioThread(this);
_audioThread->start(); // 启动线程
// 线程结束后重置指针并恢复按钮文字
connect(_audioThread,&audioThread::finished,
[this](){
_audioThread = nullptr;
ui->playBtn->setText("开始录视频");
});
ui->playBtn->setText("结束录视频");
}else{ // 停止采集
_audioThread->requestInterruption(); // 请求线程中断
_audioThread = nullptr;
ui->playBtn->setText("开始录视频");
}
}
3.4 程序入口(main.cpp)
cpp
#include "mainwindow.h"
#include <QApplication>
extern "C"{
#include <libavdevice/avdevice.h>
}
int main(int argc, char *argv[])
{
avdevice_register_all(); // 注册FFmpeg所有设备
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
四、关键知识点总结
4.1 FFmpeg 核心操作流程
avdevice_register_all():注册所有 FFmpeg 设备,必须在操作设备前调用;av_find_input_format():根据格式名称(如 dshow)获取输入格式对象;avformat_open_input():打开设备 / 文件,初始化格式上下文;av_read_frame():从设备读取一帧视频数据(封装在 AVPacket 中);avformat_close_input():关闭设备,释放格式上下文资源;av_image_get_buffer_size():计算指定分辨率、像素格式的单帧视频数据大小。
4.2 Qt 多线程注意事项
- 耗时操作(如视频采集)必须放在
QThread::run()方法中,避免阻塞 UI; - 线程中断通过
requestInterruption()+isInterruptionRequested()配合实现,优雅停止线程; - 线程结束后通过
finished信号关联deleteLater(),自动回收线程内存,避免内存泄漏; - 主线程与子线程的交互尽量通过信号槽,避免直接操作子线程对象。
4.3 YUV 文件验证
采集完成后,可通过 FFmpeg 的 ffplay 工具验证 YUV 文件是否有效:
ffplay -f rawvideo -pixel_format yuyv422 -video_size 1280x720 -framerate 10 D:/out.yuv
五、常见问题与解决
-
avformat_open_input 失败:
- 检查设备名称是否正确(Windows 下可通过
ffmpeg -list_devices true -f dshow -i dummy查看摄像头名称); - 确认 FFmpeg 编译时包含了 dshow 模块;
- 检查权限(摄像头是否被其他程序占用)。
- 检查设备名称是否正确(Windows 下可通过
-
采集数据写入文件后无法播放:
- 确认像素格式、分辨率、帧率与 ffplay 播放参数一致;
- 检查单帧数据大小计算是否正确,避免写入数据长度错误。
-
线程退出时崩溃:
- 确保退出时释放所有 FFmpeg 资源(AVPacket、AVFormatContext 等);
- 析构函数中先请求中断,再调用 quit () 和 wait (),等待线程完全结束后再释放资源。
六、总结
本文通过 Qt+FFmpeg 实现了摄像头视频采集并保存为 YUV 格式文件,核心是掌握 FFmpeg 设备操作流程和 Qt 多线程编程规范。YUV 作为原始视频格式,是音视频开发的基础,理解其采集过程有助于后续学习编码(如 H.264/H.265)、封装(如 MP4)等进阶知识点。