Qt - 视频采集程序

Qt 摄像头视频数据采集(编码数据)

使用了QMediaRecorder来录制视频。QMediaRecorder默认会使用编码器(如H.264)对视频进行编码,然后保存到文件(如MP4)。因此,这个程序保存的是已经编码的视频数据,而不是原始视频数据(如YUV)

QMediaRecorder是Qt Multimedia模块中的一个类,它用于录制音频和视频。在Qt中,QMediaRecorder本身并不直接包含编码器,而是依赖于底层的多媒体框架(如Windows上的DirectShow、Linux上的GStreamer等)来提供编码功能。

在程序中,通过以下步骤设置编码器:

  1. 在setupRecorder函数中,创建了QMediaRecorder对象,并设置了视频和音频编码设置。

  2. 通过recorder->supportedAudioCodecs()、recorder->supportedVideoCodecs()和recorder->supportedContainers()来获取系统支持的编码器和容器格式。

  3. 然后设置视频编码参数(如编码格式、质量、帧率、分辨率)和音频编码参数(如编码格式、质量、通道数、采样率、比特率)以及容器格式。

所以,这个程序使用的是Qt Multimedia模块中基于当前系统多媒体框架的编码器。Qt本身不提供编码器,而是调用系统支持的编码器。

具体代码在setupRecorder函数中:

text
QVideoEncoderSettings videoSettings;
// 尝试设置视频编码器
QStringList videoCodecs = recorder->supportedVideoCodecs();
if (videoCodecs.contains("video/mp4") || videoCodecs.contains("avc1")) {
    videoSettings.setCodec("video/mp4");
} else if (videoCodecs.contains("video/x-msvideo")) {
    videoSettings.setCodec("video/x-msvideo"); // AVI
} else if (!videoCodecs.isEmpty()) {
    videoSettings.setCodec(videoCodecs.first()); // 使用第一个支持的编码
}

// 同样,设置音频编码器
QAudioEncoderSettings audioSettings;
QStringList audioCodecs = recorder->supportedAudioCodecs();
if (!audioCodecs.isEmpty()) {
    if (audioCodecs.contains("audio/mpeg") || audioCodecs.contains("mp3")) {
        audioSettings.setCodec("audio/mpeg");
    } else {
        audioSettings.setCodec(audioCodecs.first());
    }
}

因此,这个程序使用的是系统提供的编码器,而不是Qt自带的编码器。Qt在这里扮演了一个封装和调用系统多媒体功能的作用。

注意:不同的平台和系统环境支持的多媒体编码器可能不同,因此程序会先检查系统支持的编码器,然后选择其中一个进行设置。

如果系统没有安装任何视频编码器,那么录制视频可能会失败。这也是为什么程序中有错误处理的原因。

所以,这个程序保存的视频数据是经过系统编码器编码后的数据(例如MP4格式),而不是原始YUV数据。

 

1. widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QCamera>
#include <QCameraInfo>
#include <QCameraViewfinder>
#include <QCameraImageCapture>
#include <QMediaRecorder>
#include <QComboBox>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGroupBox>
#include <QTimer>
#include <QTime>
#include <QAudioEncoderSettings>
#include <QVideoEncoderSettings>

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

public slots:
    void on_openCamera_clicked();

private slots:
    void on_closeCamera_clicked();
    void on_captureBt_clicked();
    void save_pic(int id, const QImage &preview);
    void onActivated(int index);
    void onResolutionChanged(int index);
    void on_recordButton_clicked();
    void updateRecordingTime();
    void onRecorderStateChanged(QMediaRecorder::State state);
    void onRecorderError(QMediaRecorder::Error error);

private:
    // UI 控件
    QComboBox *cameraComboBox;
    QComboBox *resolutionComboBox;
    QPushButton *openCameraButton;
    QPushButton *closeCameraButton;
    QPushButton *captureButton;
    QPushButton *recordButton;
    QLabel *imageLabel;
    QLabel *recordingTimeLabel;
    QLabel *statusLabel;
    QWidget *viewfinderContainer;
    
    // 布局
    QVBoxLayout *mainLayout;
    QHBoxLayout *controlLayout;
    QHBoxLayout *displayLayout;
    
    // 摄像头相关
    QList<QCameraInfo> cameraList;
    QCamera* myCamera;
    QCameraImageCapture* cp;
    QCameraViewfinder* vf;
    QMediaRecorder* recorder;
    QList<QSize> mResSize;
    int m_index;
    
    // 录制相关
    QTimer *recordingTimer;
    QTime recordingTime;
    bool isRecording;
    
    void initUI();
    void initResolutionComboBox();
    void setupRecorder();
    void showSupportedFormats();
};
#endif // WIDGET_H

2. widget.cpp

#include "widget.h"
#include <QDebug>
#include <QCameraViewfinderSettings>
#include <QDateTime>
#include <QMessageBox>
#include <QFileDialog>
#include <QStandardPaths>
#include <QAudioInput>
#include <QAudioDeviceInfo>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , cameraComboBox(nullptr)
    , resolutionComboBox(nullptr)
    , openCameraButton(nullptr)
    , closeCameraButton(nullptr)
    , captureButton(nullptr)
    , recordButton(nullptr)
    , imageLabel(nullptr)
    , recordingTimeLabel(nullptr)
    , statusLabel(nullptr)
    , viewfinderContainer(nullptr)
    , mainLayout(nullptr)
    , controlLayout(nullptr)
    , displayLayout(nullptr)
    , myCamera(nullptr)
    , cp(nullptr)
    , vf(nullptr)
    , recorder(nullptr)
    , m_index(0)
    , recordingTimer(nullptr)
    , isRecording(false)
{
    // 初始化分辨率列表
    mResSize << QSize(160, 120)   // QQVGA
             << QSize(320, 240)   // QVGA
             << QSize(640, 480)   // VGA
             << QSize(800, 600)   // SVGA
             << QSize(1024, 768)  // XGA
             << QSize(1280, 720)  // 720p HD
             << QSize(1920, 1080) // 1080p Full HD
             << QSize(3840, 2160); // 4K UHD
    
    initUI();
}

