【QT window】ffmpeg实现手动绘图(裁剪)、缩放、拍照,显示fps等功能

前言

QT进行摄像头相关的开发,除了可以使用自带的多媒体模块,以及opencv(前面已经分享相关博文),还可以用专注底层细节的音视频处理工具ffmpeg。

需要说明的是QT多媒体模块、opencv、ffmpeg这3者虽然都可以播放视频,但是它们专注的领域是不一样的:

  • QT多媒体模块:快速构建 GUI 应用,集成播放、录音等基础功能;
  • opencv:图像专业处理,比如图片裁剪、人脸识别、图文识别等;
  • ffmpeg:底层音视频处理库,实现编解码、转码、滤镜、流媒体、格式封装等专业处理。

在对硬件有要求的条件下,用ffmpeg才能实现极致优化(低延迟等),opencv在图片处理上非常厉害,在视频播放上不一定很全面,项目对fps有高要求时,需要结合ffmpeg库进行开发。

效果图

ffmpeg工具

安装

官网地址:https://www.ffmpeg.org/download.html#build-windows

window版本下载链接 (如果不生效,可能是最新版本更新了,按照以下步骤进官网下载)

解压到任意目录下

解压后,在bin目录中可以看到执行文件ffmpeg.exe,把路径加入到系统环境变量中就可以在cmd命令行窗口中直接输入命令使用了

常用命令

1、查看摄像头清单

复制代码
ffmpeg -list_devices true -f dshow -i dummy

video表示摄像头,audio表示麦克风

2、查看指定摄像头的分辨率、像素格式、fps

复制代码
ffplay -f dshow -list_options true -i video="xxCamera"
复制代码
pixel_format=yuyv422  min s=3264x2448 fps=2 max s=3264x2448 fps=2
//yuyv422是像素格式
//3264x2448 是分辨率
//fps=2 最大支持的fps值为2,不能超过2

vcodec=mjpeg  min s=3264x2448 fps=15 max s=3264x2448 fps=15
//mjpeg是像素格式
//3264x2448 是分辨率
//fps=15 最大支持的fps值为15,不能超过15

从以上可看出YUYV格式下,最大FPS为2,而MJPEG格式下,最大FPS为15,以下是对比

特性 YUYV格式 MJPEG(MJPG)格式
压缩方式 无压缩,原始像素格式 帧内压缩,每帧独立JPEG编码
数据量 非常大(每像素16位) 中等(压缩比1/10~1/20)
带宽需求 高(如1920×1080@30fps约1Gbps) 较低(同分辨率约48-120Mbps)
解码需求 无需解码,直接使用 需要CPU/GPU解压JPEG
帧独立性 不适用 每帧独立,便于随机访问和丢包恢复
画质 高,保留完整细节 有压缩损失,可能出现马赛克

3、打开摄像头测试

打开摄像头默认情况下是以无压缩方式(YUYV)获取图像,想要提高FPS就要指定MJPEG格式/15fps。以下两条命令分别为2fps和15fps的显示效果,可以明显感觉出2fps的画面有明显的延时顿挫感

复制代码
ffplay -f dshow -video_size 3264x2448 -framerate 2 -i video="xxCamera" -fflags nobuffer -flags low_delay -framedrop -vf "scale=960:720"
ffplay -f dshow -video_size 3264x2448 -framerate 15 -i video="xxLCamera" -fflags nobuffer -flags low_delay -framedrop -vf "scale=960:720"

QT加入ffmpeg库

以上安装ffmpeg的目录中已经包含相应的动态库和静态库

1、修改.pro配置

以上面ffmpeg安装目录D:\software\ffmpeg-8.0为例,下面是.pro文件想要加入的配置信息

复制代码
# FFmpeg 路径
FFMPEG_DIR = D:\software\ffmpeg-8.0

INCLUDEPATH += $$FFMPEG_DIR/include

LIBS += -L$$FFMPEG_DIR/bin \        # MinGW 需要链接 .dll(运行时),但 .a 在 lib/
        -L$$FFMPEG_DIR/lib \
        -lavcodec \
        -lavformat \
        -lavutil \
        -lswscale \
        -lswresample \
        -lavfilter

如果希望使用.pri方式(前面的opencv也是使用.pri方式,方便后面打包),以下是在ffmpeg目录下新建的ffmpeg.pri文件内容:

复制代码
FFMPEG_ROOT = $$PWD
INCLUDEPATH += $$FFMPEG_ROOT/include

LIBS += -L$$FFMPEG_ROOT/bin \
        -L$$FFMPEG_ROOT/lib \
        -lavutil \                  #核心工具库
        -lavdevice \                #输入输出设备库
        -lavcodec \                 #编解码库
        -lavformat \                #件格式和协议库
        -lswscale \                 #将图像进行格式转换
        -lswresample \              #用于音频重采样,可以对数字音频进行声道数、数据格式、采样率等多种基本信息的转换
        -lavfilter                  #音视频滤镜库

win32 {
    FFMPEG_DLL_PATH = $$FFMPEG_ROOT/bin
    INSTALL_DIR = $$OUT_PWD/install

    # Step 1: 清空并重新创建 install 目录
    QMAKE_POST_BUILD += $$QMAKE_MKDIR $$shell_path($$INSTALL_DIR) $$escape_expand(\\n\\t)
    #QMAKE_POST_BUILD += $$QMAKE_DEL_FILE $$shell_path($$INSTALL_DIR/*) $$escape_expand(\\n\\t)

    # Step 2: 只复制 FFmpeg DLL 文件
    QMAKE_POST_BUILD += $$QMAKE_COPY $$shell_path($$FFMPEG_DLL_PATH/avutil-60.dll) $$shell_path($$INSTALL_DIR) $$escape_expand(\\n\\t)
    QMAKE_POST_BUILD += $$QMAKE_COPY $$shell_path($$FFMPEG_DLL_PATH/avdevice-62.dll) $$shell_path($$INSTALL_DIR) $$escape_expand(\\n\\t)
    QMAKE_POST_BUILD += $$QMAKE_COPY $$shell_path($$FFMPEG_DLL_PATH/avcodec-62.dll) $$shell_path($$INSTALL_DIR) $$escape_expand(\\n\\t)
    QMAKE_POST_BUILD += $$QMAKE_COPY $$shell_path($$FFMPEG_DLL_PATH/avformat-62.dll) $$shell_path($$INSTALL_DIR) $$escape_expand(\\n\\t)
    QMAKE_POST_BUILD += $$QMAKE_COPY $$shell_path($$FFMPEG_DLL_PATH/swscale-9.dll) $$shell_path($$INSTALL_DIR) $$escape_expand(\\n\\t)
    QMAKE_POST_BUILD += $$QMAKE_COPY $$shell_path($$FFMPEG_DLL_PATH/swresample-6.dll) $$shell_path($$INSTALL_DIR) $$escape_expand(\\n\\t)
    QMAKE_POST_BUILD += $$QMAKE_COPY $$shell_path($$FFMPEG_DLL_PATH/avfilter-11.dll) $$shell_path($$INSTALL_DIR) $$escape_expand(\\n\\t)

    # Step 3: 只复制 .exe 文件
    CONFIG(debug, debug|release) {
        EXE_FILE = $$OUT_PWD/debug/$${TARGET}.exe
    } else {
        EXE_FILE = $$OUT_PWD/release/$${TARGET}.exe
    }

    # 确保只复制 .exe 文件
    QMAKE_POST_BUILD += $$QMAKE_COPY $$shell_path($$EXE_FILE) $$shell_path($$INSTALL_DIR) $$escape_expand(\\n\\t)
}

HEADERS +=

SOURCES +=

然后在工程文件.pro中加入以下两个内容:

复制代码
TARGET = ffmpegtest   #生成的执行文件名称,一定要在文件开头位置加上


include(D:\software\ffmpeg-8.0\ffmpeg.pri)   #pri文件的觉得路径,在文件的末尾位置加上

