FFmpeg - 视频采集程序
下面是一个完整的视频采集程序,使用 Qt 作为界面框架,FFmpeg 4.0 进行视频编码和录制。
VideoCaptureApp.pro
QT += core gui multimedia multimediawidgets
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
win32 {
LIBS += -L$$PWD/lib/SDL2/lib/x64 \
-L$$PWD/lib/ffmpeg-4.2.1/lib/x64 \
-lSDL2 \
-lavcodec \
-lavdevice \
-lavfilter \
-lavformat \
-lavutil \
-lswresample \
-lswscale
INCLUDEPATH += src \
lib/SDL2/include \
lib/ffmpeg-4.2.1/include_x64
}
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
SOURCES += \
videorecorder.cpp \
main.cpp \
mainwindow.cpp
HEADERS += \
videorecorder.h \
mainwindow.h
FORMS += \
mainwindow.ui
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target
videorecorder.h
#ifndef VIDEORECORDER_H
#define VIDEORECORDER_H
#include <QThread>
#include <QMutex>
// FFmpeg 头文件
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavdevice/avdevice.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
}
// 分辨率结构体
struct Resolution {
int width;
int height;
QString name;
Resolution(int w = 0, int h = 0, const QString &n = "")
: width(w), height(h), name(n) {}
};
// 注册 Resolution 类型到 Qt 的元对象系统
Q_DECLARE_METATYPE(Resolution)
// 录制线程类
class VideoRecorder : public QThread
{
Q_OBJECT
public:
explicit VideoRecorder(QObject *parent = nullptr);
~VideoRecorder();
bool initialize(const QString &outputFile, int width, int height, int fps);
void stopRecording();
void addFrame(const QImage &image);
signals:
void errorOccurred(const QString &error);
void recordingStatusChanged(const QString &status);
protected:
void run() override;
private:
bool setupOutput(const QString &outputFile);
bool encodeFrame(const QImage &image);
void cleanup();
// FFmpeg 相关变量
AVFormatContext *outputFormatContext;
AVCodecContext *videoCodecContext;
AVStream *videoStream;
SwsContext *swsContext;
AVFrame *frame;
AVPacket *packet;
int64_t frameCount;
// 录制参数
QString outputFilename;
int frameWidth;
int frameHeight;
int frameRate;
// 状态控制
QMutex mutex;
bool recording;
bool initialized;
// 帧队列
QList<QImage> frameQueue;
QMutex queueMutex;
};
#endif // VIDEORECORDER_H
videorecorder.cpp
#include "videorecorder.h"
#include <QDebug>
#include <QFileDialog>
// FFmpeg 错误处理辅助函数
QString ffmpegErrorString(int errnum)
{
char errbuf[AV_ERROR_MAX_STRING_SIZE];
av_strerror(errnum, errbuf, sizeof(errbuf));
return QString(errbuf);
}
// VideoRecorder 实现
VideoRecorder::VideoRecorder(QObject *parent)
: QThread(parent), outputFormatContext(nullptr),
videoCodecContext(nullptr), videoStream(nullptr), swsContext(nullptr),
frame(nullptr), packet(nullptr), frameCount(0),
frameWidth(640), frameHeight(480), frameRate(30),
recording(false), initialized(false)
{
// 注册所有 FFmpeg 组件
avdevice_register_all();
avformat_network_init();
}
VideoRecorder::~VideoRecorder()
{
stopRecording();
if (isRunning()) {
wait(3000); // 等待线程结束,最多3秒
}
cleanup();
}
bool VideoRecorder::initialize(const QString &outputFile,
int width, int height, int fps)
{
frameWidth = width;
frameHeight = height;
frameRate = fps;
outputFilename = outputFile;
// 只设置输出(文件),不设置输入(摄像头)
if (!setupOutput(outputFile)) {
emit errorOccurred("无法初始化输出文件");
return false;
}
initialized = true;
return true;
}
bool VideoRecorder::setupOutput(const QString &outputFile)
{
int ret;
// 创建输出格式上下文
ret = avformat_alloc_output_context2(&outputFormatContext, nullptr, nullptr, outputFile.toUtf8().constData());
if (ret < 0) {
emit errorOccurred(QString("无法创建输出上下文: %1").arg(ffmpegErrorString(ret)));
return false;
}
// 查找 H.264 编码器
AVCodec *videoCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!videoCodec) {
emit errorOccurred("找不到 H.264 编码器");
return false;
}
// 创建视频流
videoStream = avformat_new_stream(outputFormatContext, videoCodec);
if (!videoStream) {
emit errorOccurred("无法创建视频流");
return false;
}
// 配置编码器上下文
videoCodecContext = avcodec_alloc_context3(videoCodec);
if (!videoCodecContext) {
emit errorOccurred("无法分配编码器上下文");
return false;
}
videoCodecContext->codec_id = AV_CODEC_ID_H264;
videoCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
videoCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;
videoCodecContext->width = frameWidth;
videoCodecContext->height = frameHeight;
videoCodecContext->time_base = {1, frameRate};
videoCodecContext->framerate = {frameRate, 1};
videoCodecContext->gop_size = 12;
videoCodecContext->max_b_frames = 1;
videoCodecContext->bit_rate = 400000; // 400kbps
// 根据分辨率调整比特率
if (frameWidth >= 1920) {
videoCodecContext->bit_rate = 2000000; // 1080p 使用 2Mbps
} else if (frameWidth >= 1280) {
videoCodecContext->bit_rate = 1000000; // 720p 使用 1Mbps
} else {
videoCodecContext->bit_rate = 400000; // 其他分辨率使用 400kbps
}
// 设置编码器预设
if (outputFormatContext->oformat->flags & AVFMT_GLOBALHEADER) {
videoCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
AVDictionary *codecOptions = nullptr;
av_dict_set(&codecOptions, "preset", "medium", 0);
av_dict_set(&codecOptions, "crf", "23", 0);
// 打开编码器
ret = avcodec_open2(videoCodecContext, videoCodec, &codecOptions);
if (ret < 0) {
emit errorOccurred(QString("无法打开视频编码器: %1").arg(ffmpegErrorString(ret)));
return false;
}
// 复制编码器参数到流
ret = avcodec_parameters_from_context(videoStream->codecpar, videoCodecContext);
if (ret < 0) {
emit errorOccurred(QString("无法复制编码器参数: %1").arg(ffmpegErrorString(ret)));
return false;
}
// 打开输出文件
if (!(outputFormatContext->oformat->flags & AVFMT_NOFILE)) {
ret = avio_open(&outputFormatContext->pb, outputFile.toUtf8().constData(), AVIO_FLAG_WRITE);
if (ret < 0) {
emit errorOccurred(QString("无法打开输出文件: %1").arg(ffmpegErrorString(ret)));
return false;
}
}
// 写入文件头
ret = avformat_write_header(outputFormatContext, nullptr);
if (ret < 0) {
emit errorOccurred(QString("无法写入文件头: %1").arg(ffmpegErrorString(ret)));
return false;
}
// 分配帧和包
frame = av_frame_alloc();
packet = av_packet_alloc();
if (!frame || !packet) {
emit errorOccurred("无法分配帧或包");
return false;
}
frame->format = videoCodecContext->pix_fmt;
frame->width = videoCodecContext->width;
frame->height = videoCodecContext->height;
ret = av_frame_get_buffer(frame, 32);
if (ret < 0) {
emit errorOccurred(QString("无法分配帧缓冲区: %1").arg(ffmpegErrorString(ret)));
return false;
}
// 创建图像转换上下文
swsContext = sws_getContext(frameWidth, frameHeight, AV_PIX_FMT_RGB24,
frameWidth, frameHeight, AV_PIX_FMT_YUV420P,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!swsContext) {
emit errorOccurred("无法创建图像转换上下文");
return false;
}
return true;
}
void VideoRecorder::stopRecording()
{
QMutexLocker locker(&mutex);
recording = false;
}
void VideoRecorder::addFrame(const QImage &image)
{
QMutexLocker locker(&queueMutex);
if (frameQueue.size() < 30) { // 限制队列大小,防止内存溢出
// 调整图像大小以匹配编码器设置
QImage scaledImage = image.scaled(frameWidth, frameHeight, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
frameQueue.append(scaledImage.copy()); // 复制图像,避免原始图像被修改
}
}
void VideoRecorder::run()
{
if (!initialized) {
emit errorOccurred("录制器未初始化");
return;
}
recording = true;
frameCount = 0;
emit recordingStatusChanged("开始录制");
// 计算帧间隔(毫秒)
int frameInterval = 1000 / frameRate;
while (recording) {
auto startTime = std::chrono::steady_clock::now();
// 从队列获取帧
QImage currentFrame;
{
QMutexLocker locker(&queueMutex);
if (!frameQueue.isEmpty()) {
currentFrame = frameQueue.takeFirst();
}
}
if (!currentFrame.isNull()) {
if (!encodeFrame(currentFrame)) {
emit errorOccurred("编码帧失败");
break;
}
frameCount++;
// 更新状态
if (frameCount % 30 == 0) { // 每30帧更新一次状态
emit recordingStatusChanged(QString("正在录制... 已录制 %1 帧").arg(frameCount));
}
}
// 控制帧率
auto endTime = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
if (elapsed < frameInterval) {
msleep(frameInterval - elapsed);
}
}
// 刷新编码器
encodeFrame(QImage());
// 写入文件尾
if (outputFormatContext) {
av_write_trailer(outputFormatContext);
}
emit recordingStatusChanged(QString("录制完成,共 %1 帧").arg(frameCount));
}
bool VideoRecorder::encodeFrame(const QImage &image)
{
int ret;
if (!image.isNull()) {
// 转换图像格式为 RGB888
QImage rgbImage = image.convertToFormat(QImage::Format_RGB888);
// 准备源数据
const uint8_t *srcData[1] = { rgbImage.bits() };
int srcLinesize[1] = { rgbImage.bytesPerLine() };
// 转换图像格式
sws_scale(swsContext, srcData, srcLinesize, 0, frameHeight,
frame->data, frame->linesize);
frame->pts = frameCount;
}
// 发送帧到编码器
ret = avcodec_send_frame(videoCodecContext, image.isNull() ? nullptr : frame);
if (ret < 0) {
qDebug() << "发送帧到编码器失败:" << ffmpegErrorString(ret);
return false;
}
// 接收编码后的包
while (ret >= 0) {
ret = avcodec_receive_packet(videoCodecContext, packet);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else if (ret < 0) {
qDebug() << "接收编码包失败:" << ffmpegErrorString(ret);
return false;
}
// 调整时间戳
av_packet_rescale_ts(packet, videoCodecContext->time_base, videoStream->time_base);
packet->stream_index = videoStream->index;
// 写入包
ret = av_interleaved_write_frame(outputFormatContext, packet);
av_packet_unref(packet);
if (ret < 0) {
qDebug() << "写入包失败:" << ffmpegErrorString(ret);
return false;
}
}
return true;
}
void VideoRecorder::cleanup()
{
if (swsContext) {
sws_freeContext(swsContext);
swsContext = nullptr;
}
if (frame) {
av_frame_free(&frame);
frame = nullptr;
}
if (packet) {
av_packet_free(&packet);
packet = nullptr;
}
if (videoCodecContext) {
avcodec_free_context(&videoCodecContext);
videoCodecContext = nullptr;
}
if (outputFormatContext && !(outputFormatContext->oformat->flags & AVFMT_NOFILE)) {
avio_closep(&outputFormatContext->pb);
}
if (outputFormatContext) {
avformat_free_context(outputFormatContext);
outputFormatContext = nullptr;
}
// 清空帧队列
{
QMutexLocker locker(&queueMutex);
frameQueue.clear();
}
initialized = false;
}
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QCamera>
#include <QCameraViewfinder>
#include <QCameraInfo>
#include <QVBoxLayout>
#include <QPushButton>
#include <QComboBox>
#include <QMessageBox>
#include <QHBoxLayout>
#include <QFileDialog>
#include <QDateTime>
#include <QLabel>
#include <QTimer>
#include <chrono>
#include "videorecorder.h"
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
~MainWindow();
bool checkCameraAvailability();
private slots:
void startCamera();
void stopCamera();
void startRecording();
void stopRecording();
void takeScreenshot();
void cameraError(QCamera::Error error);
void updateRecordTime();
void captureFrame();
void onRecordingError(const QString &error);
void onRecordingStatusChanged(const QString &status);
void onResolutionChanged(int index);
private:
Ui::MainWindow *ui;
void setupUI();
void populateCameras();
void populateResolutions();
QString getOutputFileName();
Resolution getSelectedResolution();
QCamera *camera;
QCameraViewfinder *viewfinder;
QTimer *recordTimer;
QTimer *frameCaptureTimer;
qint64 recordDuration;
// FFmpeg 录制器
VideoRecorder *recorder;
bool isRecording;
// 分辨率列表
QList<Resolution> resolutions;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QMetaType>
#include <QStandardPaths>
#include <QDir>
#include <QDebug>
#include <QHBoxLayout>
#include <QFileDialog>
#include <QDateTime>
#include <QImageWriter>
// MainWindow 实现
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(new Ui::MainWindow),
camera(nullptr), viewfinder(nullptr), // 明确初始化为 nullptr
recordDuration(0), recorder(nullptr), isRecording(false)
{
qDebug() << "=== MainWindow Constructor ===";
// 在构造函数开始时注册 Resolution 类型
qRegisterMetaType<Resolution>();
ui->setupUi(this);
qDebug() << "UI setup completed";
// 初始化定时器
recordTimer = new QTimer(this);
frameCaptureTimer = new QTimer(this);
qDebug() << "Timers created";
setupUI();
qDebug() << "UI setup function called";
// 检查摄像头可用性
qDebug() << "=== Camera Availability Check ===";
checkCameraAvailability();
populateCameras();
qDebug() << "Cameras populated";
populateResolutions();
qDebug() << "Resolutions populated";
// 初始化 FFmpeg 录制器
recorder = new FFmpegRecorder(this);
connect(recorder, &FFmpegRecorder::errorOccurred, this, &MainWindow::onRecordingError);
connect(recorder, &FFmpegRecorder::recordingStatusChanged, this, &MainWindow::onRecordingStatusChanged);
qDebug() << "FFmpeg recorder initialized";
qDebug() << "=== MainWindow Initialized Successfully ===";
}
MainWindow::~MainWindow()
{
stopRecording();
stopCamera();
delete ui;
}
bool MainWindow::checkCameraAvailability()
{
QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
if (cameras.isEmpty()) {
qDebug() << "No cameras found";
return false;
}
qDebug() << "Available cameras:";
for (const QCameraInfo &cameraInfo : cameras) {
qDebug() << " -" << cameraInfo.description() << "Device:" << cameraInfo.deviceName();
// 检查摄像头是否可用
QCamera testCamera(cameraInfo);
if (testCamera.error() != QCamera::NoError) {
qDebug() << " Camera error:" << testCamera.error();
} else {
qDebug() << " Camera seems available";
}
}
return true;
}
void MainWindow::setupUI()
{
// UI 元素已经在 .ui 文件中定义,这里进行连接
connect(ui->startButton, &QPushButton::clicked, this, &MainWindow::startCamera);
connect(ui->stopButton, &QPushButton::clicked, this, &MainWindow::stopCamera);
connect(ui->recordButton, &QPushButton::clicked, this, &MainWindow::startRecording);
connect(ui->stopRecordButton, &QPushButton::clicked, this, &MainWindow::stopRecording);
connect(ui->screenshotButton, &QPushButton::clicked, this, &MainWindow::takeScreenshot);
connect(ui->resolutionComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &MainWindow::onResolutionChanged);
// 初始化按钮状态
ui->stopButton->setEnabled(false);
ui->recordButton->setEnabled(false);
ui->stopRecordButton->setEnabled(false);
// 创建录制计时器
recordTimer = new QTimer(this);
connect(recordTimer, &QTimer::timeout, this, &MainWindow::updateRecordTime);
// 创建帧捕获定时器
frameCaptureTimer = new QTimer(this);
connect(frameCaptureTimer, &QTimer::timeout, this, &MainWindow::captureFrame);
setWindowTitle("视频采集与录制程序 (FFmpeg API)");
resize(800, 600);
}
void MainWindow::populateCameras()
{
ui->cameraComboBox->clear();
QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
if (cameras.isEmpty()) {
ui->cameraComboBox->addItem("未找到摄像头");
ui->startButton->setEnabled(false);
return;
}
for (const QCameraInfo &cameraInfo : cameras) {
ui->cameraComboBox->addItem(cameraInfo.description(), cameraInfo.deviceName());
}
}
void MainWindow::populateResolutions()
{
// 清空分辨率列表
resolutions.clear();
ui->resolutionComboBox->clear();
// 添加常见分辨率选项
resolutions.append(Resolution(1920, 1080, "1080p (1920x1080)"));
resolutions.append(Resolution(1280, 720, "720p (1280x720)"));
resolutions.append(Resolution(1024, 768, "XGA (1024x768)"));
resolutions.append(Resolution(800, 600, "SVGA (800x600)"));
resolutions.append(Resolution(640, 480, "VGA (640x480)"));
resolutions.append(Resolution(320, 240, "QVGA (320x240)"));
// 添加到下拉框
for (const Resolution &res : resolutions) {
ui->resolutionComboBox->addItem(res.name, QVariant::fromValue(res));
}
// 默认选择 720p
ui->resolutionComboBox->setCurrentIndex(1);
}
void MainWindow::startCamera()
{
qDebug() << "=== Starting Camera ===";
try {
if (ui->cameraComboBox->currentData().isNull()) {
QMessageBox::warning(this, "警告", "没有可用的摄像头");
return;
}
// 先停止当前摄像头
stopCamera();
QString deviceName = ui->cameraComboBox->currentData().toString();
QString cameraDescription = ui->cameraComboBox->currentText();
qDebug() << "Using camera:" << cameraDescription << "Device:" << deviceName;
// 创建摄像头对象
camera = new QCamera(deviceName.toUtf8(), this);
if (!camera) {
throw std::runtime_error("Failed to allocate camera object");
}
qDebug() << "Camera object created";
// 连接错误信号
connect(camera, QOverload<QCamera::Error>::of(&QCamera::error),
this, &MainWindow::cameraError);
// 创建视图
viewfinder = new QCameraViewfinder(this);
if (!viewfinder) {
throw std::runtime_error("Failed to allocate viewfinder");
}
viewfinder->setMinimumSize(640, 480);
qDebug() << "Viewfinder created";
// 简化布局操作 - 直接添加到最后
ui->verticalLayout->addWidget(viewfinder);
qDebug() << "Viewfinder added to layout";
// 设置视图并启动摄像头
camera->setViewfinder(viewfinder);
qDebug() << "Viewfinder set for camera";
camera->start();
qDebug() << "Camera start command issued";
// 更新按钮状态
ui->startButton->setEnabled(false);
ui->stopButton->setEnabled(true);
ui->recordButton->setEnabled(true);
ui->statusLabel->setText("摄像头已启动");
qDebug() << "=== Camera Started Successfully ===";
} catch (const std::exception& e) {
qDebug() << "Exception in startCamera:" << e.what();
QMessageBox::critical(this, "错误", QString("启动摄像头失败: %1").arg(e.what()));
stopCamera();
}
}
void MainWindow::stopCamera()
{
qDebug() << "=== Stopping Camera ===";
// 先停止录制
stopRecording();
qDebug() << "Stopping camera components...";
// 先停止摄像头
if (camera) {
qDebug() << "Stopping camera object...";
try {
// 先断开所有信号连接
disconnect(camera, nullptr, this, nullptr);
// 停止摄像头
if (camera->state() != QCamera::UnloadedState) {
camera->stop();
qDebug() << "Camera stopped";
}
// 使用 deleteLater 而不是直接 delete
camera->deleteLater();
camera = nullptr;
qDebug() << "Camera scheduled for deletion";
} catch (const std::exception& e) {
qDebug() << "Error stopping camera:" << e.what();
}
}
// 处理视图
if (viewfinder) {
qDebug() << "Removing viewfinder...";
try {
// 先从布局中移除
if (ui->verticalLayout) {
// 安全地从布局中移除部件
QLayoutItem* item = nullptr;
for (int i = 0; i < ui->verticalLayout->count(); ++i) {
item = ui->verticalLayout->itemAt(i);
if (item && item->widget() == viewfinder) {
ui->verticalLayout->removeItem(item);
delete item;
qDebug() << "Viewfinder removed from layout";
break;
}
}
}
// 隐藏并删除视图
viewfinder->hide();
viewfinder->deleteLater();
viewfinder = nullptr;
qDebug() << "Viewfinder scheduled for deletion";
} catch (const std::exception& e) {
qDebug() << "Error removing viewfinder:" << e.what();
}
}
// 更新按钮状态
ui->startButton->setEnabled(true);
ui->stopButton->setEnabled(false);
ui->recordButton->setEnabled(false);
ui->stopRecordButton->setEnabled(false);
ui->statusLabel->setText("摄像头已停止");
qDebug() << "=== Camera Stopped Successfully ===";
}
void MainWindow::startRecording()
{
if (!camera) {
QMessageBox::warning(this, "警告", "请先启动摄像头");
return;
}
if (isRecording) {
QMessageBox::information(this, "提示", "已经在录制中");
return;
}
QString fileName = getOutputFileName();
if (fileName.isEmpty()) {
return;
}
// 获取选中的分辨率
Resolution selectedRes = getSelectedResolution();
// 初始化录制器
if (!recorder->initialize(fileName, selectedRes.width, selectedRes.height, 30)) {
QMessageBox::critical(this, "错误", "初始化录制器失败");
return;
}
// 开始录制
recorder->start();
isRecording = true;
ui->recordButton->setEnabled(false);
ui->stopRecordButton->setEnabled(true);
recordDuration = 0;
recordTimer->start(1000);
frameCaptureTimer->start(33); // 30fps ≈ 33ms per frame
ui->statusLabel->setText(QString("正在录制... 分辨率: %1").arg(selectedRes.name));
qDebug() << "Recording started with resolution:" << selectedRes.name;
}
void MainWindow::stopRecording()
{
if (isRecording && recorder) {
recorder->stopRecording();
frameCaptureTimer->stop();
// 等待录制线程结束
if (recorder->isRunning()) {
recorder->wait(3000);
}
recordTimer->stop();
ui->recordButton->setEnabled(true);
ui->stopRecordButton->setEnabled(false);
isRecording = false;
qDebug() << "Recording stopped";
}
}
void MainWindow::captureFrame()
{
// 捕获当前帧并添加到录制队列
if (isRecording && viewfinder) {
QImage frame = viewfinder->grab().toImage();
if (!frame.isNull()) {
recorder->addFrame(frame);
}
}
}
void MainWindow::takeScreenshot()
{
if (!camera || !viewfinder) {
QMessageBox::warning(this, "警告", "请先启动摄像头");
return;
}
QString picturesDir = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation);
if (picturesDir.isEmpty()) {
picturesDir = QDir::currentPath();
}
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss");
QString defaultName = QString("%1/screenshot_%2.png").arg(picturesDir).arg(timestamp);
QString fileName = QFileDialog::getSaveFileName(this, "保存截图", defaultName,
"图片文件 (*.png *.jpg *.bmp)");
if (fileName.isEmpty()) {
return;
}
QPixmap screenshot = viewfinder->grab();
if (screenshot.isNull()) {
QMessageBox::warning(this, "警告", "截图失败");
return;
}
QString format = "PNG";
if (fileName.endsWith(".jpg", Qt::CaseInsensitive) || fileName.endsWith(".jpeg", Qt::CaseInsensitive)) {
format = "JPG";
} else if (fileName.endsWith(".bmp", Qt::CaseInsensitive)) {
format = "BMP";
}
if (screenshot.save(fileName, format.toUtf8().constData())) {
ui->statusLabel->setText(QString("截图已保存: %1").arg(QFileInfo(fileName).fileName()));
qDebug() << "Screenshot saved to:" << fileName;
} else {
QMessageBox::warning(this, "警告", "保存截图失败");
}
}
void MainWindow::cameraError(QCamera::Error error)
{
qDebug() << "Camera error:" << error;
QMessageBox::critical(this, "摄像头错误", QString("摄像头发生错误: %1").arg(error));
stopCamera();
}
void MainWindow::updateRecordTime()
{
recordDuration++;
Resolution selectedRes = getSelectedResolution();
ui->statusLabel->setText(QString("正在录制... 时长: %1秒 分辨率: %2").arg(recordDuration).arg(selectedRes.name));
}
void MainWindow::onRecordingError(const QString &error)
{
QMessageBox::critical(this, "录制错误", error);
stopRecording();
}
void MainWindow::onRecordingStatusChanged(const QString &status)
{
ui->statusLabel->setText(status);
qDebug() << "Recording status:" << status;
}
void MainWindow::onResolutionChanged(int index)
{
if (index >= 0 && index < resolutions.size()) {
Resolution res = resolutions.at(index);
qDebug() << "Resolution changed to:" << res.name;
// 如果正在录制,更新状态显示
if (isRecording) {
ui->statusLabel->setText(QString("正在录制... 分辨率: %1").arg(res.name));
}
}
}
Resolution MainWindow::getSelectedResolution()
{
int index = ui->resolutionComboBox->currentIndex();
if (index >= 0 && index < resolutions.size()) {
// 使用 QVariant::value<Resolution>() 获取存储的 Resolution 对象
return ui->resolutionComboBox->itemData(index).value<Resolution>();
}
// 默认返回 720p
return Resolution(1280, 720, "720p (1280x720)");
}
QString MainWindow::getOutputFileName()
{
QString videosDir = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
if (videosDir.isEmpty()) {
videosDir = QDir::currentPath();
}
QDir dir(videosDir);
if (!dir.exists()) {
dir.mkpath(".");
}
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd-hhmmss");
Resolution res = getSelectedResolution();
QString resolutionStr = QString("%1x%2").arg(res.width).arg(res.height);
QString defaultName = QString("%1/video_%2_%3.mp4").arg(videosDir).arg(timestamp).arg(resolutionStr);
QString fileName = QFileDialog::getSaveFileName(this, "保存视频", defaultName,
"MP4 文件 (*.mp4);;所有文件 (*)");
if (!fileName.isEmpty() && !fileName.endsWith(".mp4", Qt::CaseInsensitive)) {
fileName += ".mp4";
}
return fileName;
}
main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
mainwindow.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>视频采集与录制程序</string>
</property>
<widget class="QWidget" name="centralWidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>摄像头:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="cameraComboBox"/>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>分辨率:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="resolutionComboBox">
<property name="toolTip">
<string>选择录制分辨率</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="startButton">
<property name="text">
<string>开始采集</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopButton">
<property name="text">
<string>停止采集</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="recordButton">
<property name="text">
<string>开始录制</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="stopRecordButton">
<property name="text">
<string>停止录制</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="screenshotButton">
<property name="text">
<string>截图</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="statusLabel">
<property name="text">
<string>就绪</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections/>
</ui>
主要特点
-
直接使用 FFmpeg API:不通过命令行调用,直接使用 libavcodec、libavformat 等库
-
多线程录制:在单独的线程中进行视频编码和写入,避免阻塞 UI
-
帧队列机制:使用队列管理待编码的帧,平衡生产和消费速度
-
实时预览:使用 Qt 的 QCameraViewfinder 进行实时预览
-
高质量编码:使用 H.264 编码,可配置编码参数
-
错误处理:完善的错误处理和状态反馈
使用说明
-
编译运行:确保 FFmpeg 4.2.1 库正确链接
-
选择摄像头:从下拉框选择要使用的摄像头
-
开始预览:点击"开始采集"启动摄像头预览
-
开始录制:点击"开始录制"选择保存位置并开始录制
-
停止录制:点击"停止录制"结束录制过程
-
截图功能:随时可以保存当前预览画面
这个实现提供了专业级的视频录制功能,直接使用 FFmpeg API 进行硬件加速编码,性能更好,控制更精细。


浙公网安备 33010602011771号