Widget::~Widget()
{
    if (myCamera) {
        myCamera->stop();
    }
    if (recordingTimer) {
        recordingTimer->stop();
    }
}

void Widget::initUI()
{
    // 创建控件
    cameraComboBox = new QComboBox(this);
    resolutionComboBox = new QComboBox(this);
    openCameraButton = new QPushButton("打开摄像头", this);
    closeCameraButton = new QPushButton("关闭摄像头", this);
    captureButton = new QPushButton("拍照", this);
    recordButton = new QPushButton("开始录制", this);
    imageLabel = new QLabel("拍照预览", this);
    recordingTimeLabel = new QLabel("", this);
    statusLabel = new QLabel("就绪", this);
    viewfinderContainer = new QWidget(this);
    
    // 设置控件属性
    viewfinderContainer->setMinimumSize(640, 480);
    viewfinderContainer->setStyleSheet("background-color: black; border: 1px solid gray;");
    
    imageLabel->setMinimumSize(320, 240);
    imageLabel->setStyleSheet("background-color: white; border: 1px solid gray;");
    imageLabel->setAlignment(Qt::AlignCenter);
    
    openCameraButton->setMinimumHeight(30);
    closeCameraButton->setMinimumHeight(30);
    captureButton->setMinimumHeight(30);
    recordButton->setMinimumHeight(30);
    
    recordingTimeLabel->setStyleSheet("color: red; font-weight: bold;");
    recordingTimeLabel->setAlignment(Qt::AlignCenter);
    recordingTimeLabel->hide(); // 初始隐藏
    
    statusLabel->setStyleSheet("color: blue; font-weight: bold;");
    statusLabel->setAlignment(Qt::AlignCenter);
    
    // 初始时禁用录制按钮
    recordButton->setEnabled(false);
    
    // 初始化分辨率下拉框
    initResolutionComboBox();
    
    // 获取摄像头列表
    cameraList = QCameraInfo::availableCameras();
    if (cameraList.count() > 0) {
        foreach(QCameraInfo info, cameraList) {
            cameraComboBox->addItem(info.description());
            qDebug() << "Camera:" << info.description() << info.deviceName();
        }
    } else {
        statusLabel->setText("未检测到摄像头");
    }
    
    // 创建布局
    mainLayout = new QVBoxLayout(this);
    
    // 控制区域布局
    QGroupBox *controlGroup = new QGroupBox("摄像头控制", this);
    controlLayout = new QHBoxLayout(controlGroup);
    
    controlLayout->addWidget(new QLabel("摄像头:", this));
    controlLayout->addWidget(cameraComboBox);
    controlLayout->addWidget(new QLabel("分辨率:", this));
    controlLayout->addWidget(resolutionComboBox);
    controlLayout->addWidget(openCameraButton);
    controlLayout->addWidget(closeCameraButton);
    controlLayout->addWidget(captureButton);
    controlLayout->addWidget(recordButton);
    controlLayout->addStretch(1); // 添加弹性空间
    
    // 状态栏
    QHBoxLayout *statusLayout = new QHBoxLayout();
    statusLayout->addWidget(new QLabel("状态:", this));
    statusLayout->addWidget(statusLabel);
    statusLayout->addStretch(1);
    
    // 显示区域布局
    QGroupBox *displayGroup = new QGroupBox("预览窗口", this);
    displayLayout = new QHBoxLayout(displayGroup);
    
    // 左侧摄像头预览区域
    QVBoxLayout *cameraLayout = new QVBoxLayout();
    cameraLayout->addWidget(viewfinderContainer, 1);
    cameraLayout->addWidget(recordingTimeLabel);
    
    // 右侧布局
    QVBoxLayout *rightLayout = new QVBoxLayout();
    rightLayout->addWidget(new QLabel("拍照预览:", this));
    rightLayout->addWidget(imageLabel);
    
    displayLayout->addLayout(cameraLayout, 2); // 2/3的空间给摄像头预览
    displayLayout->addLayout(rightLayout, 1); // 1/3的空间给拍照预览
    
    // 主布局
    mainLayout->addWidget(controlGroup);
    mainLayout->addLayout(statusLayout);
    mainLayout->addWidget(displayGroup);
    
    // 设置窗口属性
    setWindowTitle("摄像头应用");
    setMinimumSize(800, 600);
    
    // 连接信号和槽
    connect(cameraComboBox, SIGNAL(activated(int)), this, SLOT(onActivated(int)));
    connect(resolutionComboBox, SIGNAL(activated(int)), this, SLOT(onResolutionChanged(int)));
    connect(openCameraButton, SIGNAL(clicked()), this, SLOT(on_openCamera_clicked()));
    connect(closeCameraButton, SIGNAL(clicked()), this, SLOT(on_closeCamera_clicked()));
    connect(captureButton, SIGNAL(clicked()), this, SLOT(on_captureBt_clicked()));
    connect(recordButton, SIGNAL(clicked()), this, SLOT(on_recordButton_clicked()));
    
    // 初始化录制计时器
    recordingTimer = new QTimer(this);
    connect(recordingTimer, &QTimer::timeout, this, &Widget::updateRecordingTime);
}

void Widget::initResolutionComboBox()
{
    resolutionComboBox->clear();
    
    foreach (QSize size, mResSize) {
        QString resolution = QString("%1 x %2").arg(size.width()).arg(size.height());
        resolutionComboBox->addItem(resolution);
    }
    
    // 默认选择 VGA 分辨率 (640x480)
    resolutionComboBox->setCurrentIndex(2);
}