2、简单的测试

在QT下面的main.cpp中加入以下代码,查看ffmpeg的版本(注意:ffmpeg库是C语言实现的,需要extern "C"进行引用)

复制代码
#include "mainwindow.h"

#include <QApplication>
#include <iostream>
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}

int main(int argc, char *argv[])
{
    // 打印 FFmpeg 版本
    std::cout << "FFmpeg version: " << av_version_info() << std::endl;
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

打印信息如下,即说明引用正常

复制代码
FFmpeg version: 8.0-full_build-www.gyan.dev

功能讲解

本篇分享功能包括:显示/刷新摄像头和分辨率,摄像头打开/关闭,对图像进行绘画、缩放、拍照,以及显示打开的格式、实时FPS等功能。

ffmpeg接口

ffmpeg常用接口

FFmpeg 库 用途 常用接口 是否在你的项目中使用
libavutil 工具库(内存、数学、日志、像素格式等) av_malloc, av_free, av_strerror, av_image_get_buffer_size, av_get_pix_fmt_name ✅ 是
libavdevice 输入/输出设备(摄像头、屏幕捕获等) avdevice_register_all ✅ 是
libavformat 容器格式 & 协议(MP4, RTSP, DSHOW 等) avformat_open_input, avformat_find_stream_info, av_read_frame, avformat_close_input ✅ 是
libavcodec 编解码器(H.264, MJPEG, VP9 等) avcodec_find_decoder, avcodec_alloc_context3, avcodec_open2, avcodec_send_packet, avcodec_receive_frame ✅ 是
libswscale 图像缩放 & 格式转换(YUV ↔ RGB) sws_getContext, sws_scale, sws_freeContext ✅ 是
libswresample 音频重采样(声道、采样率转换) swr_init, swr_convert ❌ 否(无音频)
libavfilter 音视频滤镜(裁剪、旋转、加水印等) avfilter_graph_create, av_buffersrc_add_frame ❌ 否(未使用)

关键模块及接口

步骤 目标 使用的 FFmpeg 库 关键接口
1. 初始化设备支持 注册输入设备(如 Windows 的 dshow libavdevice avdevice_register_all()
2. 打开摄像头设备 以指定参数(分辨率、帧率、编码)打开摄像头 libavformat + libavdevice avformat_open_input()
3. 查找并初始化解码器 获取视频流信息,创建解码上下文 libavcodec avcodec_find_decoder(), avcodec_alloc_context3(), avcodec_parameters_to_context(), avcodec_open2()
4. 读取与解码帧 循环读取数据包 → 解码为原始图像帧 libavformat + libavcodec av_read_frame(), avcodec_send_packet(), avcodec_receive_frame()
5. 图像格式转换与缩放 将 YUV/其他格式转为 RGB,并缩放到显示尺寸 libswscale sws_getContext(), sws_scale()

函数流程图

  • 初始化 FFmpeg

    avdevice_register_all():注册所有输入输出设备,使得 FFmpeg 能够识别和操作这些设备。

注意:ffmpeg库接口可获取到设备的基础信息,无法直接获取到详细的分辨率、格式、fps等信息。

  • 打开摄像头设备

    • avformat_open_input():以指定参数(如分辨率、帧率)打开摄像头设备。
    • avformat_find_stream_info():获取视频流信息,这对于正确设置解码器上下文是必要的。(一个媒体内容可能不仅仅是视频流,当前是只获取视频流)
  • 查找并初始化解码器

    • avcodec_find_decoder():根据编码 ID 查找合适的解码器。
    • avcodec_alloc_context3():为选定的解码器分配解码上下文。
    • avcodec_parameters_to_context():将流参数复制到解码上下文中。
    • avcodec_open2():打开解码器准备进行解码工作。
  • 循环读取数据包

    • av_read_frame():从媒体文件中读取下一帧(对于实时流,比如摄像头,这将返回下一个可用的数据包)。
      • 在此步骤中,如果读取的数据包属于视频流,则调用:
        • avcodec_send_packet():发送数据包给解码器。
        • avcodec_receive_frame():接收已解码的帧。
  • 图像格式转换与缩放

    • sws_getContext():创建用于图像格式转换和/或尺寸调整的 SwsContext。
    • sws_scale():执行实际的格式转换和/或尺寸调整操作。
  • 显示或处理图像帧

    • 将转换后的图像帧显示在 Qt 界面上或其他进一步处理。

源码实现

复制代码
//mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QTimer>
#include <QPixmap>
#include <QImage>
#include <QElapsedTimer>
#include <QCameraInfo>
#include <QList>
#include <QSize>
#include <QMutex>
#include <QDateTime>
#include <QStandardPaths>
#include "metatypes.h"
// FFmpeg 头文件(C 风格)
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavdevice/avdevice.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
}

// 前向声明
namespace Ui {
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

protected:
    // 事件过滤器:处理 videoLabel 的鼠标和滚轮事件
    bool eventFilter(QObject *obj, QEvent *event) override;

private slots:
    void refreshCameraList();
    void toggleCamera();
    void updateFrame();
    void onCameraSelectionChanged(int index);
    void onResolutionsReady(const QList<CameraCapability> &resolutionInfos);
    void on_btnCapture_clicked(); // 拍照按钮槽函数

private:
    // 鼠标事件处理(手动裁剪)
    void handleMousePressEvent(QMouseEvent *event);
    void handleMouseMoveEvent(QMouseEvent *event);
    void handleMouseReleaseEvent(QMouseEvent *event);

    // 核心私有函数
    void setupConnections();
    void updateCameraButtonState();
    bool openCameraWithBestParams(const QString &cameraName, const QString &resolution, const QString &codec, double fps);
    void openCamera();
    void closeCamera();
    QList<CameraCapability> getCameraCapabilities(const QString &cameraName);
    void populateResolutionComboBox(const QList<CameraCapability> &resolutionInfos);
    void startCapabilityDetection(const QString &cameraName);
    QString getCodecName(AVCodecID codecId);
    QString getPixelFormatName(AVPixelFormat pixFmt);
    void applyZoom(); // 应用缩放和绘制矩形

    // UI 指针
    Ui::MainWindow *ui;

    // 定时器(用于帧更新)
    QTimer *timer;

    // 摄像头列表
    QList<CameraInfo> cameraList;

    // FFmpeg 相关变量
    AVFormatContext *formatContext = nullptr;
    AVCodecContext *codecContext = nullptr;
    const AVCodec *videoCodec  = nullptr;
    AVDictionary *options = nullptr;
    SwsContext *swsContext = nullptr;           // 用于显示缩放
    SwsContext *highResSwsCtx = nullptr;        // 用于保存原始帧(YUV → RGB)
    AVFrame *frame = nullptr;                   // 原始解码帧
    AVFrame *rgbFrame = nullptr;                // 显示用 RGB 帧
    uint8_t *rgbBuffer = nullptr;               // 显示缓冲区
    int rgbBufferSize = 0;
    int videoStreamIndex = -1;
    bool isCameraOpened = false;

    // FPS 计算
    int frameCount = 0;
    QElapsedTimer fpsTimer;
    double currentFps = 0.0;

    // 当前摄像头信息
    CameraInfo currentCamera;
    QString currentCodec;
    double currentMaxFps;

    // 视频显示尺寸(固定)
    const int videoDisplayWidth = 1024;
    const int videoDisplayHeight = 768;

    // 图像数据(用于保存)
    QImage latestHighResFrame; // 原始高分辨率 RGB 图像(用于拍照)
    mutable QMutex frameMutex; // 线程安全锁

    // 手动裁剪相关
    bool isManualCropMode = false; // 是否启用手动裁剪
    bool isDrawingRect = false;    // 是否正在拖拽
    QPoint rectStart;
    QPoint rectEnd;
    QRect manualCropRect;          // 最终确认的裁剪矩形(在 videoLabel 坐标系中)

    // 缩放相关
    double currentZoomFactor = 1.0;
    QPixmap originalDisplayPixmap; // 原始未缩放的显示 pixmap
};

#endif // MAINWINDOW_H

//mainwindow.cpp
// Qt核心模块
#include "mainwindow.h"        // 主窗口头文件
#include "ui_mainwindow.h"     // UI自动生成的头文件
#include "cameraprobing.h"     // 摄像头探测模块

// Qt GUI组件
#include <QLabel>              // 标签控件
#include <QComboBox>           // 下拉选择框
#include <QPushButton>         // 按钮控件
#include <QMessageBox>         // 消息对话框
#include <QDebug>              // 调试输出
#include <QApplication>        // 应用程序对象
#include <QDesktopWidget>       // 桌面窗口管理
#include <QCheckBox>           // 复选框
#include <QFileDialog>          // 文件对话框
#include <QDir>                // 目录操作
#include <QPainter>             // 绘图工具
#include <QWheelEvent>          // 滚轮事件
#include <QMouseEvent>          // 鼠标事件
#include <QStandardPaths>       // 标准路径
#include <QtConcurrent>         // 并发编程

// FFmpeg C接口(多媒体处理)
extern "C" {
#include <libavcodec/avcodec.h>   // 编解码器
#include <libavformat/avformat.h> // 格式处理
#include <libavdevice/avdevice.h> // 设备输入
#include <libswscale/swscale.h>  // 图像缩放
#include <libavutil/avutil.h>    // 工具函数
#include <libavutil/imgutils.h>  // 图像工具
#include <libavutil/opt.h>       // 选项设置
}


MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 初始化 FFmpeg 帧
    frame = av_frame_alloc();

    // 居中窗口
    QDesktopWidget *desktop = QApplication::desktop();
    move((desktop->width() - width()) / 2, (desktop->height() - height()) / 2);

    // 注册设备驱动(如 dshow)
    avdevice_register_all();

    // 创建定时器(用于读取帧)
    timer = new QTimer(this);
    connect(timer, &QTimer::timeout, this, &MainWindow::updateFrame);

    // 设置信号槽连接
    setupConnections();

    // 固定 videoLabel 尺寸,并安装事件过滤器以捕获鼠标/滚轮
    ui->videoLabel->setFixedSize(videoDisplayWidth, videoDisplayHeight);
    ui->videoLabel->setMouseTracking(true);
    ui->videoLabel->installEventFilter(this);

    // 初始刷新摄像头列表
    QTimer::singleShot(100, this, &MainWindow::refreshCameraList);
    fpsTimer.start();// 启动FPS计时器
}

