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>

 

主要特点

  1. 直接使用 FFmpeg API:不通过命令行调用,直接使用 libavcodec、libavformat 等库

  2. 多线程录制:在单独的线程中进行视频编码和写入,避免阻塞 UI

  3. 帧队列机制:使用队列管理待编码的帧,平衡生产和消费速度

  4. 实时预览:使用 Qt 的 QCameraViewfinder 进行实时预览

  5. 高质量编码:使用 H.264 编码,可配置编码参数

  6. 错误处理:完善的错误处理和状态反馈

使用说明

  1. 编译运行:确保 FFmpeg 4.2.1 库正确链接

  2. 选择摄像头:从下拉框选择要使用的摄像头

  3. 开始预览:点击"开始采集"启动摄像头预览

  4. 开始录制:点击"开始录制"选择保存位置并开始录制

  5. 停止录制:点击"停止录制"结束录制过程

  6. 截图功能:随时可以保存当前预览画面

这个实现提供了专业级的视频录制功能,直接使用 FFmpeg API 进行硬件加速编码,性能更好,控制更精细。

image

 

posted @ 2025-10-08 13:37  [BORUTO]  阅读(23)  评论(0)    收藏  举报