void Widget::showSupportedFormats()
{
    if (!recorder) return;
    
    qDebug() << "Supported audio codecs:" << recorder->supportedAudioCodecs();
    qDebug() << "Supported video codecs:" << recorder->supportedVideoCodecs();
    qDebug() << "Supported containers:" << recorder->supportedContainers();
}

void Widget::setupRecorder()
{
    if (!myCamera) return;
    
    // 创建录制器
    recorder = new QMediaRecorder(myCamera, this);
    
    // 连接录制器信号
    connect(recorder, &QMediaRecorder::stateChanged, this, &Widget::onRecorderStateChanged);
    connect(recorder, QOverload<QMediaRecorder::Error>::of(&QMediaRecorder::error), 
            this, &Widget::onRecorderError);
    
    // 显示支持的格式(用于调试)
    showSupportedFormats();
    
    // 设置视频编码
    QVideoEncoderSettings videoSettings;
    
    // 尝试多种视频编码格式
    QStringList videoCodecs = recorder->supportedVideoCodecs();
    if (videoCodecs.contains("video/mp4") || videoCodecs.contains("avc1")) {
        videoSettings.setCodec("video/mp4");
    } else if (videoCodecs.contains("video/x-msvideo")) {
        videoSettings.setCodec("video/x-msvideo"); // AVI
    } else if (!videoCodecs.isEmpty()) {
        videoSettings.setCodec(videoCodecs.first()); // 使用第一个支持的编码
    }
    
    videoSettings.setQuality(QMultimedia::NormalQuality);
    videoSettings.setFrameRate(30.0);
    
    // 根据选择的分辨率设置录制分辨率
    int resolutionIndex = resolutionComboBox->currentIndex();
    if (resolutionIndex >= 0 && resolutionIndex < mResSize.size()) {
        QSize selectedSize = mResSize[resolutionIndex];
        videoSettings.setResolution(selectedSize);
        qDebug() << "Setting video resolution to:" << selectedSize;
    }
    
    // 设置音频编码
    QAudioEncoderSettings audioSettings;
    QStringList audioCodecs = recorder->supportedAudioCodecs();
    if (!audioCodecs.isEmpty()) {
        if (audioCodecs.contains("audio/mpeg") || audioCodecs.contains("mp3")) {
            audioSettings.setCodec("audio/mpeg");
        } else {
            audioSettings.setCodec(audioCodecs.first());
        }
        audioSettings.setQuality(QMultimedia::NormalQuality);
        audioSettings.setChannelCount(2);
        audioSettings.setSampleRate(44100);
        audioSettings.setBitRate(128000);
    }
    
    // 设置容器格式
    QStringList containers = recorder->supportedContainers();
    if (containers.contains("mp4") || containers.contains("video/mp4")) {
        recorder->setContainerFormat("mp4");
    } else if (containers.contains("avi")) {
        recorder->setContainerFormat("avi");
    } else if (!containers.isEmpty()) {
        recorder->setContainerFormat(containers.first());
    }
    
    recorder->setEncodingSettings(audioSettings, videoSettings);
    
    qDebug() << "Recorder setup complete";
    qDebug() << "Audio codec:" << audioSettings.codec();
    qDebug() << "Video codec:" << videoSettings.codec();
    qDebug() << "Container:" << recorder->containerFormat();
}

void Widget::on_openCamera_clicked()
{
    // 检查摄像头列表是否为空
    if (cameraList.isEmpty() || m_index < 0 || m_index >= cameraList.count()) {
        qDebug() << "No camera available or invalid index";
        statusLabel->setText("错误: 没有可用的摄像头");
        QMessageBox::warning(this, "警告", "没有可用的摄像头或索引无效");
        return;
    }
    
    // 如果已存在摄像头对象,先清理
    if (myCamera) {
        myCamera->stop();
        delete myCamera;
        myCamera = nullptr;
    }
    
    // 创建新的摄像头对象
    myCamera = new QCamera(cameraList[m_index], this);
    
    // 检查摄像头状态
    if (myCamera->error() != QCamera::NoError) {
        qDebug() << "Camera error:" << myCamera->errorString();
        statusLabel->setText("错误: " + myCamera->errorString());
        QMessageBox::critical(this, "错误", QString("摄像头错误: %1").arg(myCamera->errorString()));
        delete myCamera;
        myCamera = nullptr;
        return;
    }
    
    // 设置分辨率
    int resolutionIndex = resolutionComboBox->currentIndex();
    if (resolutionIndex >= 0 && resolutionIndex < mResSize.size()) {
        QSize selectedSize = mResSize[resolutionIndex];
        QCameraViewfinderSettings settings;
        settings.setResolution(selectedSize);
        myCamera->setViewfinderSettings(settings);
        qDebug() << "Set resolution to:" << selectedSize;
    }
    
    // 设置录制器
    setupRecorder();
    
    // 创建图像捕获对象
    cp = new QCameraImageCapture(myCamera, this);
    connect(cp, &QCameraImageCapture::imageCaptured, this, &Widget::save_pic);
    
    // 创建取景器
    if (vf) {
        delete vf;
        vf = nullptr;
    }
    vf = new QCameraViewfinder(viewfinderContainer);
    vf->resize(viewfinderContainer->size());
    myCamera->setViewfinder(vf);
    vf->show();
    
    // 启动摄像头
    myCamera->start();
    
    // 启用录制按钮
    recordButton->setEnabled(true);
    statusLabel->setText("摄像头已启动");
    
    qDebug() << "Camera started successfully";
}

void Widget::on_closeCamera_clicked()
{
    // 如果正在录制,先停止录制
    if (isRecording && recorder) {
        recorder->stop();
        recordingTimer->stop();
        recordingTimeLabel->hide();
        isRecording = false;
        recordButton->setText("开始录制");
        statusLabel->setText("录制已停止,摄像头已关闭");
    }
    
    if (myCamera) {
        myCamera->stop();
        qDebug() << "Camera stopped";
    }
    
    if (vf) {
        vf->close();
    }
    
    // 禁用录制按钮
    recordButton->setEnabled(false);
}