MainWindow::~MainWindow()
{
    closeCamera();
    delete ui;
}

void MainWindow::setupConnections()
{
    // 摄像头控制
    connect(ui->refreshButton, &QPushButton::clicked, this, &MainWindow::refreshCameraList);
    connect(ui->cameraToggleButton, &QPushButton::clicked, this, &MainWindow::toggleCamera);
    connect(ui->cameraComboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
            this, &MainWindow::onCameraSelectionChanged);

    // 手动裁剪复选框
    connect(ui->manualCropCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
        isManualCropMode = checked;// 设置裁剪模式标志
        ui->videoLabel->setCursor(checked ? Qt::CrossCursor : Qt::ArrowCursor);
        if (!checked) {
            // 清除矩形并重绘
            manualCropRect = QRect();
            isDrawingRect = false;
            applyZoom();
        }
    });

    // 拍照按钮
    connect(ui->captureButton, &QPushButton::clicked, this, &MainWindow::on_btnCapture_clicked);
}
//摄像头状态更新
void MainWindow::updateCameraButtonState()
{
    if (isCameraOpened) {
        // 摄像头已打开状态
        ui->cameraToggleButton->setText("关闭摄像头");
        ui->cameraToggleButton->setStyleSheet("QPushButton { background-color: #f44336; color: white; }");
    } else {
        // 摄像头已关闭状态
        ui->cameraToggleButton->setText("打开摄像头");
        ui->cameraToggleButton->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; }");
    }
}

//‌获取编解码器信息
QString MainWindow::getCodecName(AVCodecID codecId)
{
    const char* name = avcodec_get_name(codecId);
    return name ? QString(name) : "unknown";
}
// 获取像素格式名称
QString MainWindow::getPixelFormatName(AVPixelFormat pixFmt)
{
    const char* name = av_get_pix_fmt_name(pixFmt);
    return name ? QString(name) : "unknown";
}
//‌摄像头关闭
void MainWindow::closeCamera()
{
    timer->stop();// 停止定时器

    // 释放 FFmpeg 资源
    if (swsContext) {
        sws_freeContext(swsContext);
        swsContext = nullptr;
    }
    if (highResSwsCtx) {
        sws_freeContext(highResSwsCtx);
        highResSwsCtx = nullptr;
    }
    if (codecContext) {
        avcodec_free_context(&codecContext);
        codecContext = nullptr;
    }
    if (formatContext) {
        avformat_close_input(&formatContext);
        formatContext = nullptr;
    }
    if (options) {
        av_dict_free(&options);
        options = nullptr;
    }
    if (frame) {
        av_frame_free(&frame);
        frame = nullptr;
    }
    if (rgbFrame) {
        av_frame_free(&rgbFrame);
        rgbFrame = nullptr;
    }
    if (rgbBuffer) {
        av_free(rgbBuffer);
        rgbBuffer = nullptr;
    }

    isCameraOpened = false;
    updateCameraButtonState();

    // 启用控件
    ui->cameraComboBox->setEnabled(true);
    ui->resolutionComboBox->setEnabled(true);
    ui->refreshButton->setEnabled(true);

    // 更新 UI 状态
    ui->videoLabel->setText("摄像头已关闭");
    ui->fpsLabel->setText("FPS: 0.00 | 状态: 已关闭");
    ui->statusLabel->setText("摄像头已关闭");
    qDebug() << "摄像头已关闭";
}

