**前言:**本文为手把手教学的基于 ESP32_CAM 与 Qt Creator 的智能视频监控项目,项目使用的 MCU 为乐鑫的 ESP32_CAM 搭配 Qt Creator 制作上位机,Qt 的版本为 Qt 5.9.0。本项目的智能 ESP32 Camera 拥有多种视频格式解码、WIFI 灯源控制、WIFI Camera 和智能预警等功能。项目分为上位机与下位机两部分的代码编程,也包含简单的图像算法设计,算是一个 ESP32 很好的练手项目。希望这篇博文能给读者朋友的工程项目给予些许帮助,Respect(代码开源)!
**硬件与软件:**ESP32_CAM、iKun ESP32 Camera Studio、Arduino IDE、Qt 5.9.0
项目结果图:


一、ESP32_CAM 智能监控项目
1.1 ESP32_CAM 概述
ESP32_CAM 是一款超小型、低成本、集成 WiFi + 蓝牙 + 摄像头 的物联网视觉模组,由 AI-Thinker(安信可)主导设计,核心基于乐鑫 ESP32 SoC;无板载 USB-TTL,需外接串口模块烧录;采用 DIP-16 封装,可直插面包板 / 专用底板,是嵌入式视觉原型与量产小批量方案的热门选择。乐鑫官方为 ESP32_CAM 提供了标准的摄像头例程,研发工程师可以根据自己的需求进行摄像头相关的开发。
核心硬件规格:
- 主控:ESP32(常见 WROVER-E 版本,双核 Xtensa LX6,最高 240MHz,600 DMIPS;520KB SRAM + 4MB Flash + 4MB PSRAM);支持 2.4GHz WiFi(802.11b/g/n)、蓝牙 4.2(BLE/BR/EDR);支持 STA/AP/STA+AP,SmartConfig/AirKiss 配网,OTA 升级
- 摄像头:标配 OV2640 200 万像素(UXGA 1600×1200),DVP 接口;输出 YUV422、RGB565、JPEG;板载 LED 补光灯;部分兼容 OV7670
- 尺寸与供电:27×40.5×4.5 mm;推荐 5V DC 供电(3.3V 易导致摄像头工作异常);深度睡眠功耗约 6mA
- 扩展:MicroSD 卡槽本地存储;IO 资源因 DVP 占用较多,剩余引脚有限

1.2 ESP32_CAM 智能监控项目概述
本篇博客项目中作者制作的 ikun ESP32 Camera 拥有上位机与下位机,上位机为 Qt Creator 编写的 ikun ESP32 Camera Studio 软件,下位机为乐鑫官方发布的 ESP32_CAM。本项目基于 ESP32_CAM 与 Qt Creator的智能视频监控项目共有 4 项功能,包括:1、WIFI Camera 功能;2、多种视频流解码;3、远程 HTTP 控灯;4、视频流智能预警;
1、WIFI Camera:利用 Qt Creator 的 QNetworkAccessManager 函数来拉取 ESP32_CAM 发送过来的视频流;

2、多种视频流解码:使用 Qt Creator 提供的多种多媒体视频解码库兼容 ESP32_CAM 端发送过来的各种视频流;

3、远程 HTTP 控灯:ESP32CAM 作为 HTTP 服务器,监听指定端口,ESP32_CAM 利用 HTTP 请求,发送给 ESP32 的 IP 地址;

4、视频流智能预警:针对视频流进行像素级比较,计算RGB差异,当差异超过阈值时判定为图像变化,并保存预警照片;智能预警功能可以有效地监控家中出现未知异动之后都情况,亦如陌生人闯入或者火灾等情况,个人认为还是比较有用的功能!

二、ESP32_CAM 例程代码引用
乐鑫官方给 ESP32_CAM 提供了很多编写开发的 IDE 选择,包括:Arduino IDE(ESP32 库)和 ESP-IDF(乐鑫官方 SDK)。作者这里使用 Arduino IDE 进行 ESP_CAM 的开发。
1、本篇博客工程代码的基本框架可以从 Arduino IDE 的示例教程中获取,后续的项目代码在此基础上进行修改删减即可。读者朋友在正确导入ESP32工程库后,按照下图去创建项目的基础框架:

2、使用 Arduino IDE 对示例进行编译,确保当前编译调试环境是 OK 的;

3、将 Arduino IDE 示例代码进行保存,方便进行后续代码编写;

作者补充:
ESP32_CAM 开发板必须将 GPIO0 接地进入下载模式;串口 TX/RX 交叉、GND 共地;供电不足是烧录失败 / 复位的高频原因
三、ESP32 CAM Video Sur 代码与解析
3.1 代码库文件引入与变量定义
#include <WebServer.h> 引入 WebServer 库,方便 ESP32 启用 HTTP
#define CAMERA_MODEL_AI_THINKER // Has PSRAM 启用乐鑫的 AI_THINKER
#define LED_pin 4 ESP32_CAM 的 LED 是GPIO4(乐鑫官方提供原理图)
const char* ssid = "TP-LINK_E386"; ESP32_CAM 连接的 WIFI 名称
const char* password = "13852200640"; ESP32_CAM 连接的 WIFI 密码
WebServer server(80); 创建Web服务器对象,监听80端口(HTTP默认端口)

3.2 WIFI ESP32 Camera 代码
WIFI ESP32 Camera 这部分功能代码作者直接使用了乐鑫官方提供的代码,读者朋友根据本篇博客的第 2 章节去进行操作即可!作者这边针对官方提供的源码进行简单的解析!
乐鑫官方提供的 ESP32_CAM 例程代码核心就是 setup() 函数,包括:WIFI Camera 的 GPIO 引脚配置、摄像头的视频流格式、摄像头的数据格式初始化设置和 WIFI 连接等