void Widget::on_captureBt_clicked()
{
    if (cp && myCamera && myCamera->state() == QCamera::ActiveState) {
        QString filename = QString("./capture_%1.jpg").arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"));
        cp->capture(filename);
        statusLabel->setText("照片已保存: " + filename);
        qDebug() << "Capture image:" << filename;
    } else {
        qDebug() << "Cannot capture image: camera not ready";
        statusLabel->setText("错误: 摄像头未准备好");
        QMessageBox::warning(this, "警告", "摄像头未准备好,无法拍照");
    }
}

void Widget::on_recordButton_clicked()
{
    if (!recorder || !myCamera) {
        statusLabel->setText("错误: 录制器未初始化");
        QMessageBox::warning(this, "警告", "摄像头未打开,无法录制");
        return;
    }
    
    if (!isRecording) {
        // 开始录制
        QString videosDir = QStandardPaths::writableLocation(QStandardPaths::MoviesLocation);
        if (videosDir.isEmpty()) {
            videosDir = ".";
        }
        
        QString filename = QString("%1/video_%2.mp4").arg(videosDir).arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"));
        recorder->setOutputLocation(QUrl::fromLocalFile(filename));
        
        qDebug() << "Starting recording to:" << filename;
        statusLabel->setText("开始录制: " + filename);
        
        recorder->record();
            if (recorder->state() != QMediaRecorder::RecordingState) {
        QMessageBox::warning(this, "警告", "无法开始录制,可能缺少编码器支持");
        return;
    }
    } else {
        // 停止录制
        qDebug() << "Stopping recording";
        statusLabel->setText("停止录制中...");
        recorder->stop();
    }
}

void Widget::updateRecordingTime()
{
    recordingTime = recordingTime.addSecs(1);
    recordingTimeLabel->setText("录制中: " + recordingTime.toString("hh:mm:ss"));
}

void Widget::onRecorderStateChanged(QMediaRecorder::State state)
{
    qDebug() << "Recorder state changed to:" << state;
    
    switch (state) {
    case QMediaRecorder::StoppedState:
        recordingTimer->stop();
        recordingTimeLabel->hide();
        recordButton->setText("开始录制");
        isRecording = false;
        statusLabel->setText("录制已停止,视频已保存");
        QMessageBox::information(this, "信息", "视频录制已保存");
        break;
        
    case QMediaRecorder::RecordingState:
        recordingTime = QTime(0, 0, 0);
        recordingTimer->start(1000); // 每秒更新一次
        recordingTimeLabel->show();
        recordButton->setText("停止录制");
        isRecording = true;
        statusLabel->setText("正在录制...");
        break;
        
    case QMediaRecorder::PausedState:
        recordingTimer->stop();
        recordButton->setText("继续录制");
        statusLabel->setText("录制已暂停");
        break;
    }
}

void Widget::onRecorderError(QMediaRecorder::Error error)
{
    QString errorMsg;
    switch (error) {
    case QMediaRecorder::ResourceError:
        errorMsg = "资源错误";
        break;
    case QMediaRecorder::FormatError:
        errorMsg = "格式错误";
        break;
    case QMediaRecorder::OutOfSpaceError:
        errorMsg = "存储空间不足";
        break;
    default:
        errorMsg = "未知错误";
        break;
    }
    
    statusLabel->setText("录制错误: " + errorMsg);
    QMessageBox::critical(this, "录制错误", 
                         QString("录制过程中发生错误: %1 - %2").arg(errorMsg).arg(recorder->errorString()));
    qDebug() << "Recorder error:" << error << recorder->errorString();
    
    // 重置录制状态
    if (isRecording) {
        recordingTimer->stop();
        recordingTimeLabel->hide();
        recordButton->setText("开始录制");
        isRecording = false;
    }
}