bool MainWindow::openCameraWithBestParams(const QString &cameraName, const QString &resolution, const QString &codec, double fps)
{
    // 清理之前的选项
    if (options) {
        av_dict_free(&options);
        options = nullptr;
    }

    // 设置输入参数
    av_dict_set(&options, "video_size", resolution.toUtf8().constData(), 0);
    av_dict_set(&options, "framerate", QString::number(fps, 'f', 0).toUtf8().constData(), 0);
    if (!codec.isEmpty() && codec != "auto") {
        av_dict_set(&options, "vcodec", codec.toUtf8().constData(), 0);
    }

    // 优化延迟和缓冲设置
    av_dict_set(&options, "fflags", "nobuffer", 0);
    av_dict_set(&options, "flags", "low_delay", 0);
    av_dict_set(&options, "framedrop", "1", 0);
    av_dict_set(&options, "rtbufsize", "2M", 0);

    // 分配格式上下文
    formatContext = avformat_alloc_context();
    if (!formatContext) {
        qDebug() << "无法分配 format context";
        return false;
    }

    // 查找 DirectShow 输入格式(Windows)
    const AVInputFormat *inputFormat = av_find_input_format("dshow");
    if (!inputFormat) {
        QMessageBox::warning(this, "错误", "未找到 dshow 输入格式");
        return false;
    }

    QString deviceName = "video=" + cameraName;
    qDebug() << "尝试打开摄像头:" << deviceName;

    int result = avformat_open_input(&formatContext, deviceName.toUtf8().constData(), inputFormat, &options);
    if (result < 0) {
        char error[1024];
        av_strerror(result, error, sizeof(error));
        qDebug() << "打开摄像头失败:" << error;
        return false;
    }

    if (avformat_find_stream_info(formatContext, nullptr) < 0) {
        QMessageBox::warning(this, "错误", "无法获取流信息");
        return false;
    }

    // 查找视频流
    videoStreamIndex = -1;
    for (unsigned int i = 0; i < formatContext->nb_streams; ++i) {
        if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStreamIndex = i;
            break;
        }
    }
    if (videoStreamIndex == -1) {
        QMessageBox::warning(this, "错误", "未找到视频流");
        return false;
    }

    // 获取解码器参数并查找解码器
    AVCodecParameters *codecPar = formatContext->streams[videoStreamIndex]->codecpar;
    videoCodec  = avcodec_find_decoder(codecPar->codec_id);
    if (!videoCodec ) {
        QMessageBox::warning(this, "错误", "未找到合适的解码器");
        return false;
    }
    // 分配解码器上下文
    codecContext = avcodec_alloc_context3(videoCodec);
    if (!codecContext) {
        qDebug() << "无法分配 codec context";
        return false;
    }
    // 将参数复制到解码器上下文
    if (avcodec_parameters_to_context(codecContext, codecPar) < 0) {
        QMessageBox::warning(this, "错误", "无法设置解码器参数");
        return false;
    }

    // 打开解码器
    AVDictionary *codecOpts = nullptr;
    av_dict_set(&codecOpts, "threads", "auto", 0);
    if (avcodec_open2(codecContext, videoCodec, &codecOpts) < 0) {
        QMessageBox::warning(this, "错误", "无法打开解码器");
        av_dict_free(&codecOpts);
        return false;
    }
    av_dict_free(&codecOpts);

    qDebug() << "成功打开摄像头";
    return true;
}
//‌打开摄像头
void MainWindow::openCamera()
{
    if (isCameraOpened) return;// 防止重复打开
    // 检查摄像头选择
    if (ui->cameraComboBox->currentIndex() < 0 || cameraList.isEmpty()) {
        QMessageBox::warning(this, "错误", "请先选择摄像头");
        return;
    }
    if (ui->resolutionComboBox->currentIndex() < 0) {
        QMessageBox::warning(this, "错误", "请先选择分辨率");
        return;
    }

    // 获取选中的分辨率信息
    QVariant var = ui->resolutionComboBox->currentData();
    CameraCapability selectedInfo;
    if (var.isValid()) {
        selectedInfo = var.value<CameraCapability>();
    } else {
        selectedInfo = CameraCapability(QSize(640, 480), "mjpeg", 30.0);
        qWarning() << "使用默认分辨率";
    }
    // 构建参数
    QString resolution = QString("%1x%2").arg(selectedInfo.resolution.width()).arg(selectedInfo.resolution.height());
    QString cameraName = cameraList[ui->cameraComboBox->currentIndex()].name;
    currentCodec = selectedInfo.codec;
    currentMaxFps = selectedInfo.maxFps;

    ui->statusLabel->setText("正在初始化摄像头...");
    QApplication::processEvents();
    // 尝试打开摄像头
    if (!openCameraWithBestParams(cameraName, resolution, selectedInfo.codec, selectedInfo.maxFps)) {
        ui->statusLabel->setText("打开摄像头失败");
        QMessageBox::warning(this, "错误", QString("无法打开摄像头 %1").arg(cameraName));
        return;
    }

    // 创建显示用的缩放上下文(YUV → RGB,1024x768)
    swsContext = sws_getContext(
        codecContext->width, codecContext->height, codecContext->pix_fmt,
        videoDisplayWidth, videoDisplayHeight, AV_PIX_FMT_RGB24,
        SWS_FAST_BILINEAR, nullptr, nullptr, nullptr
    );
    if (!swsContext) {
        QMessageBox::warning(this, "错误", "无法创建显示缩放上下文");
        closeCamera();
        return;
    }

    // 创建高分辨率保存用的缩放上下文(YUV → RGB,原始尺寸)
    highResSwsCtx = sws_getContext(
        codecContext->width, codecContext->height, codecContext->pix_fmt,
        codecContext->width, codecContext->height, AV_PIX_FMT_RGB24,
        SWS_BILINEAR, nullptr, nullptr, nullptr
    );
    if (!highResSwsCtx) {
        QMessageBox::warning(this, "警告", "无法创建高分辨率缩放上下文,拍照可能受影响");
    }

    // 分配显示缓冲区
    rgbBufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGB24, videoDisplayWidth, videoDisplayHeight, 1);
    rgbBuffer = (uint8_t*)av_malloc(rgbBufferSize);
    if (!rgbBuffer) {
        QMessageBox::warning(this, "错误", "无法分配 RGB 缓冲区");
        closeCamera();
        return;
    }
    // 分配RGB帧
    rgbFrame = av_frame_alloc();
    if (!rgbFrame) {
        QMessageBox::warning(this, "错误", "无法分配 RGB 帧");
        closeCamera();
        return;
    }
    // 填充图像数组
    if (av_image_fill_arrays(rgbFrame->data, rgbFrame->linesize, rgbBuffer, AV_PIX_FMT_RGB24,
                             videoDisplayWidth, videoDisplayHeight, 1) < 0) {
        QMessageBox::warning(this, "错误", "无法填充图像数组");
        closeCamera();
        return;
    }

    // 启动摄像头
    isCameraOpened = true;
    frameCount = 0;
    fpsTimer.restart();
    updateCameraButtonState();
    // 禁用相关控件
    ui->cameraComboBox->setEnabled(false);
    ui->resolutionComboBox->setEnabled(false);
    ui->refreshButton->setEnabled(false);
    // 计算定时器间隔并启动
    int timerInterval = qMax(1, (int)(1000.0 / currentMaxFps));
    timer->start(timerInterval);

    ui->videoLabel->setText("正在初始化视频流...");
    ui->statusLabel->setText(QString("摄像头运行中 - %1 - %2fps").arg(currentCodec).arg(currentMaxFps, 0, 'f', 1));
}
//摄像头切换
void MainWindow::toggleCamera()
{
    if (isCameraOpened) {
        closeCamera();
    } else {
        openCamera();
    }
}
//刷新摄像头
void MainWindow::refreshCameraList()
{
    if (isCameraOpened) closeCamera();// 先关闭已打开的摄像头
    // 清空UI列表
    ui->cameraComboBox->clear();
    ui->resolutionComboBox->clear();
    cameraList.clear();
    ui->statusLabel->setText("正在检测摄像头...");
    // 获取可用摄像头列表
    QList<QString> cameraNames = CameraProbing::getAvailableCameras();
    if (cameraNames.isEmpty()) {
        QList<QCameraInfo> cameras = QCameraInfo::availableCameras();
        if (cameras.isEmpty()) {
            ui->statusLabel->setText("未检测到摄像头");
            ui->cameraComboBox->addItem("未找到摄像头");
            return;
        }
        for (const QCameraInfo &info : cameras) {
            CameraInfo cam;
            cam.name = info.description().replace(QRegExp("[\\\\/:]"), "");
            cam.description = info.description();
            cam.deviceName = info.deviceName();
            cam.isDefault = (info == QCameraInfo::defaultCamera());
            cameraList.append(cam);
            ui->cameraComboBox->addItem(cam.description + (cam.isDefault ? " (默认)" : ""));
        }
    } else {
        for (const QString &name : cameraNames) {
            CameraInfo cam;
            cam.name = name;
            cam.description = name;
            cam.deviceName = name;
            cam.isDefault = (&name == &cameraNames.first());
            cameraList.append(cam);
            ui->cameraComboBox->addItem(name + (cam.isDefault ? " (默认)" : ""));
        }
    }

    ui->statusLabel->setText(QString("发现 %1 个摄像头").arg(cameraList.size()));
    if (!cameraList.isEmpty()) {
        ui->cameraComboBox->setCurrentIndex(0);
        onCameraSelectionChanged(0);
    }
}

void MainWindow::onCameraSelectionChanged(int index)
{
    if (index >= 0 && index < cameraList.size()) {
        currentCamera = cameraList[index];
        startCapabilityDetection(currentCamera.name);// 开始探测摄像头能力
    }
}