cpp
void setup() {
Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();
Serial.println("iKun ESP32 CAM Video Sur By:混分巨兽龙某某");
//设置LED1引脚为输出模式
pinMode(LED_pin, OUTPUT);
//LED1引脚输出低电平,灯灭
digitalWrite(LED_pin, LOW);
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG; // for streaming
//config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
// if PSRAM IC present, init with UXGA resolution and higher JPEG quality
// for larger pre-allocated frame buffer.
if(config.pixel_format == PIXFORMAT_JPEG){
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// Limit the frame size when PSRAM is not available
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
} else {
// Best option for face detection/recognition
config.frame_size = FRAMESIZE_240X240;
#if CONFIG_IDF_TARGET_ESP32S3
config.fb_count = 2;
#endif
}
#if defined(CAMERA_MODEL_ESP_EYE)
pinMode(13, INPUT_PULLUP);
pinMode(14, INPUT_PULLUP);
#endif
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
sensor_t * s = esp_camera_sensor_get();
// initial sensors are flipped vertically and colors are a bit saturated
if (s->id.PID == OV3660_PID) {
s->set_vflip(s, 1); // flip it back
s->set_brightness(s, 1); // up the brightness just a bit
s->set_saturation(s, -2); // lower the saturation
}
// drop down frame size for higher initial frame rate
if(config.pixel_format == PIXFORMAT_JPEG){
s->set_framesize(s, FRAMESIZE_QVGA);
}
#if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM)
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
#endif
#if defined(CAMERA_MODEL_ESP32S3_EYE)
s->set_vflip(s, 1);
#endif
WiFi.begin(ssid, password);
WiFi.setSleep(false);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
startCameraServer();
Serial.print("Camera Ready! Use 'http://");
Serial.print(WiFi.localIP());
Serial.println("' to connect");
// LED灯控制
server.on("/control", handleControlRequest); // LED控制接口
// 启动服务器
server.begin();
Serial.println("HTTP服务器已启动");
}
3.3 ESP32_CAM 的 WIFI 控制灯代码
乐鑫官方提供的 ESP32_CAM 代码可以稳定运行 WIFI Camera 功能,并且预留了很多网络协议的库,这边作者引入 WebServer.h 库进行 HTTP 协议的 LED 小灯控制!
根据乐鑫官方提供的 ESP32_CAM 原理图,我们可以发送控制板载的 LED 的 GPIO 引脚为 GPIO4,我们只需要在 Qt 端上位机发送指令去控制 ESP32_CAM 的 GPIO 引脚高低电平即可。

作者编写了一个简单的 HTTP 服务器,监听 /control 接口,解析 var=led&val=40 或 var=led&val=41 参数,并控制 LED 灯。代码的关键核心:
1、WiFi 配置:你需要把 ssid 和 password 替换成自己的 WiFi 名称和密码,这样 ESP32-CAM 才能接入局域网;
2、LED 引脚:ESP32-CAM 的板载 LED 通常接在 GPIO4,且是低电平点亮(LOW 亮、HIGH 灭),如果你的硬件不同,需要调整 digitalWrite 的 HIGH/LOW。
3、HTTP 接口处理:服务器监听 /control 路径,对应 QT 端的 http://IP/control?var=led\&val=40 请求;通过 server.arg("val") 获取 QT 发送的参数值(40/41)。
cpp
// 处理/control接口的请求
void handleControlRequest() {
// 检查是否有var参数,且值为led
if (server.hasArg("var") && server.arg("var") == "led") {
// 检查是否有val参数
if (server.hasArg("val")) {
String valStr = server.arg("val");
int val = valStr.toInt();
// 根据参数值控制LED
if (val == 40) {
// val=40 关闭LED
digitalWrite(LED_pin, LOW); // 注意:ESP32-CAM的LED可能是低电平点亮,根据实际情况调整HIGH/LOW
Serial.printf("LED已关闭 (val=%d)\n", val);
server.send(200, "text/plain", "LED OFF"); // 返回成功响应
} else if (val == 41) {
// val=41 打开LED
digitalWrite(LED_pin, HIGH);
Serial.printf("LED已打开 (val=%d)\n", val);
server.send(200, "text/plain", "LED ON"); // 返回成功响应
} else {
// 无效的参数值
Serial.printf("无效的LED参数值: %d\n", val);
server.send(400, "text/plain", "Invalid LED value"); // 返回错误响应
}
} else {
// 缺少val参数
server.send(400, "text/plain", "Missing 'val' parameter");
}
} else {
// 不是控制LED的请求
server.send(400, "text/plain", "Not a LED control request");
}
}
void loop() {
// 处理客户端的HTTP请求
server.handleClient();
delay(10);
}
3.4 ESP32_CAM 的完整代码
cpp
/********************************** (C) COPYRIGHT *******************************
* File Name : CameraWebServer.ino
* Author : 混分巨兽龙某某
* Version : V1.0.0
* Date : 2026/02/21
* Description : Intelligent Video Monitoring Project Based on ESP32_CAM and Qt Creator
********************************************************************************/
#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>
#define CAMERA_MODEL_AI_THINKER // Has PSRAM
#include "camera_pins.h"
#define LED_pin 4
const char* ssid = "TP-LINK_E386";
const char* password = "xxxxxxxx";
// 创建Web服务器对象,监听80端口(HTTP默认端口)
WebServer server(80);
void startCameraServer();
// 处理/control接口的请求
void handleControlRequest() {
// 检查是否有var参数,且值为led
if (server.hasArg("var") && server.arg("var") == "led") {
// 检查是否有val参数
if (server.hasArg("val")) {
String valStr = server.arg("val");
int val = valStr.toInt();
// 根据参数值控制LED
if (val == 40) {
// val=40 关闭LED
digitalWrite(LED_pin, LOW); // 注意:ESP32-CAM的LED可能是低电平点亮,根据实际情况调整HIGH/LOW
Serial.printf("LED已关闭 (val=%d)\n", val);
server.send(200, "text/plain", "LED OFF"); // 返回成功响应
} else if (val == 41) {
// val=41 打开LED
digitalWrite(LED_pin, HIGH);
Serial.printf("LED已打开 (val=%d)\n", val);
server.send(200, "text/plain", "LED ON"); // 返回成功响应
} else {
// 无效的参数值
Serial.printf("无效的LED参数值: %d\n", val);
server.send(400, "text/plain", "Invalid LED value"); // 返回错误响应
}
} else {
// 缺少val参数
server.send(400, "text/plain", "Missing 'val' parameter");
}
} else {
// 不是控制LED的请求
server.send(400, "text/plain", "Not a LED control request");
}
}
void setup() {
Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();
Serial.println("iKun ESP32 CAM Video Sur By:混分巨兽龙某某");
//设置LED1引脚为输出模式
pinMode(LED_pin, OUTPUT);
//LED1引脚输出低电平,灯灭
digitalWrite(LED_pin, LOW);
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG; // for streaming
//config.pixel_format = PIXFORMAT_RGB565; // for face detection/recognition
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
// if PSRAM IC present, init with UXGA resolution and higher JPEG quality
// for larger pre-allocated frame buffer.
if(config.pixel_format == PIXFORMAT_JPEG){
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// Limit the frame size when PSRAM is not available
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
} else {
// Best option for face detection/recognition
config.frame_size = FRAMESIZE_240X240;
#if CONFIG_IDF_TARGET_ESP32S3
config.fb_count = 2;
#endif
}
#if defined(CAMERA_MODEL_ESP_EYE)
pinMode(13, INPUT_PULLUP);
pinMode(14, INPUT_PULLUP);
#endif
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
sensor_t * s = esp_camera_sensor_get();
// initial sensors are flipped vertically and colors are a bit saturated
if (s->id.PID == OV3660_PID) {
s->set_vflip(s, 1); // flip it back
s->set_brightness(s, 1); // up the brightness just a bit
s->set_saturation(s, -2); // lower the saturation
}
// drop down frame size for higher initial frame rate
if(config.pixel_format == PIXFORMAT_JPEG){
s->set_framesize(s, FRAMESIZE_QVGA);
}
#if defined(CAMERA_MODEL_M5STACK_WIDE) || defined(CAMERA_MODEL_M5STACK_ESP32CAM)
s->set_vflip(s, 1);
s->set_hmirror(s, 1);
#endif
#if defined(CAMERA_MODEL_ESP32S3_EYE)
s->set_vflip(s, 1);
#endif
WiFi.begin(ssid, password);
WiFi.setSleep(false);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
startCameraServer();
Serial.print("Camera Ready! Use 'http://");
Serial.print(WiFi.localIP());
Serial.println("' to connect");
// LED灯控制
server.on("/control", handleControlRequest); // LED控制接口
// 启动服务器
server.begin();
Serial.println("HTTP服务器已启动");
}
void loop() {
// 处理客户端的HTTP请求
server.handleClient();
delay(10);
}
四、ESP32 Camera Studio
4.1 WIFI Camera 部分代码
作者这边利用 Qt 的多媒体库从而可以轻松去解码各种视频流协议,支持的格式如下:

cpp
/**
* @file mainwindow.cpp
* @brief ESP32-CAM QT客户端主窗口实现
*
* 该文件实现了ESP32-CAM网络摄像头的视频流显示、分辨率调整等功能
*/
/**
* @def WINDOW_TITLE
* @brief 窗口标题宏定义
*/
#define WINDOW_TITLE "iKun ESP32 Camera Studio"
/**
* @brief MainWindow构造函数
* @param parent 父窗口指针
*
* 初始化UI界面,设置窗口标题,添加分辨率选项,请求ESP32-CAM状态
*/
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_ledState(40)
, m_safetyEnabled(false)
, m_previousImage()
, m_lastCaptureTime(QDateTime::currentDateTime())
{
// 初始化UI界面
ui->setupUi(this);
// 设置窗口标题
setWindowTitle(WINDOW_TITLE);
// 分辨率选项列表
QStringList vPixelList = {
"96x96",
"QQVGA(160x120)",
"QCIF(176x144)",
"HQVGA(240x176)",
"240x240",
"QVGA(320x240)",
"CIF(400x296)",
"HVGA(480x320)",
"VGA(640x480)",
"SVGA(800x600)",
"XGA(1024x768)",
"HD(1280x720)",
"SXGA(1280x1024)",
"UXGA(1600x1200)",
};
// 添加分辨率选项到下拉框
ui->cbxPixel->addItems(vPixelList);
// 请求ESP32-CAM当前状态
RequestStatus();
}
/**
* @brief MainWindow析构函数
*
* 释放UI资源
*/
MainWindow::~MainWindow()
{
delete ui;
}
开始取流的按键代码:
cpp
/**
* @brief 开始取流按钮点击事件处理
*
* 连接到ESP32-CAM的视频流,开始获取视频数据
*/
void MainWindow::on_btnStart_clicked()
{
// 如果已经连接,直接返回
if (m_pvClient != nullptr) {
return;
}
// 获取IP地址
const QString sIPAddr = ui->txtAddr->text();
if (sIPAddr.size() == 0) {
QMessageBox::warning(this, "提示", "请输入IP地址");
return;
}
// 构建视频流URL
const QString sURL = "http://" + sIPAddr + ":81/stream";
// 创建网络访问管理器
QNetworkAccessManager *pvManager = new QNetworkAccessManager();
// 创建网络请求
QNetworkRequest vRequest;
vRequest.setUrl(sURL);
vRequest.setRawHeader("Connection", "Keep-Alive");
vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
// 发送GET请求获取视频流
m_pvClient = pvManager->get(vRequest);
// 连接readyRead信号,处理接收到的数据
connect(m_pvClient, &QNetworkReply::readyRead, this, &MainWindow::readyRead);
}
结束取流的按键代码:
cpp
/**
* @brief 结束取流按钮点击事件处理
*
* 断开与ESP32-CAM的连接,停止获取视频数据
*/
void MainWindow::on_btnStop_clicked()
{
if (m_pvClient != nullptr) {
// 断开信号连接
disconnect(m_pvClient);
// 中止请求
m_pvClient->abort();
// 延迟删除
m_pvClient->deleteLater();
// 重置指针
m_pvClient = nullptr;
// 清空缓冲区
m_buffer.clear();
}
}
视频流分辨率的解码:
cpp
/**
* @brief 请求ESP32-CAM状态
*
* 获取ESP32-CAM的当前状态,包括分辨率等信息
*/
void MainWindow::RequestStatus()
{
// 清空缓冲区
m_buffer.clear();
// 获取IP地址
const QString sIPAddr = ui->txtAddr->text();
if (sIPAddr.size() == 0) {
return;
}
// 构建状态请求URL
const QString sUrl = "http://" + sIPAddr + "/status";
qDebug() << sUrl;
// 创建网络访问管理器
QNetworkAccessManager manager;
// 创建网络请求
QNetworkRequest vRequest;
QNetworkReply *reply;
vRequest.setUrl(QUrl(sUrl));
vRequest.setRawHeader("Connection", "Keep-Alive");
vRequest.setRawHeader("Cache-Control", "no-cache");
vRequest.setRawHeader("Pragma", "no-cache");
vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
// 发送GET请求
reply = manager.get(vRequest);
// 创建事件循环,等待请求完成
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// 处理响应
if (reply->error() == QNetworkReply::NoError) {
// 读取响应数据
QString jsonString = reply->readAll();
// 解析JSON数据
QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonString.toUtf8());
if (!jsonDocument.isNull()) {
if (jsonDocument.isObject()) {
QJsonObject jsonObject = jsonDocument.object();
// 获取分辨率索引
int framesize = jsonObject["framesize"].toInt();
// 设置下拉框当前索引
ui->cbxPixel->setCurrentIndex(framesize);
}
}
} else {
// 输出错误信息
qDebug() << "Error: " << reply->errorString();
}
// 释放回复对象
reply->deleteLater();
}
/**
* @brief 分辨率下拉框选择变化事件处理
* @param index 选择的分辨率索引
*
* 当用户选择不同的分辨率时,发送请求到ESP32-CAM更改分辨率
*/
void MainWindow::on_cbxPixel_currentIndexChanged(int index)
{
// 静态变量,用于跳过初始化时的第一次触发
static bool isFirstSend = true;
if (isFirstSend) {
isFirstSend = false;
return;
}
// 获取IP地址
const QString sIPAddr = ui->txtAddr->text();
if (sIPAddr.size() == 0) {
return;
}
// 构建分辨率更改请求URL
const QString sUrl = "http://" + sIPAddr + "/control?var=framesize&val=" + QString::number(index);
qDebug() << sUrl;
// 创建网络访问管理器
QNetworkAccessManager manager;
// 创建网络请求
QNetworkRequest vRequest;
QNetworkReply *reply;
vRequest.setUrl(QUrl(sUrl));
vRequest.setRawHeader("Connection", "Keep-Alive");
vRequest.setRawHeader("Cache-Control", "no-cache");
vRequest.setRawHeader("Pragma", "no-cache");
vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
// 发送GET请求
reply = manager.get(vRequest);
// 清空缓冲区
m_mutex.lock();
m_buffer.clear();
m_mutex.unlock();
// 创建事件循环,等待请求完成
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// 处理错误
if (reply->error() != QNetworkReply::NoError) {
qDebug() << "Error: " << reply->errorString();
}
// 释放回复对象
reply->deleteLater();
}
/**
* @brief 网络数据就绪事件处理
*
* 当接收到网络数据时触发,处理MJPEG视频流数据,解析并显示视频帧
*/
void MainWindow::readyRead()
{
// 如果没有网络回复对象,直接返回
if (m_pvClient == nullptr) {
return;
}
// 读取网络数据到缓冲区
m_mutex.lock();
m_buffer.append(m_pvClient->readAll());
m_mutex.unlock();
// MJPEG流标记
const QString sStartKey = "Content-Type: image/jpeg\r\nContent-Length:";
const QString sEndKey = "\r\n--123456789000000000000987654321\r\n";
// 循环处理缓冲区中的数据
while (true) {
// 查找JPEG图像的开始标记
int nStartPos = m_buffer.indexOf(sStartKey);
if (nStartPos < 0) {
break;
}
// 查找JPEG图像的结束标记
int nEndPos = m_buffer.indexOf(sEndKey, nStartPos);
if (nEndPos < 0) {
break;
}
// 调整开始位置
nStartPos = m_buffer.indexOf("\r\n");
if (nStartPos < 0) {
break;
}
// 计算结束位置
nEndPos += sEndKey.size();
// 提取JPEG图像数据
QByteArray vBuf = m_buffer.mid(nStartPos, nEndPos - nStartPos);
if (vBuf.size() == 0) {
continue;
}
// 解析并显示图像
ParseFrame(vBuf);
// 从缓冲区中移除已处理的数据
m_mutex.lock();
m_buffer = m_buffer.mid(nEndPos, m_buffer.size() - nEndPos);
m_mutex.unlock();
}
}
4.2 控灯部分代码
控制 LED 灯的方法有很多,作者这边利用了 QNetworkAccessManager 发送 HTTP GET 请求,使用QEventLoop等待网络请求完成。Qt 端上位机捕获并处理网络请求错误,在成功后切换LED状态,实现40和41的循环。作者补充:本人为了防止出现 HTTP 传输异常的情况,默认连续使用 2 次的 QNetworkAccessManager 发送 HTTP GET 请求,保证了 Qt 端数据的可以稳定控制下位机 ESP32_CAM 的 LED,读者朋友们平时也可以这样操作!
cpp
/**
* @brief LED控制按钮点击事件处理
*
* 控制ESP32-CAM上的LED灯开关,实现参数循环切换:40 -> 41 -> 40 -> ...
* 仅发送一次请求,不进行重试
*/
void MainWindow::on_btnLED_clicked()
{
// 获取IP地址
const QString sIPAddr = ui->txtAddr->text();
if (sIPAddr.size() == 0) {
QMessageBox::warning(this, "提示", "请输入IP地址");
return;
}
// 构建LED控制URL,使用当前m_ledState值
const QString sUrl = "http://" + sIPAddr + "/control?var=led&val=" + QString::number(m_ledState);
qDebug() << "LED control URL:" << sUrl;
// 创建网络访问管理器
QNetworkAccessManager manager;
// 创建网络请求
QNetworkRequest vRequest;
vRequest.setUrl(QUrl(sUrl));
vRequest.setRawHeader("Connection", "Keep-Alive");
vRequest.setRawHeader("Cache-Control", "no-cache");
vRequest.setRawHeader("Pragma", "no-cache");
vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
vRequest.setRawHeader("Accept", "*/*");
// 发送GET请求
QNetworkReply *reply = manager.get(vRequest);
// 创建事件循环,等待请求完成
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// 处理响应
if (reply->error() != QNetworkReply::NoError) {
// 创建网络访问管理器(设置2级重传机制,避免网络异常开灯失败)
QNetworkAccessManager manager1;
// 创建网络请求
QNetworkRequest vRequest1;
vRequest1.setUrl(QUrl(sUrl));
vRequest1.setRawHeader("Connection", "Keep-Alive");
vRequest1.setRawHeader("Cache-Control", "no-cache");
vRequest1.setRawHeader("Pragma", "no-cache");
vRequest1.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
vRequest1.setRawHeader("Accept", "*/*");
// 发送GET请求
QNetworkReply *reply = manager1.get(vRequest1);
// 创建事件循环,等待请求完成
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// 处理响应
if (reply->error() != QNetworkReply::NoError) {
qDebug() << "LED control error: " << reply->errorString();
QMessageBox::warning(this, "错误", "控制LED失败: " + reply->errorString());
reply->deleteLater();
}else{
qDebug() << "LED control successful, val:" << m_ledState;
// 切换LED状态:40 -> 41 -> 40 -> ...
m_ledState = (m_ledState == 40) ? 41 : 40;
reply->deleteLater();
}
} else {
qDebug() << "LED control successful, val:" << m_ledState;
// 切换LED状态:40 -> 41 -> 40 -> ...
m_ledState = (m_ledState == 40) ? 41 : 40;
reply->deleteLater();
}
}
4.3 智能预警部分代码
作者针对这种很实用的智能预警功能进行了很巧妙的图像算法设计(智能预警:可以用于监控家中是否进入陌生人或者火灾等异常状况)。
智能预警的功能实现:
智能预警检测 :当 boxSafety 复选框被勾选时,启用智能预警功能
图像变化检测 :每隔 1 秒比较前后两帧图像,检测是否发生变化
自动保存 :当检测到图像变化时,自动保存当前画面到桌面的 "ikun ESP32 Camera" 文件夹
文件夹自动创建 :每次运行时会自动创建保存文件夹,确保文件能够正确保存
智能预警的技术核心:
图像比较算法 :使用像素级比较,计算RGB差异,当差异超过阈值时判定为图像变化
时间间隔控制 :使用QDateTime记录时间戳,确保每1秒进行一次比较
文件系统操作 :使用QDir创建文件夹,QStandardPaths获取桌面路径
多线程安全 :使用QMutex保护共享资源