// 保存图片槽函数
void Widget::save_pic(int id, const QImage &preview)
{
    qDebug() << "Image captured with id:" << id;
    QPixmap mmp = QPixmap::fromImage(preview);
    mmp = mmp.scaled(imageLabel->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
    imageLabel->setPixmap(mmp);
    imageLabel->setText(""); // 清除文本,显示图片
}

void Widget::onActivated(int index)
{
    m_index = index;
    qDebug() << "Selected camera index:" << index;
}

void Widget::onResolutionChanged(int index)
{
    qDebug() << "Resolution changed to:" << mResSize[index];
    
    // 如果摄像头正在运行,重新启动以应用新的分辨率
    if (myCamera && myCamera->state() == QCamera::ActiveState) {
        qDebug() << "Restarting camera to apply new resolution...";
        
        // 如果正在录制,先停止录制
        if (isRecording && recorder) {
            recorder->stop();
            recordingTimer->stop();
            recordingTimeLabel->hide();
            isRecording = false;
            recordButton->setText("开始录制");
        }
        
        myCamera->stop();
        
        // 设置新的分辨率
        QSize selectedSize = mResSize[index];
        QCameraViewfinderSettings settings;
        settings.setResolution(selectedSize);
        myCamera->setViewfinderSettings(settings);
        
        // 重新设置录制器
        setupRecorder();
        
        myCamera->start();
    }
}

获取系统支持的编码参数

在实际应用中,我们通常需要先查询系统支持的编码参数,然后选择合适的设置。可以通过以下方法获取:

// 获取支持的视频编码器列表
QStringList codecs = recorder->supportedVideoCodecs();

// 获取支持的 resolutions
QList<QSize> resolutions = recorder->supportedResolutions();

// 获取支持的帧率范围
QList<qreal> frameRates = recorder->supportedFrameRates();

// 获取指定编码器支持的 resolutions 和 frame rates
QList<QSize> resForCodec = recorder->supportedResolutions("video/mp4");
QList<qreal> frameRatesForCodec = recorder->supportedFrameRates("video/mp4");

检查系统编码器支持

cpp
void Widget::checkEncoderSupport()
{
    if (!recorder) return;
    
    qDebug() << "=== 系统编码器支持情况 ===";
    qDebug() << "视频编码器:" << recorder->supportedVideoCodecs();
    qDebug() << "音频编码器:" << recorder->supportedAudioCodecs();
    qDebug() << "容器格式:" << recorder->supportedContainers();
    
    // 如果没有H.264编码器,可能需要安装额外的编解码器包
    if (recorder->supportedVideoCodecs().isEmpty()) {
        QMessageBox::warning(this, "警告", 
            "系统没有可用的视频编码器。\n"
            "Windows: 安装K-Lite Codec Pack\n"
            "Linux: 安装GStreamer插件\n"
            "macOS: 确保系统完整");
    }
}

3. VideoCaptureApp.pro

确保包含所有必要的模块:

QT += core gui multimedia multimediawidgets

CONFIG += c++11

TARGET = CameraApp
TEMPLATE = app

SOURCES += \
    main.cpp \
    widget.cpp

HEADERS += \
    widget.h

注意:Qt本身不提供完整的音视频编码器实现,而是作为一个封装层,调用系统底层的多媒体框架。

Qt的多媒体架构-后端多媒体框架

Qt通过不同的后端调用系统编码器:

  • Windows: DirectShow, Media Foundation

  • Linux: GStreamer, PulseAudio

  • macOS: AVFoundation

  • Android: MediaCodec, OpenSL ES

 

运行效果:

image

总结:

当前程序保存的是经过编码压缩的MP4视频文件,不是原始YUV数据。要保存原始YUV数据,需要使用QVideoProbe来获取原始视频帧,然后直接写入文件。

  1. Qt不提供内置编码器,而是封装系统编码器

  2. 程序确实使用了编码器,通过QMediaRecorder调用系统编码器

  3. 录制问题通常源于

    • 系统缺少相应编码器

    • 编码器配置不兼容

    • 权限或资源问题

推荐使用原始YUV数据录制方案,这样可以避免编码器兼容性问题,同时获得原始视频数据用于后续处理。

 

Qt 摄像头视频数据采集(原始数据)

我们将使用Qt5.14.2来编写一个简单的摄像头应用程序。这个应用程序将包含以下功能:

  1. 打开摄像头并显示视频流

  2. 关闭摄像头

  3. 拍照(保存为图片)

  4. 录制视频(保存为YUV格式)

  5. 在界面上显示控制按钮和状态信息

注意:录制视频时,我们将使用原始数据(即YUV格式)保存。Qt的QCameraViewfinderFrame可以获取到视频帧,但是获取原始数据需要访问底层视频帧。

由于Qt的摄像头模块可能不直接提供原始YUV数据的访问,我们需要通过QAbstractVideoSurface或自定义的QVideoFrame处理来获取原始数据。

但是,在Qt5.14.2中,我们可以使用QCameraViewfinder来显示,同时使用QCameraImageCapture来捕获图像,而录制视频则可以使用QMediaRecorder。但是QMediaRecorder通常保存为压缩格式,如果我们想要原始YUV数据,可能需要直接处理视频帧。

另一种方法是使用QVideoProbe来捕获视频帧,然后将其保存为YUV格式。

由于要求保存为原始YUV文件,我们将使用QVideoProbe来获取每一帧,然后写入文件。

步骤:

  1. 创建Qt应用程序,包含必要的按钮和标签。

  2. 使用QCamera和QVideoProbe来捕获视频帧。

  3. 当点击录制按钮时,开始将视频帧的YUV数据写入文件,再次点击停止录制。

注意:不同的摄像头可能支持不同的像素格式,我们假设使用YUV420P(即YUV420Planar)格式。如果摄像头不支持,可能需要转换。

由于我们使用原始数据,所以需要知道帧的尺寸和格式。在QVideoFrame中,我们可以获取到这些信息。

我们将创建一个简单的界面,包含以下按钮:

  • 打开摄像头

  • 关闭摄像头

  • 拍照

  • 录制

状态信息可以显示在标签上。

由于YUV文件很大,我们只录制一段时间,并保存为.yuv文件。

实现步骤:

  1. 创建项目,并在.pro文件中加入:
    QT += core gui multimedia multimediawidgets

  2. 设计界面,使用QPushButton和QLabel。

  3. 在头文件中,包含必要的类。

  4. 实现功能:

    • 打开摄像头:初始化QCamera,设置Viewfinder,启动摄像头。

    • 关闭摄像头:停止摄像头。

    • 拍照:使用QCameraImageCapture捕获图片并保存。

    • 录制:使用QVideoProbe捕获视频帧,当录制按钮点击时,开始录制,再次点击停止录制,并将数据写入文件。

  5. 注意:录制过程中,我们需要将每一帧的YUV数据写入文件。但是,QVideoFrame可能不是连续的YUV数据,可能有不同的布局(比如平面或打包),我们需要根据其格式来写入。

由于YUV420P是三个平面,我们需要分别写入Y、U、V三个部分。

但是,注意:QVideoFrame::map()函数可以让我们访问帧的数据。

我们将使用QVideoProbe来捕获视频帧,当有视频帧时,我们将其写入文件(如果正在录制)。

注意:不同的摄像头可能输出不同的格式,我们假设为YUV420P。如果格式不同,我们需要进行转换,或者根据实际格式写入。

由于时间关系,我们假设摄像头输出Format_YUV420P。

如果格式不是YUV420P,我们可以尝试转换,或者提示错误。

由于转换比较复杂,我们这里只处理YUV420P格式。

实现:

我们创建一个类,继承自QMainWindow,然后实现上述功能。

下面是一个简单的实现示例:

 

以下是一个使用Qt5.14.2编写的简单摄像头应用程序,可以捕获原始YUV数据并保存到文件:

main.cpp

// main.cpp
#include <QApplication>
#include "camerawidget.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    
    CameraWidget w;
    w.show();
    
    return a.exec();
}