void MainWindow::startCapabilityDetection(const QString &cameraName)
{
    ui->statusLabel->setText("探测摄像头能力...");
    auto future = QtConcurrent::run([this, cameraName]() {
        CameraProbing prober;
        return prober.probeCameraCapabilities(cameraName);
    });
    // 创建监视器处理异步结果
    QFutureWatcher<QList<CameraCapability>> *watcher = new QFutureWatcher<QList<CameraCapability>>(this);
    connect(watcher, &QFutureWatcher<QList<CameraCapability>>::finished, this, [this, watcher]() {
        onResolutionsReady(watcher->result());
        watcher->deleteLater();
    });
    watcher->setFuture(future);
}

void MainWindow::onResolutionsReady(const QList<CameraCapability> &infos)
{
    populateResolutionComboBox(infos);// 填充分辨率下拉框
    ui->statusLabel->setText(QString("支持 %1 种分辨率").arg(infos.size()));
}

void MainWindow::populateResolutionComboBox(const QList<CameraCapability> &infos)
{
    ui->resolutionComboBox->clear();
    if (infos.isEmpty()) {
        // 添加默认分辨率选项
        CameraCapability def(QSize(640, 480), "mjpeg", 30.0);
        ui->resolutionComboBox->addItem("640x480 - mjpeg - 30.0fps");
        QVariant v;
        v.setValue(def);
        ui->resolutionComboBox->setItemData(0, v);
    } else {
        // 添加所有探测到的分辨率选项
        for (int i = 0; i < infos.size(); ++i) {
            const auto& cap = infos[i];
            QString text = QString("%1x%2 - %3 - %4fps")
                .arg(cap.resolution.width()).arg(cap.resolution.height())
                .arg(cap.codec).arg(cap.maxFps, 0, 'f', 1);
            if (cap.isPreferred) text += " ★";
            ui->resolutionComboBox->addItem(text);
            QVariant v;
            v.setValue(cap);
            ui->resolutionComboBox->setItemData(i, v);
        }
    }
    if (ui->resolutionComboBox->count() > 0) {
        ui->resolutionComboBox->setCurrentIndex(0);
    }
}

// ================== 核心:帧更新与图像处理 ==================
void MainWindow::updateFrame()
{
    if (!isCameraOpened || !formatContext || !codecContext) return;
    // 分配数据包
    AVPacket *packet = av_packet_alloc();
    if (!packet) return;
    // 读取视频帧
    if (av_read_frame(formatContext, packet) >= 0) {
        if (packet->stream_index == videoStreamIndex) {
            // 发送数据包到解码器
            if (avcodec_send_packet(codecContext, packet) == 0) {
                // 接收解码后的帧
                while (avcodec_receive_frame(codecContext, frame) == 0) {
                    // 保存原始高分辨率 RGB 图像(线程安全)
                    {
                        QMutexLocker locker(&frameMutex);
                        if (highResSwsCtx) {
                            // 分配 QImage(RGB888)
                            QImage img(codecContext->width, codecContext->height, QImage::Format_RGB888);
                            uint8_t *data[1] = { img.bits() };
                            int linesize[1] = { img.bytesPerLine() };

                            // YUV → RGB
                            sws_scale(highResSwsCtx, frame->data, frame->linesize,
                                      0, codecContext->height, data, linesize);
                            latestHighResFrame = img;
                        }
                    }

                    // 更新 FPS
                    frameCount++;
                    if (fpsTimer.elapsed() >= 1000) {
                        currentFps = frameCount * 1000.0 / fpsTimer.elapsed();
                        ui->fpsLabel->setText(QString("FPS: %1 | 目标: %2fps | 编码: %3")
                                              .arg(currentFps, 0, 'f', 1)
                                              .arg(currentMaxFps, 0, 'f', 1)
                                              .arg(currentCodec));
                        frameCount = 0;
                        fpsTimer.restart();
                    }

                    // 转换为显示尺寸 RGB
                    sws_scale(swsContext, frame->data, frame->linesize,
                              0, codecContext->height, rgbFrame->data, rgbFrame->linesize);

                    // 创建 QImage 并保存为原始显示图
                    QImage displayImg(rgbFrame->data[0], videoDisplayWidth, videoDisplayHeight,
                                      rgbFrame->linesize[0], QImage::Format_RGB888);
                    originalDisplayPixmap = QPixmap::fromImage(displayImg);

                    // 应用缩放和矩形绘制
                    applyZoom();

                    av_frame_unref(frame);
                    break; // 只处理一帧
                }
            }
        }
        av_packet_unref(packet);
    }
    av_packet_free(&packet);
}

// ================== 缩放与矩形绘制 ==================
void MainWindow::applyZoom()
{
    if (originalDisplayPixmap.isNull()) return;

    // 缩放图像
    QPixmap scaled = originalDisplayPixmap.scaled(
        qRound(originalDisplayPixmap.width() * currentZoomFactor),
        qRound(originalDisplayPixmap.height() * currentZoomFactor),
        Qt::KeepAspectRatio,
        Qt::SmoothTransformation
    );

    // 创建目标 pixmap(固定大小)
    QPixmap target(videoDisplayWidth, videoDisplayHeight);
    target.fill(Qt::black); // 黑色背景

    QPainter painter(&target);
    // 居中绘制
    QPoint drawPos((videoDisplayWidth - scaled.width()) / 2,
                   (videoDisplayHeight - scaled.height()) / 2);
    painter.drawPixmap(drawPos, scaled);

    // 绘制手动裁剪矩形(如果启用)
    if (isManualCropMode) {
        QRect rectToDraw;
        bool isTemp = false;

        if (isDrawingRect) {
            rectToDraw = QRect(rectStart, rectEnd).normalized();
            isTemp = true;
        } else if (!manualCropRect.isEmpty()) {
            rectToDraw = manualCropRect;
        }

        if (!rectToDraw.isEmpty()) {
            double scale = currentZoomFactor;
            int offsetX = (videoDisplayWidth - scaled.width()) / 2;
            int offsetY = (videoDisplayHeight - scaled.height()) / 2;

            QRectF mapped(
                rectToDraw.x() * scale + offsetX,
                rectToDraw.y() * scale + offsetY,
                rectToDraw.width() * scale,
                rectToDraw.height() * scale
            );

            if (isTemp) {
                painter.setPen(QPen(Qt::yellow, 2, Qt::DashLine));
                painter.setBrush(QColor(255, 255, 0, 50));
            } else {
                painter.setPen(QPen(Qt::red, 3, Qt::SolidLine));
                painter.setBrush(Qt::NoBrush);
            }
            painter.drawRect(mapped);
        }
    }

    painter.end();
    ui->videoLabel->setPixmap(target);
}

// ================== 鼠标与滚轮事件 ==================
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
{
    if (obj == ui->videoLabel) {
        // 滚轮缩放(所有模式)
        if (event->type() == QEvent::Wheel) {
            QWheelEvent *wheel = static_cast<QWheelEvent*>(event);
            double delta = wheel->angleDelta().y();
            double factor = delta > 0 ? 1.1 : 0.9;
            currentZoomFactor *= factor;
            currentZoomFactor = qBound(0.3, currentZoomFactor, 3.0);
            applyZoom();
            return true;
        }

        // 手动裁剪模式下的鼠标事件
        if (isManualCropMode) {
            switch (event->type()) {
            case QEvent::MouseButtonPress:
                handleMousePressEvent(static_cast<QMouseEvent*>(event));
                return true;
            case QEvent::MouseMove:
                handleMouseMoveEvent(static_cast<QMouseEvent*>(event));
                return true;
            case QEvent::MouseButtonRelease:
                handleMouseReleaseEvent(static_cast<QMouseEvent*>(event));
                return true;
            default:
                break;
            }
        }
    }
    return QMainWindow::eventFilter(obj, event);
}