cpp
/**
* @brief 比较两个图像是否发生了变化
* @param img1 第一个图像
* @param img2 第二个图像
* @return 是否发生了变化
*/
bool MainWindow::compareImages(const QImage &img1, const QImage &img2)
{
// 检查图像是否有效
if (img1.isNull() || img2.isNull()) {
return false;
}
// 检查图像大小是否相同
if (img1.size() != img2.size()) {
return true;
}
// 计算图像差异
int diffCount = 0;
int threshold = 10; // 像素差异阈值
int maxDiff = img1.width() * img1.height() * 0.05; // 最大差异像素数(5%)
for (int y = 0; y < img1.height(); y++) {
for (int x = 0; x < img1.width(); x++) {
QRgb pixel1 = img1.pixel(x, y);
QRgb pixel2 = img2.pixel(x, y);
// 计算RGB差异
int rDiff = qAbs(qRed(pixel1) - qRed(pixel2));
int gDiff = qAbs(qGreen(pixel1) - qGreen(pixel2));
int bDiff = qAbs(qBlue(pixel1) - qBlue(pixel2));
// 如果差异超过阈值,增加差异计数
if (rDiff > threshold || gDiff > threshold || bDiff > threshold) {
diffCount++;
// 如果差异超过最大允许值,直接返回true
if (diffCount > maxDiff) {
return true;
}
}
}
}
return diffCount > maxDiff;
}
/**
* @brief 保存图像到指定文件夹
* @param img 要保存的图像
*/
void MainWindow::saveImage(const QImage &img)
{
// 创建保存目录
QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
QString saveDir = desktopPath + "/ikun ESP32 Camera";
QDir dir(saveDir);
if (!dir.exists()) {
if (!dir.mkpath(saveDir)) {
qDebug() << "Failed to create save directory";
return;
}
}
// 生成文件名(基于时间戳)
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss_zzz");
QString filename = saveDir + "/capture_" + timestamp + ".jpg";
// 保存图像
if (img.save(filename)) {
qDebug() << "Image saved to:" << filename;
} else {
qDebug() << "Failed to save image";
}
}
/**
* @brief 解析视频帧数据
* @param vBuf 包含JPEG图像的数据
*
* 从MJPEG流中提取并解析单个JPEG图像,然后显示在UI界面上
* 当智能预警功能启用时,检测前后1秒图像的变化并保存变化的画面
*/
void MainWindow::ParseFrame(const QByteArray &vBuf)
{
// 定义常量
const QString sBodyKey = "\r\n\r\n";
const QString sItemKey = "\r\n";
const QString sLengthKey = "Content-Length";
// 查找body的起始位置
int nBodyIndex = vBuf.indexOf(sBodyKey);
if (nBodyIndex <= 0) {
return;
}
// 提取头部信息
QString sHead = vBuf.mid(0, nBodyIndex);
QStringList vHeadSplit = sHead.split(sItemKey);
// 解析头部信息到映射表
QMap<QString, QString> vHeadMap;
for (const QString &vItem : vHeadSplit) {
QStringList vItemSplit = vItem.split(": ");
if (vItemSplit.size() == 2) {
vHeadMap[vItemSplit[0]] = vItemSplit[1];
}
}
// 获取图像数据长度
uint32_t nLength = 0;
if (vHeadMap.count(sLengthKey) > 0) {
nLength = vHeadMap[sLengthKey].toUInt();
}
if (nLength == 0) {
return;
}
// 提取图像数据
QByteArray sBody = vBuf.mid(nBodyIndex + sBodyKey.size(), nLength);
// 加载图像数据
QPixmap vPixmap;
if (!vPixmap.loadFromData(sBody)) {
qDebug() << "Failed to load image";
return;
}
// 调整图像大小以适应显示区域
QPixmap vImage = vPixmap.scaled(ui->lblVideo->width(), ui->lblVideo->height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
// 设置标签的大小策略
ui->lblVideo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// 显示图像
ui->lblVideo->setPixmap(vImage);
// 智能预警功能
if (m_safetyEnabled) {
// 转换为QImage用于比较
QImage currentImage = vPixmap.toImage();
// 获取当前时间
QDateTime currentTime = QDateTime::currentDateTime();
// 计算与上次捕获的时间差
qint64 elapsed = m_lastCaptureTime.msecsTo(currentTime);
// 每隔1秒进行一次图像比较
if (elapsed >= 1000) {
// 检查是否有前一秒的图像可比较
if (!m_previousImage.isNull()) {
// 比较前后图像是否发生了变化
if (compareImages(m_previousImage, currentImage)) {
// 图像发生变化,保存当前画面
saveImage(currentImage);
}
}
// 更新前一秒图像和时间戳
m_previousImage = currentImage;
m_lastCaptureTime = currentTime;
}
}
}
4.4 ESP32 Camera Studio 的完整代码
cpp
/********************************** (C) COPYRIGHT *******************************
* File Name : CameraWebServer.ino
* Author : 混分巨兽龙某某
* Version : V1.0.0
* Date : 2026/02/21
* Description : Intelligent Video Monitoring Project Based on ESP32_CAM and Qt Creator
********************************************************************************/
#include "mainwindow.h"
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkRequest>
#include <QDebug>
#include <QPixmap>
#include <QMessageBox>
#include <QComboBox>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QThread>
#include <QStandardPaths>
#include <QDir>
#include <QImage>
#include "ui_mainwindow.h"
/**
* @file mainwindow.cpp
* @brief ESP32-CAM QT客户端主窗口实现
*
* 该文件实现了ESP32-CAM网络摄像头的视频流显示、分辨率调整等功能
*/
/**
* @def WINDOW_TITLE
* @brief 窗口标题宏定义
*/
#define WINDOW_TITLE "iKun ESP32 Camera Studio"
/**
* @brief MainWindow构造函数
* @param parent 父窗口指针
*
* 初始化UI界面,设置窗口标题,添加分辨率选项,请求ESP32-CAM状态
*/
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
, m_ledState(40)
, m_safetyEnabled(false)
, m_previousImage()
, m_lastCaptureTime(QDateTime::currentDateTime())
{
// 初始化UI界面
ui->setupUi(this);
// 设置窗口标题
setWindowTitle(WINDOW_TITLE);
// 分辨率选项列表
QStringList vPixelList = {
"96x96",
"QQVGA(160x120)",
"QCIF(176x144)",
"HQVGA(240x176)",
"240x240",
"QVGA(320x240)",
"CIF(400x296)",
"HVGA(480x320)",
"VGA(640x480)",
"SVGA(800x600)",
"XGA(1024x768)",
"HD(1280x720)",
"SXGA(1280x1024)",
"UXGA(1600x1200)",
};
// 添加分辨率选项到下拉框
ui->cbxPixel->addItems(vPixelList);
// 请求ESP32-CAM当前状态
RequestStatus();
}
/**
* @brief MainWindow析构函数
*
* 释放UI资源
*/
MainWindow::~MainWindow()
{
delete ui;
}
/**
* @brief 开始取流按钮点击事件处理
*
* 连接到ESP32-CAM的视频流,开始获取视频数据
*/
void MainWindow::on_btnStart_clicked()
{
// 如果已经连接,直接返回
if (m_pvClient != nullptr) {
return;
}
// 获取IP地址
const QString sIPAddr = ui->txtAddr->text();
if (sIPAddr.size() == 0) {
QMessageBox::warning(this, "提示", "请输入IP地址");
return;
}
// 构建视频流URL
const QString sURL = "http://" + sIPAddr + ":81/stream";
// 创建网络访问管理器
QNetworkAccessManager *pvManager = new QNetworkAccessManager();
// 创建网络请求
QNetworkRequest vRequest;
vRequest.setUrl(sURL);
vRequest.setRawHeader("Connection", "Keep-Alive");
vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
// 发送GET请求获取视频流
m_pvClient = pvManager->get(vRequest);
// 连接readyRead信号,处理接收到的数据
connect(m_pvClient, &QNetworkReply::readyRead, this, &MainWindow::readyRead);
}
/**
* @brief 结束取流按钮点击事件处理
*
* 断开与ESP32-CAM的连接,停止获取视频数据
*/
void MainWindow::on_btnStop_clicked()
{
if (m_pvClient != nullptr) {
// 断开信号连接
disconnect(m_pvClient);
// 中止请求
m_pvClient->abort();
// 延迟删除
m_pvClient->deleteLater();
// 重置指针
m_pvClient = nullptr;
// 清空缓冲区
m_buffer.clear();
}
}
/**
* @brief 比较两个图像是否发生了变化
* @param img1 第一个图像
* @param img2 第二个图像
* @return 是否发生了变化
*/
bool MainWindow::compareImages(const QImage &img1, const QImage &img2)
{
// 检查图像是否有效
if (img1.isNull() || img2.isNull()) {
return false;
}
// 检查图像大小是否相同
if (img1.size() != img2.size()) {
return true;
}
// 计算图像差异
int diffCount = 0;
int threshold = 10; // 像素差异阈值
int maxDiff = img1.width() * img1.height() * 0.05; // 最大差异像素数(5%)
for (int y = 0; y < img1.height(); y++) {
for (int x = 0; x < img1.width(); x++) {
QRgb pixel1 = img1.pixel(x, y);
QRgb pixel2 = img2.pixel(x, y);
// 计算RGB差异
int rDiff = qAbs(qRed(pixel1) - qRed(pixel2));
int gDiff = qAbs(qGreen(pixel1) - qGreen(pixel2));
int bDiff = qAbs(qBlue(pixel1) - qBlue(pixel2));
// 如果差异超过阈值,增加差异计数
if (rDiff > threshold || gDiff > threshold || bDiff > threshold) {
diffCount++;
// 如果差异超过最大允许值,直接返回true
if (diffCount > maxDiff) {
return true;
}
}
}
}
return diffCount > maxDiff;
}
/**
* @brief 保存图像到指定文件夹
* @param img 要保存的图像
*/
void MainWindow::saveImage(const QImage &img)
{
// 创建保存目录
QString desktopPath = QStandardPaths::writableLocation(QStandardPaths::DesktopLocation);
QString saveDir = desktopPath + "/ikun ESP32 Camera";
QDir dir(saveDir);
if (!dir.exists()) {
if (!dir.mkpath(saveDir)) {
qDebug() << "Failed to create save directory";
return;
}
}
// 生成文件名(基于时间戳)
QString timestamp = QDateTime::currentDateTime().toString("yyyyMMdd_HHmmss_zzz");
QString filename = saveDir + "/capture_" + timestamp + ".jpg";
// 保存图像
if (img.save(filename)) {
qDebug() << "Image saved to:" << filename;
} else {
qDebug() << "Failed to save image";
}
}
/**
* @brief 解析视频帧数据
* @param vBuf 包含JPEG图像的数据
*
* 从MJPEG流中提取并解析单个JPEG图像,然后显示在UI界面上
* 当智能预警功能启用时,检测前后1秒图像的变化并保存变化的画面
*/
void MainWindow::ParseFrame(const QByteArray &vBuf)
{
// 定义常量
const QString sBodyKey = "\r\n\r\n";
const QString sItemKey = "\r\n";
const QString sLengthKey = "Content-Length";
// 查找body的起始位置
int nBodyIndex = vBuf.indexOf(sBodyKey);
if (nBodyIndex <= 0) {
return;
}
// 提取头部信息
QString sHead = vBuf.mid(0, nBodyIndex);
QStringList vHeadSplit = sHead.split(sItemKey);
// 解析头部信息到映射表
QMap<QString, QString> vHeadMap;
for (const QString &vItem : vHeadSplit) {
QStringList vItemSplit = vItem.split(": ");
if (vItemSplit.size() == 2) {
vHeadMap[vItemSplit[0]] = vItemSplit[1];
}
}
// 获取图像数据长度
uint32_t nLength = 0;
if (vHeadMap.count(sLengthKey) > 0) {
nLength = vHeadMap[sLengthKey].toUInt();
}
if (nLength == 0) {
return;
}
// 提取图像数据
QByteArray sBody = vBuf.mid(nBodyIndex + sBodyKey.size(), nLength);
// 加载图像数据
QPixmap vPixmap;
if (!vPixmap.loadFromData(sBody)) {
qDebug() << "Failed to load image";
return;
}
// 调整图像大小以适应显示区域
QPixmap vImage = vPixmap.scaled(ui->lblVideo->width(), ui->lblVideo->height(), Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
// 设置标签的大小策略
ui->lblVideo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// 显示图像
ui->lblVideo->setPixmap(vImage);
// 智能预警功能
if (m_safetyEnabled) {
// 转换为QImage用于比较
QImage currentImage = vPixmap.toImage();
// 获取当前时间
QDateTime currentTime = QDateTime::currentDateTime();
// 计算与上次捕获的时间差
qint64 elapsed = m_lastCaptureTime.msecsTo(currentTime);
// 每隔1秒进行一次图像比较
if (elapsed >= 1000) {
// 检查是否有前一秒的图像可比较
if (!m_previousImage.isNull()) {
// 比较前后图像是否发生了变化
if (compareImages(m_previousImage, currentImage)) {
// 图像发生变化,保存当前画面
saveImage(currentImage);
}
}
// 更新前一秒图像和时间戳
m_previousImage = currentImage;
m_lastCaptureTime = currentTime;
}
}
}
/**
* @brief 请求ESP32-CAM状态
*
* 获取ESP32-CAM的当前状态,包括分辨率等信息
*/
void MainWindow::RequestStatus()
{
// 清空缓冲区
m_buffer.clear();
// 获取IP地址
const QString sIPAddr = ui->txtAddr->text();
if (sIPAddr.size() == 0) {
return;
}
// 构建状态请求URL
const QString sUrl = "http://" + sIPAddr + "/status";
qDebug() << sUrl;
// 创建网络访问管理器
QNetworkAccessManager manager;
// 创建网络请求
QNetworkRequest vRequest;
QNetworkReply *reply;
vRequest.setUrl(QUrl(sUrl));
vRequest.setRawHeader("Connection", "Keep-Alive");
vRequest.setRawHeader("Cache-Control", "no-cache");
vRequest.setRawHeader("Pragma", "no-cache");
vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
// 发送GET请求
reply = manager.get(vRequest);
// 创建事件循环,等待请求完成
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// 处理响应
if (reply->error() == QNetworkReply::NoError) {
// 读取响应数据
QString jsonString = reply->readAll();
// 解析JSON数据
QJsonDocument jsonDocument = QJsonDocument::fromJson(jsonString.toUtf8());
if (!jsonDocument.isNull()) {
if (jsonDocument.isObject()) {
QJsonObject jsonObject = jsonDocument.object();
// 获取分辨率索引
int framesize = jsonObject["framesize"].toInt();
// 设置下拉框当前索引
ui->cbxPixel->setCurrentIndex(framesize);
}
}
} else {
// 输出错误信息
qDebug() << "Error: " << reply->errorString();
}
// 释放回复对象
reply->deleteLater();
}
/**
* @brief 分辨率下拉框选择变化事件处理
* @param index 选择的分辨率索引
*
* 当用户选择不同的分辨率时,发送请求到ESP32-CAM更改分辨率
*/
void MainWindow::on_cbxPixel_currentIndexChanged(int index)
{
// 静态变量,用于跳过初始化时的第一次触发
static bool isFirstSend = true;
if (isFirstSend) {
isFirstSend = false;
return;
}
// 获取IP地址
const QString sIPAddr = ui->txtAddr->text();
if (sIPAddr.size() == 0) {
return;
}
// 构建分辨率更改请求URL
const QString sUrl = "http://" + sIPAddr + "/control?var=framesize&val=" + QString::number(index);
qDebug() << sUrl;
// 创建网络访问管理器
QNetworkAccessManager manager;
// 创建网络请求
QNetworkRequest vRequest;
QNetworkReply *reply;
vRequest.setUrl(QUrl(sUrl));
vRequest.setRawHeader("Connection", "Keep-Alive");
vRequest.setRawHeader("Cache-Control", "no-cache");
vRequest.setRawHeader("Pragma", "no-cache");
vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
// 发送GET请求
reply = manager.get(vRequest);
// 清空缓冲区
m_mutex.lock();
m_buffer.clear();
m_mutex.unlock();
// 创建事件循环,等待请求完成
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// 处理错误
if (reply->error() != QNetworkReply::NoError) {
qDebug() << "Error: " << reply->errorString();
}
// 释放回复对象
reply->deleteLater();
}
/**
* @brief 网络数据就绪事件处理
*
* 当接收到网络数据时触发,处理MJPEG视频流数据,解析并显示视频帧
*/
void MainWindow::readyRead()
{
// 如果没有网络回复对象,直接返回
if (m_pvClient == nullptr) {
return;
}
// 读取网络数据到缓冲区
m_mutex.lock();
m_buffer.append(m_pvClient->readAll());
m_mutex.unlock();
// MJPEG流标记
const QString sStartKey = "Content-Type: image/jpeg\r\nContent-Length:";
const QString sEndKey = "\r\n--123456789000000000000987654321\r\n";
// 循环处理缓冲区中的数据
while (true) {
// 查找JPEG图像的开始标记
int nStartPos = m_buffer.indexOf(sStartKey);
if (nStartPos < 0) {
break;
}
// 查找JPEG图像的结束标记
int nEndPos = m_buffer.indexOf(sEndKey, nStartPos);
if (nEndPos < 0) {
break;
}
// 调整开始位置
nStartPos = m_buffer.indexOf("\r\n");
if (nStartPos < 0) {
break;
}
// 计算结束位置
nEndPos += sEndKey.size();
// 提取JPEG图像数据
QByteArray vBuf = m_buffer.mid(nStartPos, nEndPos - nStartPos);
if (vBuf.size() == 0) {
continue;
}
// 解析并显示图像
ParseFrame(vBuf);
// 从缓冲区中移除已处理的数据
m_mutex.lock();
m_buffer = m_buffer.mid(nEndPos, m_buffer.size() - nEndPos);
m_mutex.unlock();
}
}
/**
* @brief LED控制按钮点击事件处理
*
* 控制ESP32-CAM上的LED灯开关,实现参数循环切换:40 -> 41 -> 40 -> ...
* 仅发送一次请求,不进行重试
*/
void MainWindow::on_btnLED_clicked()
{
// 获取IP地址
const QString sIPAddr = ui->txtAddr->text();
if (sIPAddr.size() == 0) {
QMessageBox::warning(this, "提示", "请输入IP地址");
return;
}
// 构建LED控制URL,使用当前m_ledState值
const QString sUrl = "http://" + sIPAddr + "/control?var=led&val=" + QString::number(m_ledState);
qDebug() << "LED control URL:" << sUrl;
// 创建网络访问管理器
QNetworkAccessManager manager;
// 创建网络请求
QNetworkRequest vRequest;
vRequest.setUrl(QUrl(sUrl));
vRequest.setRawHeader("Connection", "Keep-Alive");
vRequest.setRawHeader("Cache-Control", "no-cache");
vRequest.setRawHeader("Pragma", "no-cache");
vRequest.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
vRequest.setRawHeader("Accept", "*/*");
// 发送GET请求
QNetworkReply *reply = manager.get(vRequest);
// 创建事件循环,等待请求完成
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// 处理响应
if (reply->error() != QNetworkReply::NoError) {
// 创建网络访问管理器(设置2级重传机制,避免网络异常开灯失败)
QNetworkAccessManager manager1;
// 创建网络请求
QNetworkRequest vRequest1;
vRequest1.setUrl(QUrl(sUrl));
vRequest1.setRawHeader("Connection", "Keep-Alive");
vRequest1.setRawHeader("Cache-Control", "no-cache");
vRequest1.setRawHeader("Pragma", "no-cache");
vRequest1.setRawHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0");
vRequest1.setRawHeader("Accept", "*/*");
// 发送GET请求
QNetworkReply *reply = manager1.get(vRequest1);
// 创建事件循环,等待请求完成
QEventLoop loop;
QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
loop.exec();
// 处理响应
if (reply->error() != QNetworkReply::NoError) {
qDebug() << "LED control error: " << reply->errorString();
QMessageBox::warning(this, "错误", "控制LED失败: " + reply->errorString());
reply->deleteLater();
}else{
qDebug() << "LED control successful, val:" << m_ledState;
// 切换LED状态:40 -> 41 -> 40 -> ...
m_ledState = (m_ledState == 40) ? 41 : 40;
reply->deleteLater();
}
} else {
qDebug() << "LED control successful, val:" << m_ledState;
// 切换LED状态:40 -> 41 -> 40 -> ...
m_ledState = (m_ledState == 40) ? 41 : 40;
reply->deleteLater();
}
}
/**
* @brief 智能预警复选框状态变化事件处理
* @param arg1 复选框的状态(Qt::Checked或Qt::Unchecked)
*
* 启用或禁用智能预警功能
*/
void MainWindow::on_boxSafety_stateChanged(int arg1)
{
// 检查复选框是否被选中
if (arg1 == Qt::Checked) {
// 启用智能预警功能
m_safetyEnabled = true;
qDebug() << "智能预警功能已启用";
// 重置时间戳和前一帧图像
m_lastCaptureTime = QDateTime::currentDateTime();
m_previousImage = QImage();
// 显示提示信息
QMessageBox::information(this, "提示", "智能预警功能已启用\n当检测到图像变化时,会自动保存画面到桌面的'ikun ESP32 Camera'文件夹");
} else {
// 禁用智能预警功能
m_safetyEnabled = false;
qDebug() << "智能预警功能已禁用";
}
}
五、ESP32_CAM 智能视频监控演示

六、代码开源
代码地址: 基于ESP32-CAM与QtCreator的智能视频监控项目代码资源-CSDN下载
如果积分不够的朋友,点波关注,评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!