1. camerawidget.h

// camerawidget.h
#ifndef CAMERAWIDGET_H
#define CAMERAWIDGET_H

#include <QWidget>
#include <QCamera>
#include <QCameraViewfinder>
#include <QCameraImageCapture>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QFile>
#include <QTimer>
#include <QDebug>
#include <QComboBox>  // 添加下拉框头文件

class CameraWidget : public QWidget
{
    Q_OBJECT

public:
    CameraWidget(QWidget *parent = nullptr);
    ~CameraWidget();

private slots:
    void openCamera();
    void closeCamera();
    void takePhoto();
    void toggleRecording();
    void updateFrame(const QVideoFrame &frame);
    void updateStatus();
    void onCameraChanged(int index);  // 添加摄像头切换槽函数

private:
    void setupUI();
    void setupCamera();
    void refreshCameraList();  // 添加刷新摄像头列表函数

    void writeYUVFrame(const QVideoFrame &frame);
    void convertRGB32ToYUV420P(const uchar *rgbData, int width, int height, int bytesPerLine);

    QCamera *camera;
    QCameraViewfinder *viewfinder;

    QComboBox *cameraComboBox;  // 添加摄像头下拉框
    QPushButton *btnOpen;
    QPushButton *btnClose;
    QPushButton *btnPhoto;
    QPushButton *btnRecord;
    QLabel *statusLabel;

    QFile *yuvFile;
    bool isRecording;
    int frameCount;
    QTimer *statusTimer;

    QSize videoResolution;

    QList<QCameraInfo> availableCameras;  // 存储可用的摄像头列表

};

#endif // CAMERAWIDGET_H

2. camerawidget.cpp

// camerawidget.cpp
#include "CameraWidget.h"
#include <QCameraInfo>
#include <QVideoProbe>
#include <QMessageBox>
#include <QDateTime>
#include <QVideoSurfaceFormat>
#include <QUrl>

CameraWidget::CameraWidget(QWidget *parent)
    : QWidget(parent)
    , camera(nullptr)
    , viewfinder(nullptr)
    , cameraComboBox(nullptr)
    , yuvFile(nullptr)
    , isRecording(false)
    , frameCount(0)
{
    setupUI();
    refreshCameraList();  // 初始化摄像头列表
}

CameraWidget::~CameraWidget()
{
    if (isRecording) {
        yuvFile->close();
        delete yuvFile;
    }
    if (camera) {
        camera->stop();
        delete camera;
    }
}

void CameraWidget::setupUI()
{
    setWindowTitle("摄像头采集程序");
    setFixedSize(800, 600);

    // 创建视图
    viewfinder = new QCameraViewfinder(this);
    viewfinder->setMinimumSize(640, 480);

    // 创建摄像头选择下拉框
    cameraComboBox = new QComboBox(this);
    cameraComboBox->setMinimumWidth(200);

    // 创建按钮
    btnOpen = new QPushButton("打开摄像头", this);
    btnClose = new QPushButton("关闭摄像头", this);
    btnPhoto = new QPushButton("拍照", this);
    btnRecord = new QPushButton("开始录制", this);

    // 状态标签
    statusLabel = new QLabel("状态: 摄像头未开启", this);

    // 设置布局
    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    QHBoxLayout *cameraLayout = new QHBoxLayout();  // 添加摄像头选择布局
    QHBoxLayout *buttonLayout = new QHBoxLayout();
    QHBoxLayout *statusLayout = new QHBoxLayout();

    // 摄像头选择布局
    cameraLayout->addWidget(new QLabel("选择摄像头:", this));
    cameraLayout->addWidget(cameraComboBox);
    cameraLayout->addStretch();

    buttonLayout->addWidget(btnOpen);
    buttonLayout->addWidget(btnClose);
    buttonLayout->addWidget(btnPhoto);
    buttonLayout->addWidget(btnRecord);
    buttonLayout->addStretch();

    statusLayout->addWidget(statusLabel);
    statusLayout->addStretch();

    mainLayout->addLayout(cameraLayout);  // 添加摄像头选择布局
    mainLayout->addWidget(viewfinder);
    mainLayout->addLayout(buttonLayout);
    mainLayout->addLayout(statusLayout);

    // 连接信号槽
    connect(btnOpen, &QPushButton::clicked, this, &CameraWidget::openCamera);
    connect(btnClose, &QPushButton::clicked, this, &CameraWidget::closeCamera);
    connect(btnPhoto, &QPushButton::clicked, this, &CameraWidget::takePhoto);
    connect(btnRecord, &QPushButton::clicked, this, &CameraWidget::toggleRecording);
    connect(cameraComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
            this, &CameraWidget::onCameraChanged);

    // 状态更新定时器
    statusTimer = new QTimer(this);
    connect(statusTimer, &QTimer::timeout, this, &CameraWidget::updateStatus);
    statusTimer->start(1000); // 每秒更新一次状态

    // 初始状态
    btnClose->setEnabled(false);
    btnPhoto->setEnabled(false);
    btnRecord->setEnabled(false);
}

void CameraWidget::refreshCameraList()
{
    cameraComboBox->clear();
    availableCameras = QCameraInfo::availableCameras();

    if (availableCameras.isEmpty()) {
        cameraComboBox->addItem("未找到摄像头");
        statusLabel->setText("状态: 未找到摄像头");
        btnOpen->setEnabled(false);
    } else {
        for (const QCameraInfo &cameraInfo : availableCameras) {
            cameraComboBox->addItem(cameraInfo.description(), cameraInfo.deviceName());
        }
        statusLabel->setText("状态: 请选择摄像头并打开");
        btnOpen->setEnabled(true);
    }
}