void MainWindow::handleMousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        isDrawingRect = true;
        rectStart = event->pos();
        rectEnd = rectStart;
        manualCropRect = QRect();
        applyZoom();
    } else if (event->button() == Qt::RightButton) {
        manualCropRect = QRect();
        isDrawingRect = false;
        applyZoom();
    }
}

void MainWindow::handleMouseMoveEvent(QMouseEvent *event)
{
    if (isDrawingRect) {
        rectEnd = event->pos();
        applyZoom(); // 实时更新
    }
}

void MainWindow::handleMouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton && isDrawingRect) {
        isDrawingRect = false;
        QRect finalRect = QRect(rectStart, rectEnd).normalized();
        if (finalRect.width() >= 20 && finalRect.height() >= 20) {
            manualCropRect = finalRect;
        } else {
            manualCropRect = QRect(); // 太小,忽略
        }
        applyZoom();
    }
}

// ================== 拍照功能 ==================
void MainWindow::on_btnCapture_clicked()
{
    QMutexLocker locker(&frameMutex);
    if (latestHighResFrame.isNull()) {
        qDebug() << "暂无图像可保存!";
        return;
    }

    QString desktop = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
    QString fileName = desktop + "/capture_" + QDateTime::currentDateTime().toString("yyyyMMdd_hhmmss") + ".png";

    if (isManualCropMode && !manualCropRect.isEmpty()) {
        // 将 videoLabel 坐标系的矩形映射回原始高分辨率坐标
        double scaleX = (double)latestHighResFrame.width() / videoDisplayWidth;
        double scaleY = (double)latestHighResFrame.height() / videoDisplayHeight;

        int x = qRound(manualCropRect.x() * scaleX);
        int y = qRound(manualCropRect.y() * scaleY);
        int w = qRound(manualCropRect.width() * scaleX);
        int h = qRound(manualCropRect.height() * scaleY);

        // 边界检查
        x = qMax(0, x);
        y = qMax(0, y);
        w = qMin(latestHighResFrame.width() - x, w);
        h = qMin(latestHighResFrame.height() - y, h);

        if (w > 0 && h > 0) {
            QImage cropped = latestHighResFrame.copy(x, y, w, h);
            if (cropped.save(fileName)) {
                qDebug() << "裁剪图片已保存到桌面!";
            } else {
                qDebug() << "保存失败!";
            }
        } else {
            qDebug() << "裁剪区域无效!";
        }
    } else {
        // 保存完整图像
        if (latestHighResFrame.save(fileName)) {
            qDebug() << "完整图片已保存到桌面!";
        } else {
            qDebug() << "保存失败!";
        }
    }
}

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>1070</width>
    <height>967</height>
   </rect>
  </property>
  <property name="maximumSize">
   <size>
    <width>1070</width>
    <height>967</height>
   </size>
  </property>
  <property name="windowTitle">
   <string>FFmpeg摄像头应用 - MJPG格式</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <widget class="QGroupBox" name="controlGroup">
      <property name="title">
       <string>摄像头控制</string>
      </property>
      <layout class="QGridLayout" name="gridLayout">
       <item row="1" column="0">
        <widget class="QLabel" name="resolutionLabel">
         <property name="text">
          <string>分辨率:</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
         </property>
        </widget>
       </item>
       <item row="0" column="0">
        <widget class="QLabel" name="cameraLabel">
         <property name="text">
          <string>摄像头:</string>
         </property>
         <property name="alignment">
          <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
         </property>
        </widget>
       </item>
       <item row="1" column="1">
        <widget class="QComboBox" name="resolutionComboBox"/>
       </item>
       <item row="0" column="1">
        <widget class="QComboBox" name="cameraComboBox"/>
       </item>
       <item row="0" column="2">
        <widget class="QPushButton" name="refreshButton">
         <property name="text">
          <string>刷新列表</string>
         </property>
        </widget>
       </item>
       <item row="1" column="2">
        <widget class="QPushButton" name="cameraToggleButton">
         <property name="styleSheet">
          <string notr="true">QPushButton { background-color: #4CAF50; color: white; }</string>
         </property>
         <property name="text">
          <string>打开摄像头</string>
         </property>
        </widget>
       </item>
       <item row="0" column="3">
        <widget class="QPushButton" name="captureButton">
         <property name="text">
          <string>拍照</string>
         </property>
        </widget>
       </item>
       <item row="1" column="3">
        <widget class="QCheckBox" name="manualCropCheckBox">
         <property name="text">
          <string>手动裁剪区域</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <widget class="QGroupBox" name="videoGroup">
      <property name="title">
       <string>视频预览</string>
      </property>
      <layout class="QVBoxLayout" name="verticalLayout_2">
       <item>
        <widget class="QLabel" name="videoLabel">
         <property name="minimumSize">
          <size>
           <width>1024</width>
           <height>768</height>
          </size>
         </property>
         <property name="maximumSize">
          <size>
           <width>1024</width>
           <height>768</height>
          </size>
         </property>
         <property name="styleSheet">
          <string notr="true">border: 1px solid gray; background-color: black;</string>
         </property>
         <property name="text">
          <string>摄像头未打开</string>
         </property>
         <property name="scaledContents">
          <bool>false</bool>
         </property>
         <property name="alignment">
          <set>Qt::AlignCenter</set>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
       <widget class="QLabel" name="fpsLabel">
        <property name="text">
         <string>FPS: 0.00 | 状态: 就绪</string>
        </property>
       </widget>
      </item>
      <item>
       <spacer name="horizontalSpacer">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>40</width>
          <height>20</height>
         </size>
        </property>
       </spacer>
      </item>
      <item>
       <widget class="QLabel" name="statusLabel">
        <property name="text">
         <string/>
        </property>
       </widget>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

引用的结构体头文件

复制代码
//metatypes.h
// metatypes.h - 修改后的版本
#ifndef METATYPES_H
#define METATYPES_H

#include <QSize>
#include <QString>
#include <QMetaType>

// 摄像头能力信息结构体
struct CameraCapability {
    QSize resolution;
    QString codec;          // 编码格式
    double maxFps;          // 最大帧率
    QString pixelFormat;    // 像素格式
    bool isPreferred;       // 是否为首选(MJPG等)

    CameraCapability() : resolution(640, 480), codec("mjpeg"), maxFps(30.0),
                        pixelFormat("yuyv422"), isPreferred(false) {}

    CameraCapability(QSize res, QString c, double fps, QString pf = "yuyv422", bool preferred = false)
        : resolution(res), codec(c), maxFps(fps), pixelFormat(pf), isPreferred(preferred) {}
};

// 摄像头信息结构体
struct CameraInfo {
    QString name;           // 摄像头名称(用于FFmpeg)
    QString description;    // 摄像头描述
    QString deviceName;     // 设备名称
    bool isDefault;         // 是否是默认摄像头

    CameraInfo() : isDefault(false) {}

    CameraInfo(QString n, QString desc, QString dev, bool isDef = false)
        : name(n), description(desc), deviceName(dev), isDefault(isDef) {}
};

// 必须添加元类型声明
Q_DECLARE_METATYPE(CameraCapability)
Q_DECLARE_METATYPE(CameraInfo)

#endif // METATYPES_H

引用的类

复制代码
//cameraprobing.h
#ifndef CAMERAPROBING_H
#define CAMERAPROBING_H

#include <QObject>
#include <QString>
#include <QSize>
#include <QList>
#include <QProcess>
#include <QTextStream>
#include <QRegularExpression>
#include <QDebug>

// 包含元类型定义(CameraCapability等结构体)
#include "metatypes.h"

// 摄像头能力探测类
class CameraProbing : public QObject
{
    Q_OBJECT  // Qt元对象系统宏,支持信号槽机制

public:
    // 构造函数
    explicit CameraProbing(QObject *parent = nullptr);

    // 主探测函数:探测指定摄像头的支持能力
    QList<CameraCapability> probeCameraCapabilities(const QString &cameraName);

    // 静态函数:获取系统中可用的摄像头列表
    static QList<QString> getAvailableCameras();

private:
    // 解析FFmpeg/FFplay输出的私有函数
    QList<CameraCapability> parseFFmpegOutput(const QString &output, const QString &cameraName);

    // 使用FFplay探测摄像头能力(备用方法)
    QList<CameraCapability> probeWithFFplay(const QString &cameraName);

    // 使用DirectShow探测摄像头能力(主要方法)
    QList<CameraCapability> probeWithDirectShow(const QString &cameraName);

    // 执行FFmpeg命令的静态辅助函数
    static QString executeFFmpegCommand(const QStringList &arguments);

    // 通用进程执行函数
    static QString executeProcess(const QString &program, const QStringList &arguments, int timeoutMs = 10000);

};

#endif // CAMERAPROBING_H

//cameraprobing.cpp
#include "cameraprobing.h"

// 构造函数
CameraProbing::CameraProbing(QObject *parent) : QObject(parent)
{
}

// 主探测函数:探测摄像头支持的能力(分辨率、编码格式、帧率等)
QList<CameraCapability> CameraProbing::probeCameraCapabilities(const QString &cameraName)
{
    qDebug() << "=== 开始探测摄像头能力: " << cameraName << "===";

    QList<CameraCapability> capabilities;

    // 方法1: 使用DirectShow探测(Windows平台)
    qDebug() << "尝试使用DirectShow探测...";
    capabilities = probeWithDirectShow(cameraName);

    if (!capabilities.isEmpty()) {
        qDebug() << "通过DirectShow探测到" << capabilities.size() << "个能力";
        return capabilities;
    }

    // 方法2: 如果DirectShow失败,使用FFplay探测(备用方法)
    qDebug() << "DirectShow探测失败,尝试使用FFplay探测...";
    capabilities = probeWithFFplay(cameraName);

    if (!capabilities.isEmpty()) {
        qDebug() << "通过FFplay探测到" << capabilities.size() << "个能力";
        return capabilities;
    }

    // 如果所有方法都失败,返回默认能力配置
    qDebug() << "无法探测摄像头能力,使用默认值";
    // 返回默认能力:三种常见配置
    capabilities << CameraCapability(QSize(640, 480), "yuyv422", 30.0, "yuyv422", false)  // 640x480, YUYV422, 30fps
                << CameraCapability(QSize(1280, 720), "yuyv422", 15.0, "yuyv422", false) // 1280x720, YUYV422, 15fps
                << CameraCapability(QSize(1920, 1080), "mjpeg", 10.0, "yuyv422", true);  // 1920x1080, MJPEG, 10fps(首选)

    return capabilities;
}

// 使用DirectShow探测摄像头能力(Windows专用)
QList<CameraCapability> CameraProbing::probeWithDirectShow(const QString &cameraName)
{
    QList<CameraCapability> capabilities;

    // 构建FFmpeg命令参数:使用dshow格式列出摄像头选项
    QStringList arguments;
    arguments << "-f" << "dshow" << "-list_options" << "true" << "-i" << QString("video=%1").arg(cameraName);

    qDebug() << "执行FFmpeg命令: ffmpeg" << arguments;

    // 执行FFmpeg命令获取摄像头能力信息
    QString output = executeFFmpegCommand(arguments);

    if (output.isEmpty()) {
        qDebug() << "FFmpeg探测无输出";
        return capabilities;
    }

    qDebug() << "FFmpeg原始输出:\n" << output;
    qDebug() << "=== 开始解析FFmpeg输出 ===";

    // 解析FFmpeg输出,提取摄像头能力信息
    return parseFFmpegOutput(output, cameraName);
}

// 解析FFmpeg输出的摄像头能力信息
QList<CameraCapability> CameraProbing::parseFFmpegOutput(const QString &output, const QString &cameraName)
{
    QList<CameraCapability> capabilities;

    // 使用文本流逐行读取输出
    QTextStream stream(const_cast<QString*>(&output), QIODevice::ReadOnly);
    QString line;

    qDebug() << "=== 开始解析FFmpeg输出,查找所有格式 ===";

    // 定义多种正则表达式模式来匹配不同的输出格式
    QRegularExpression pattern1(R"(pixel_format=(\w+)\s+min s=(\d+)x(\d+)\s+fps=(\d+))");  // 格式1:像素格式在前
    QRegularExpression pattern2(R"(vcodec=(\w+)\s+min s=(\d+)x(\d+)\s+fps=(\d+))");         // 格式2:编码格式在前
    QRegularExpression pattern3(R"(min s=(\d+)x(\d+)\s+fps=(\d+)\s+pixel_format=(\w+))");   // 格式3:分辨率在前
    QRegularExpression pattern4(R"(min s=(\d+)x(\d+)\s+fps=(\d+)\s+vcodec=(\w+))");        // 格式4:分辨率在前,编码格式在后

    // 专门匹配MJPG格式的模式(MJPG通常有更好的性能)
    QRegularExpression mjpegPattern(R"((mjpeg|mjpg|MJPEG|MJPG).*?min s=(\d+)x(\d+).*?fps=(\d+))",
                                   QRegularExpression::CaseInsensitiveOption);

    QSet<QString> uniqueConfigs;  // 用于去重,避免重复配置
    int lineNumber = 0;           // 行号计数器,用于调试
    int configCount = 0;          // 配置计数器

    // 逐行解析输出
    while (stream.readLineInto(&line)) {
        lineNumber++;
        line = line.trimmed();  // 去除首尾空白字符

        qDebug() << "检查行" << lineNumber << ":" << line;

        CameraCapability cap;  // 临时存储解析出的能力信息
        bool matched = false;  // 标记是否成功匹配

        // 优先匹配MJPG格式(MJPG通常性能更好)
        QRegularExpressionMatch mjpegMatch = mjpegPattern.match(line);
        if (mjpegMatch.hasMatch()) {
            QString codec = mjpegMatch.captured(1).toLower();  // 获取编码格式并转为小写
            int width = mjpegMatch.captured(2).toInt();        // 宽度
            int height = mjpegMatch.captured(3).toInt();       // 高度
            double fps = mjpegMatch.captured(4).toDouble();    // 帧率

            // 设置能力信息
            cap.codec = "mjpeg";
            cap.pixelFormat = "mjpeg";
            cap.resolution = QSize(width, height);
            cap.maxFps = fps;
            cap.isPreferred = true;  // MJPG格式标记为首选

            matched = true;
            qDebug() << "匹配到MJPG格式:" << width << "x" << height << "fps:" << fps;
        }

        // 如果没有匹配到MJPG,尝试匹配其他格式
        if (!matched) {
            QRegularExpressionMatch match;

            // 按优先级尝试不同的正则表达式模式
            if (pattern1.match(line).hasMatch()) match = pattern1.match(line);
            else if (pattern2.match(line).hasMatch()) match = pattern2.match(line);
            else if (pattern3.match(line).hasMatch()) match = pattern3.match(line);
            else if (pattern4.match(line).hasMatch()) match = pattern4.match(line);

            if (match.hasMatch()) {
                QString formatOrCodec = match.captured(1).toLower();  // 获取格式或编码
                int width, height;
                double fps;

                // 根据匹配的模式调整参数捕获位置
                if (match.regularExpression() == pattern1 || match.regularExpression() == pattern2) {
                    width = match.captured(2).toInt();
                    height = match.captured(3).toInt();
                    fps = match.captured(4).toDouble();
                } else {
                    width = match.captured(1).toInt();
                    height = match.captured(2).toInt();
                    fps = match.captured(3).toDouble();
                    formatOrCodec = match.captured(4).toLower();
                }

                // 确定是编码格式还是像素格式,并设置相应属性
                if (formatOrCodec == "mjpeg" || formatOrCodec == "mjpg") {
                    cap.codec = "mjpeg";
                    cap.pixelFormat = "mjpeg";
                    cap.isPreferred = true;  // MJPG格式优先
                } else {
                    cap.codec = formatOrCodec;
                    cap.pixelFormat = formatOrCodec;
                    cap.isPreferred = false;  // 其他格式不优先
                }

                cap.resolution = QSize(width, height);
                cap.maxFps = fps;
                matched = true;

                qDebug() << "匹配到格式:" << formatOrCodec << width << "x" << height << "fps:" << fps;
            }
        }

        // 如果成功匹配到配置信息
        if (matched) {
            // 生成唯一标识键,用于去重
            QString configKey = QString("%1x%2-%3-%4").arg(cap.resolution.width())
                                                    .arg(cap.resolution.height())
                                                    .arg(cap.codec)
                                                    .arg(cap.maxFps);

            // 检查是否已存在相同配置,避免重复添加
            if (!uniqueConfigs.contains(configKey)) {
                uniqueConfigs.insert(configKey);      // 添加到已存在集合
                capabilities.append(cap);             // 添加到能力列表
                configCount++;                        // 计数器递增

                qDebug() << "添加配置" << configCount << ":"
                         << cap.resolution.width() << "x" << cap.resolution.height()
                         << "-" << cap.codec << "-" << cap.maxFps << "fps"
                         << "- 首选:" << cap.isPreferred;
            }
        }
    }

    qDebug() << "=== 解析完成,共找到" << capabilities.size() << "个配置 ===";

    // 如果没有找到任何配置,使用默认值作为后备方案
    if (capabilities.isEmpty()) {
        qDebug() << "未找到任何配置,使用默认值";
        capabilities << CameraCapability(QSize(640, 480), "yuyv422", 30.0, "yuyv422", false)
                    << CameraCapability(QSize(1280, 720), "yuyv422", 15.0, "yuyv422", false)
                    << CameraCapability(QSize(1920, 1080), "mjpeg", 10.0, "mjpeg", true);
    }

    // 对配置进行排序:MJPG格式优先,然后按分辨率从大到小排序
    std::sort(capabilities.begin(), capabilities.end(),
        [](const CameraCapability& a, const CameraCapability& b) {
            // 优先选择MJPG格式
            if (a.isPreferred && !b.isPreferred) return true;
            if (!a.isPreferred && b.isPreferred) return false;

            // 相同格式下按分辨率面积排序(从大到小)
            int areaA = a.resolution.width() * a.resolution.height();
            int areaB = b.resolution.width() * b.resolution.height();
            return areaA > areaB;
        });

    // 打印最终排序后的配置列表
    qDebug() << "=== 最终配置列表 ===";
    for (int i = 0; i < capabilities.size(); ++i) {
        const CameraCapability& cap = capabilities[i];
        qDebug() << "配置" << (i+1) << ":" << cap.resolution.width() << "x" << cap.resolution.height()
                 << "-" << cap.codec << "-" << cap.maxFps << "fps"
                 << (cap.isPreferred ? "(首选)" : "");
    }

    return capabilities;
}

// 使用FFplay探测摄像头能力(备用方法)
QList<CameraCapability> CameraProbing::probeWithFFplay(const QString &cameraName)
{
    QList<CameraCapability> capabilities;

    qDebug() << "尝试使用FFplay探测摄像头:" << cameraName;

    // 构建FFplay命令参数(与FFmpeg类似)
    QStringList arguments;
    arguments << "-f" << "dshow" << "-list_options" << "true" << "-i" << QString("video=%1").arg(cameraName);

    // 执行FFplay命令
    QString output = executeProcess("ffplay", arguments, 5000);

    if (output.isEmpty()) {
        qDebug() << "FFplay探测无输出";
        return capabilities;
    }

    qDebug() << "FFplay原始输出:\n" << output;

    // 使用相同的解析逻辑处理FFplay输出
    return parseFFmpegOutput(output, cameraName);
}

// 执行FFmpeg命令的封装函数
QString CameraProbing::executeFFmpegCommand(const QStringList &arguments)
{
    return executeProcess("ffmpeg", arguments, 15000); // 设置15秒超时
}

// 通用进程执行函数
QString CameraProbing::executeProcess(const QString &program, const QStringList &arguments, int timeoutMs)
{
    QProcess process;
    process.setProcessChannelMode(QProcess::MergedChannels); // 合并标准输出和错误输出

    qDebug() << "执行命令:" << program << arguments;

    // 启动进程
    process.start(program, arguments);

    // 等待进程启动(3秒超时)
    if (!process.waitForStarted(3000)) {
        qDebug() << "无法启动" << program << "进程";
        return QString();
    }

    // 等待进程完成(使用指定的超时时间)
    if (!process.waitForFinished(timeoutMs)) {
        qDebug() << program << "进程超时";
        process.kill();  // 超时后终止进程
        return QString();
    }

    // 读取进程输出
    QString output = QString::fromLocal8Bit(process.readAll());
    qDebug() << "进程退出码:" << process.exitCode();
    qDebug() << "进程输出长度:" << output.length();

    return output;
}

// 获取系统中可用的摄像头列表
QList<QString> CameraProbing::getAvailableCameras()
{
    QList<QString> cameras;

    // 构建FFmpeg命令来列出设备
    QStringList arguments;
    arguments << "-f" << "dshow" << "-list_devices" << "true" << "-i" << "dummy";

    // 执行命令获取摄像头列表
    QString output = executeFFmpegCommand(arguments);

    if (output.isEmpty()) {
        qDebug() << "获取摄像头列表失败";
        return cameras;
    }

    qDebug() << "摄像头列表原始输出:\n" << output;

    // 解析输出,提取摄像头名称
    QTextStream stream(const_cast<QString*>(&output), QIODevice::ReadOnly);
    QString line;

    // 定义正则表达式模式来匹配摄像头名称
    QRegularExpression cameraPattern(R"(\""(.*?)\""\s\(video\))");      // 模式1
    QRegularExpression cameraPattern2(R"(\[dshow.*\]\s+\""(.*?)\""\s\(video\))"); // 模式2

    // 逐行解析输出
    while (stream.readLineInto(&line)) {
        QRegularExpressionMatch match = cameraPattern.match(line);
        if (!match.hasMatch()) {
            match = cameraPattern2.match(line);  // 尝试第二种模式
        }
        if (match.hasMatch()) {
            QString cameraName = match.captured(1);  // 提取摄像头名称
            cameras.append(cameraName);
            qDebug() << "发现摄像头:" << cameraName;
        }
    }

    return cameras;
}

代码可以在资源中下载。

相关推荐
kkoral10 分钟前
【FFmpeg 智慧园区场景应用】1.实战命令清单
ffmpeg
天虎13 分钟前
使用VS2019编译ShiftMediaProject版本FFmpeg
ffmpeg
努力学习的小廉1 小时前
【QT(九)】—— 窗口
数据库·qt·系统架构
kkoral1 小时前
【FFmpeg 智慧园区场景应用】2.自动化处理 Shell 脚本
运维·ffmpeg·自动化
火山上的企鹅1 小时前
QGC 中修改原生 Android 串口 BUG 实操
qt·串口·qgc·无人机开发
一只小bit2 小时前
Qt 多媒体:快速解决音视频播放问题
前端·c++·qt·音视频·cpp·页面
程序小馆2 小时前
Qt cmake add_subdirectory 后无法使用子模块的资源(如图片、翻译文件)的解决方案
开发语言·qt
努力学习的小廉2 小时前
【QT(十)】—— 系统
开发语言·qt
誰能久伴不乏2 小时前
Qt 启动时序与事件循环:为什么监控启动不要放在构造函数里,以及 `QTimer::singleShot(0, ...)` 到底做了什么
c语言·c++·qt
少控科技2 小时前
QT高阶日记011
开发语言·qt