在音视频处理领域,直接获取摄像头的原始YUV数据并进行录制是一项基础但极其重要的能力。相比常见的MP4、AVI等封装格式,YUV格式保留了最原始的图像数据,为后续的视频分析、编解码测试、算法验证等场景提供了纯净的输入源。
我最近在开发一个视频质量评估系统时,就需要大量原始YUV视频作为测试样本。市面上的录制软件大多直接输出压缩后的视频,而使用Qt+FFmpeg这套组合可以完美解决这个问题。Qt提供了跨平台的摄像头访问接口,FFmpeg则是处理多媒体数据的瑞士军刀,两者结合既能保证兼容性又能实现高性能采集。
推荐使用以下环境组合:
安装时特别注意:
| 工具选项 | 优点 | 缺点 |
|---|---|---|
| Qt Multimedia | 跨平台性好,接口简单 | 功能相对基础 |
| DirectShow | Windows原生,低延迟 | 仅限Windows平台 |
| V4L2 | Linux专用,性能优异 | 配置复杂 |
选择Qt Multimedia是因为我们需要在多个操作系统上运行,且其对摄像头设备的管理更加友好。虽然DirectShow/V4L2在各自平台可能有更好性能,但跨平台需求让我们选择了Qt方案。
cpp复制// 创建摄像头对象
QCamera *camera = new QCamera(QCameraInfo::defaultCamera());
// 设置采集格式为YUV420P(最通用的YUV格式)
QCameraViewfinderSettings settings;
settings.setPixelFormat(QVideoFrame::Format_YUV420P);
settings.setResolution(640, 480); // 推荐使用标准分辨率
camera->setViewfinderSettings(settings);
// 创建视频探针用于获取原始帧
QVideoProbe *probe = new QVideoProbe;
probe->setSource(camera);
connect(probe, SIGNAL(videoFrameProbed(QVideoFrame)),
this, SLOT(processFrame(QVideoFrame)));
关键参数说明:
cpp复制AVFormatContext *fmt_ctx = nullptr;
avformat_alloc_output_context2(&fmt_ctx, nullptr, "rawvideo", "output.yuv");
AVStream *stream = avformat_new_stream(fmt_ctx, nullptr);
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
stream->codecpar->codec_id = AV_CODEC_ID_RAWVIDEO;
stream->codecpar->width = 640;
stream->codecpar->height = 480;
stream->codecpar->format = AV_PIX_FMT_YUV420P;
avio_open(&fmt_ctx->pb, fmt_ctx->filename, AVIO_FLAG_WRITE);
avformat_write_header(fmt_ctx, nullptr);
注意事项:
cpp复制void processFrame(const QVideoFrame &frame) {
frame.map(QAbstractVideoBuffer::ReadOnly);
AVFrame *av_frame = av_frame_alloc();
av_frame->width = frame.width();
av_frame->height = frame.height();
av_frame->format = AV_PIX_FMT_YUV420P;
// 填充YUV数据
av_frame->data[0] = frame.bits(); // Y分量
av_frame->data[1] = frame.bits() + frame.width() * frame.height(); // U分量
av_frame->data[2] = av_frame->data[1] + (frame.width()/2 * frame.height()/2); // V分量
av_frame->linesize[0] = frame.width();
av_frame->linesize[1] = frame.width()/2;
av_frame->linesize[2] = frame.width()/2;
av_interleaved_write_frame(fmt_ctx, av_frame);
frame.unmap();
}
内存布局要点:
直接每帧都立即写入文件会导致性能问题,建议采用生产者-消费者模式:
cpp复制// 全局定义
QQueue<AVFrame*> frameQueue;
QMutex queueMutex;
QWaitCondition queueNotEmpty;
// 生产者线程(原processFrame修改)
void processFrame(...) {
// ...填充av_frame...
queueMutex.lock();
frameQueue.enqueue(av_frame);
queueNotEmpty.wakeAll();
queueMutex.unlock();
}
// 消费者线程
void run() {
while(running) {
queueMutex.lock();
while(frameQueue.isEmpty()) {
queueNotEmpty.wait(&queueMutex);
}
AVFrame *frame = frameQueue.dequeue();
queueMutex.unlock();
av_interleaved_write_frame(fmt_ctx, frame);
av_frame_free(&frame);
}
}
频繁分配释放AVFrame会带来内存碎片,可以预先分配固定数量的AVFrame循环使用:
cpp复制#define POOL_SIZE 10
AVFrame *framePool[POOL_SIZE];
// 初始化时
for(int i=0; i<POOL_SIZE; i++) {
framePool[i] = av_frame_alloc();
// ...初始化各帧参数...
}
// 使用时从池中获取可用帧
AVFrame* getFreeFrame() {
// 实现查找并返回空闲帧的逻辑
}
现象:录制的视频播放时卡顿
排查步骤:
cpp复制qDebug() << camera->supportedViewfinderFrameRates(settings);
解决方案:
原始YUV文件需要额外信息才能正确播放:
code复制width 640
height 480
fps 30
format yuv420p
bash复制ffplay -f rawvideo -pixel_format yuv420p -video_size 640x480 output.yuv
Linux系统特有注意事项:
bash复制sudo apt install v4l-utils
bash复制sudo usermod -a -G video $USER
qmake复制linux: LIBS += -lv4l2
在录制同时显示预览画面:
cpp复制// 创建Viewfinder
QVideoWidget *viewfinder = new QVideoWidget;
camera->setViewfinder(viewfinder);
viewfinder->show();
// 在processFrame中复制帧数据用于预览
QImage image(frame.bits(), frame.width(), frame.height(),
QVideoFrame::imageFormatFromPixelFormat(frame.pixelFormat()));
emit newPreviewImage(image); // 通过信号传递到UI线程
枚举所有可用摄像头:
cpp复制foreach(const QCameraInfo &cameraInfo, QCameraInfo::availableCameras()) {
qDebug() << "Camera:" << cameraInfo.description()
<< "Position:" << cameraInfo.position();
}
// 切换摄像头
camera->stop();
delete camera;
camera = new QCamera(selectedCameraInfo);
camera->start();
为每帧添加精确时间信息:
cpp复制// 在帧处理时记录时间
qint64 timestamp = QDateTime::currentMSecsSinceEpoch();
// 写入额外的元数据文件
QFile metaFile("timestamps.txt");
if(metaFile.open(QIODevice::Append)) {
QTextStream stream(&metaFile);
stream << timestamp << "\n";
}
在实际项目中,我发现正确处理YUV的内存布局是最容易出错的地方。特别是在不同平台上,内存对齐要求可能不同。建议在初始化完成后,先用静态测试图案验证录制功能,比如生成一个彩条测试图,确认YUV各分量正确存储后再接入真实摄像头信号。