void CameraWidget::onCameraChanged(int index)
{
    // 如果当前有摄像头在运行,先关闭
    if (camera && camera->status() == QCamera::ActiveStatus) {
        closeCamera();
    }

    // 更新UI状态
    if (index >= 0 && index < availableCameras.size()) {
        btnOpen->setEnabled(true);
    } else {
        btnOpen->setEnabled(false);
    }
}

void CameraWidget::setupCamera()
{
    if (!camera) {
        return;
    }

    camera->setViewfinder(viewfinder);

    // 尝试设置摄像头使用 YUV 格式
    QCameraViewfinderSettings settings;
    QList<QVideoFrame::PixelFormat> formats = camera->supportedViewfinderPixelFormats();

    qDebug() << "支持的像素格式:";
    for (const auto &format : formats) {
        qDebug() << "  " << format;
    }

    // 优先选择 YUV 格式
    if (formats.contains(QVideoFrame::Format_YUV420P)) {
        settings.setPixelFormat(QVideoFrame::Format_YUV420P);
    } else if (formats.contains(QVideoFrame::Format_NV12)) {
        settings.setPixelFormat(QVideoFrame::Format_NV12);
    } else if (formats.contains(QVideoFrame::Format_NV21)) {
        settings.setPixelFormat(QVideoFrame::Format_NV21);
    }

    camera->setViewfinderSettings(settings);

    QVideoProbe *probe = new QVideoProbe(this);
    if (probe->setSource(camera)) {
        connect(probe, &QVideoProbe::videoFrameProbed, this, &CameraWidget::updateFrame);
    }
}

void CameraWidget::openCamera()
{
    int currentIndex = cameraComboBox->currentIndex();
    if (currentIndex < 0 || currentIndex >= availableCameras.size()) {
        QMessageBox::warning(this, "警告", "请选择有效的摄像头!");
        return;
    }

    // 如果已有摄像头在运行,先释放
    if (camera) {
        camera->stop();
        delete camera;
        camera = nullptr;
    }

    // 创建新的摄像头对象
    QCameraInfo selectedCamera = availableCameras.at(currentIndex);
    camera = new QCamera(selectedCamera, this);

    setupCamera();

    if (camera)
    {
        camera->start();
        btnOpen->setEnabled(false);
        btnClose->setEnabled(true);
        btnPhoto->setEnabled(true);
        btnRecord->setEnabled(true);
        cameraComboBox->setEnabled(false);  // 打开摄像头后禁用下拉框
        statusLabel->setText(QString("状态: 摄像头已开启 - %1").arg(selectedCamera.description()));
    }
}

void CameraWidget::closeCamera()
{
    if (isRecording) {
        toggleRecording(); // 如果正在录制,先停止录制
    }

    if (camera) {
        camera->stop();
        btnOpen->setEnabled(true);
        btnClose->setEnabled(false);
        btnPhoto->setEnabled(false);
        btnRecord->setEnabled(false);
        cameraComboBox->setEnabled(true);  // 关闭摄像头后启用下拉框
        statusLabel->setText("状态: 摄像头已关闭");
    }
}

void CameraWidget::takePhoto()
{
    if (!camera || camera->status() != QCamera::ActiveStatus) {
        QMessageBox::warning(this, "警告", "摄像头未开启!");
        return;
    }

    // 这里可以添加拍照功能
    statusLabel->setText("状态: 拍照完成");
}

void CameraWidget::toggleRecording()
{
    if (!isRecording) {
        // 开始录制
        QString fileName = QString("video_%1.yuv").arg(QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss"));
        yuvFile = new QFile(fileName);

        if (!yuvFile->open(QIODevice::WriteOnly)) {
            QMessageBox::critical(this, "错误", "无法创建文件: " + fileName);
            delete yuvFile;
            yuvFile = nullptr;
            return;
        }

        isRecording = true;
        frameCount = 0;
        btnRecord->setText("停止录制");
        statusLabel->setText("状态: 正在录制...");

        qDebug() << "开始录制到文件:" << fileName;
    } else {
        // 停止录制
        if (yuvFile) {
            yuvFile->close();
            delete yuvFile;
            yuvFile = nullptr;
        }

        isRecording = false;
        btnRecord->setText("开始录制");
        statusLabel->setText(QString("状态: 录制完成,共 %1 帧").arg(frameCount));

        qDebug() << "录制停止,总帧数:" << frameCount;
    }
}

void CameraWidget::updateFrame(const QVideoFrame &frame)
{
    if (isRecording && yuvFile && yuvFile->isOpen()) {
        writeYUVFrame(frame);
        frameCount++;
    }
}

void CameraWidget::updateStatus()
{
    if (isRecording) {
        statusLabel->setText(QString("状态: 正在录制... 已录制 %1 帧").arg(frameCount));
    }
}

void CameraWidget::writeYUVFrame(const QVideoFrame &frame)
{
    QVideoFrame cloneFrame(frame);
    if (cloneFrame.map(QAbstractVideoBuffer::ReadOnly)) {
        QVideoFrame::PixelFormat pixelFormat = cloneFrame.pixelFormat();
        QSize size = cloneFrame.size();

        if (frameCount == 0) {
            videoResolution = size;
            qDebug() << "视频分辨率:" << size << "格式:" << pixelFormat;
        }

        const uchar *bits = cloneFrame.bits();
        int bytes = cloneFrame.mappedBytes();

        if (pixelFormat == QVideoFrame::Format_YUV420P ||
            pixelFormat == QVideoFrame::Format_NV12 ||
            pixelFormat == QVideoFrame::Format_NV21) {

            // 写入原始YUV数据
            yuvFile->write((const char*)bits, bytes);

        } else if (pixelFormat == QVideoFrame::Format_RGB32) {

            // 将 RGB32 转换为 YUV420P
            convertRGB32ToYUV420P(bits, size.width(), size.height(), cloneFrame.bytesPerLine());

        } else {
            qDebug() << "不支持的视频格式:" << pixelFormat;
        }

        cloneFrame.unmap();
    }
}

// 此方法录制的图像是倒过来的
//void CameraWidget::convertRGB32ToYUV420P(const uchar *rgbData, int width, int height, int bytesPerLine)
//{
//    // 计算 YUV 数据大小
//    int ySize = width * height;
//    int uvSize = ySize / 4;
//    QByteArray yuvData(ySize + uvSize * 2, 0);

//    uchar *yPlane = (uchar*)yuvData.data();
//    uchar *uPlane = yPlane + ySize;
//    uchar *vPlane = uPlane + uvSize;

//    // RGB32 到 YUV420P 转换
//    for (int y = 0; y < height; y++) {
//        const uchar *rgbLine = rgbData + y * bytesPerLine;
//        for (int x = 0; x < width; x++) {
//            int r = rgbLine[x * 4 + 2]; // R
//            int g = rgbLine[x * 4 + 1]; // G
//            int b = rgbLine[x * 4 + 0]; // B

//            // 计算 Y 分量
//            int yValue = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
//            yPlane[y * width + x] = qBound(0, yValue, 255);

//            // 只在 2x2 块的中心像素计算 UV 分量
//            if ((y % 2 == 0) && (x % 2 == 0)) {
//                int uValue = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
//                int vValue = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;

//                int uvIndex = (y / 2) * (width / 2) + (x / 2);
//                uPlane[uvIndex] = qBound(0, uValue, 255);
//                vPlane[uvIndex] = qBound(0, vValue, 255);
//            }
//        }
//    }

//    // 写入转换后的 YUV 数据
//    yuvFile->write(yuvData);
//}

void CameraWidget::convertRGB32ToYUV420P(const uchar *rgbData, int width, int height, int bytesPerLine)
{
    // 计算 YUV 数据大小
    int ySize = width * height;
    int uvSize = ySize / 4;
    QByteArray yuvData(ySize + uvSize * 2, 0);

    uchar *yPlane = (uchar*)yuvData.data();
    uchar *uPlane = yPlane + ySize;
    uchar *vPlane = uPlane + uvSize;

    // RGB32 到 YUV420P 转换,同时进行垂直翻转
    for (int y = 0; y < height; y++) {
        // 从底部开始读取,实现垂直翻转
        const uchar *rgbLine = rgbData + (height - 1 - y) * bytesPerLine;
        for (int x = 0; x < width; x++) {
            int r = rgbLine[x * 4 + 2]; // R
            int g = rgbLine[x * 4 + 1]; // G
            int b = rgbLine[x * 4 + 0]; // B

            // 计算 Y 分量
            int yValue = ((66 * r + 129 * g + 25 * b + 128) >> 8) + 16;
            yPlane[y * width + x] = qBound(0, yValue, 255);

            // 只在 2x2 块的中心像素计算 UV 分量
            if ((y % 2 == 0) && (x % 2 == 0)) {
                int uValue = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
                int vValue = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;

                int uvIndex = (y / 2) * (width / 2) + (x / 2);
                uPlane[uvIndex] = qBound(0, uValue, 255);
                vPlane[uvIndex] = qBound(0, vValue, 255);
            }
        }
    }

    // 写入转换后的 YUV 数据
    yuvFile->write(yuvData);
}

3. CameraRecorder.pro

# CameraRecorder.pro
QT += core gui widgets multimedia multimediawidgets

CONFIG += c++11

TARGET = CameraRecorder
TEMPLATE = app

SOURCES += \
    main.cpp \
    camerawidget.cpp

HEADERS += \
    camerawidget.h
 
image

功能说明:

  1. 主要功能:

    • 打开/关闭摄像头

    • 实时显示摄像头画面

    • 录制原始YUV数据到文件

    • 显示录制状态和帧数

  2. 文件格式:

    • 保存为原始YUV格式文件

    • 文件名包含时间戳:video_yyyyMMdd_hhmmss.yuv

  3. 界面控件:

    • 打开摄像头按钮

    • 关闭摄像头按钮

    • 拍照按钮(功能待扩展)

    • 录制按钮(开始/停止)

    • 状态显示标签

使用说明:

  1. 编译运行程序

  2. 点击"打开摄像头"启动摄像头

  3. 点击"开始录制"开始保存YUV数据

  4. 再次点击"停止录制"结束录制

  5. 录制文件保存在程序运行目录

注意事项:

  • 需要系统有可用的摄像头设备

  • YUV文件可能会很大,请确保有足够的磁盘空间

  • 实际支持的YUV格式取决于摄像头驱动和系统支持

  • 可以根据需要扩展拍照功能和其他视频格式支持

这个程序提供了基本的摄像头采集和YUV数据保存功能,可以根据具体需求进一步扩展和完善。

 

使用VLC播放YUV原始视频文件需要正确设置参数,因为YUV文件是原始数据,没有包含格式信息。

以下是几种播放方法:

使用VLC播放YUV原始视频:

  1. 打开VLC

  2. 菜单:媒体 → 打开文件

  3. 选择你的YUV文件

  4. 点击"显示更多选项"

  5. 在"编辑选项"中输入:

    :demux=rawvideo :rawvid-fps=30 :rawvid-width=640 :rawvid-height=480 :rawvid-chroma=I420
  6. 点击播放

参数说明:

  • --demux=rawvideo:指定解复用器为原始视频

  • --rawvid-fps=30:设置帧率(根据实际情况调整)

  • --rawvid-width=640:设置宽度(根据实际情况调整)

  • --rawvid-height=480:设置高度(根据实际情况调整)

  • --rawvid-chroma=I420:设置颜色格式(I420就是YUV420P)

 

 

posted @ 2025-09-30 15:56  [BORUTO]  阅读(49)  评论(0)    收藏  举报