长生栈 长生栈
首页
  • 编程语言

    • C语言
    • C++
    • Java
    • Python
  • 数据结构和算法

    • 全排列算法实现
    • 动态规划算法
  • CMake
  • gitlab 安装和配置
  • docker快速搭建wordpress
  • electron+react开发和部署
  • Electron-创建你的应用程序
  • ImgUI编译环境
  • 搭建图集网站
  • 使用PlantUml画时序图
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Living Team

编程技术分享
首页
  • 编程语言

    • C语言
    • C++
    • Java
    • Python
  • 数据结构和算法

    • 全排列算法实现
    • 动态规划算法
  • CMake
  • gitlab 安装和配置
  • docker快速搭建wordpress
  • electron+react开发和部署
  • Electron-创建你的应用程序
  • ImgUI编译环境
  • 搭建图集网站
  • 使用PlantUml画时序图
  • 友情链接
关于
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 计算机视觉

  • ESP32开发

    • ESP32-开发环境配置
    • ESP32-点亮LED灯
    • ESP32-点亮OLED屏幕
    • ESP32-实时操作系统freertos
    • ESP32-PWM驱动SG90舵机
    • ESP32-网络摄像头方案
      • 硬件选择
      • 系统架构
      • 环境搭建
        • MQTT Broker搭建
        • NodeJs搭建
      • 关键代码
        • esp32-s3的代码
        • 服务端代码
        • 前端代码
  • Linux系统移植

  • 快速开始

  • 编程小知识

  • 技术
  • ESP32开发
DC Wang
2025-06-14
目录

ESP32-网络摄像头方案

# ESP32网络摄像头方案

基于ESP32、MJPEG 640×480@10fps和ECS(暂由本地模拟)的 完整可控视频传输方案,包含启动/停止控制、状态反馈和优化策略。该方案实现了从设备控制到视频传输的完整闭环,预期在3Mbps带宽下可稳定运行。

# 硬件选择

ESP32-CAM (ESP32 + OV2640)

image-20250614102725011

# 系统架构

image-20250614094743985

# 环境搭建

以Windows为例:

# MQTT Broker搭建

  • 下载安装Mosquitto ,官网 https://mosquitto.org/download/

  • 选择目录进行安装,安装目录之下会有一个mosquitto.conf,添加下面内容:

    # 监听所有网络接口(允许局域网访问)
    listener 10086 0.0.0.0
    
    # 允许匿名连接(测试用,生产环境需关闭)
    allow_anonymous true
    
    1
    2
    3
    4
    5

    mosquitto.conf 参考 mosquitto.conf man page | Eclipse Mosquitto (opens new window)

  • 安装目录之下,使用终端启动:

    .\mosquitto -c .\mosquitto.conf
    
    1
  • 新开终端,启动订阅(可选)

    # -t TOPIC
    .\mosquitto_sub -t 'cam/device1/ctrl' -v -p 10086
    
    1
    2

# NodeJs搭建

官网: Node.js — 在任何地方运行 JavaScript (opens new window)

  • 下载nodejs

  • 创建目录,安装依赖

    npm install express multer mqtt ws
    
    1

# 关键代码

# esp32-s3的代码

#include "esp_camera.h"
#include <WiFi.h>
#include <HTTPClient.h>
#include <PubSubClient.h>

// 在全局或静态变量中标记初始化状态
static bool gpio_isr_installed = false;

// 摄像头配置
#define PWDN_GPIO_NUM  32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM  0
#define SIOD_GPIO_NUM  26
#define SIOC_GPIO_NUM  27

#define Y9_GPIO_NUM    35
#define Y8_GPIO_NUM    34
#define Y7_GPIO_NUM    39
#define Y6_GPIO_NUM    36
#define Y5_GPIO_NUM    21
#define Y4_GPIO_NUM    19
#define Y3_GPIO_NUM    18
#define Y2_GPIO_NUM    5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM  23
#define PCLK_GPIO_NUM  22

// 4 for flash led or 33 for normal led
#define LED_GPIO_NUM   4

// Wi-Fi配置
const char* ssid = "wifi名";
const char* password = "wifi密码";
const char* serverURL = "http://127.0.0.1:10188/video";

// MQTT配置
const char* mqttServer = "127.0.0.1";
const int mqttPort = 10086;
const char* mqttTopic = "cam/device1/ctrl";

WiFiClient espClient;
PubSubClient mqttClient(espClient);
bool isStreaming = false;

// MQTT回调函数
void callback(char* topic, byte* payload, unsigned int length) {
  String message = String((char*)payload, length);
  if(message == "ON") {
    startStreaming();
  } else if(message == "OFF") {
    stopStreaming();
  }
}

void startStreaming() {
  // 初始化 GPIO ISR 服务(仅一次)(没有作用)
  if (!gpio_isr_installed) {
      esp_err_t ret = gpio_install_isr_service(0);
      if (ret != ESP_OK) {
        Serial.println("初始化GPIO ISR 服务失败");
        return;
      }
      gpio_isr_installed = true;
  }
  if(!isStreaming) {
    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_sccb_sda = SIOD_GPIO_NUM;
    config.pin_sccb_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_VGA; // 640x480
    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 = 2;
    esp_camera_init(&config); // 初始化摄像头
    isStreaming = true;
    Serial.println("视频流已启动");
  }
}

void stopStreaming() {
  if(isStreaming) {
    esp_camera_deinit(); // 释放摄像头资源
    isStreaming = false;
    Serial.println("视频流已停止");
  }
}

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();
  // 初始化Wi-Fi(同前)
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) delay(500);
  Serial.println("Wi-Fi连接成功");
  // 连接MQTT
  mqttClient.setServer(mqttServer, mqttPort);
  mqttClient.setCallback(callback);
  while(!mqttClient.connect("ESP32CAM")) delay(500);
  mqttClient.subscribe(mqttTopic);
  mqttClient.publish(mqttTopic, "666");
  Serial.println("mqtt连接成功");
}

void loop() {
  mqttClient.loop();
  
  if(isStreaming) {
    camera_fb_t *fb = esp_camera_fb_get();
    if(fb) {
      // 发送视频帧(同前)
      HTTPClient http;
      http.begin(serverURL);

      String boundary = "1234567890";
      String body = "--" + boundary + "\r\n";
      body += "Content-Disposition: form-data; name=\"frame\"; filename=\"image.jpg\"\r\n";
      body += "Content-Type: image/jpeg\r\n\r\n";
      body += String((const char*)fb->buf, fb->len); // 注意:仅适用于文本安全的数据
      body += "\r\n--" + boundary + "--\r\n";

      http.addHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
      http.POST(body); // 直接发送完整请求体

      http.end();
      esp_camera_fb_return(fb);
    }
    delay(100); // 10fps控制
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149

# 服务端代码

const express = require('express');
const mqtt = require('mqtt');
const app = express();
const cors = require('cors');  // 新增引入cors
app.use(express.json());

// 跨域设置
app.use(cors());

// 设备状态存储
let deviceStatus = { 
  device1: { 
    running: false,
    lastFrame: null 
  }
};

// MQTT连接
const mqttClient = mqtt.connect('mqtt://127.0.0.1:10086');

// 控制指令接口
app.post('/control', (req, res) => {
  const { deviceId, command } = req.body;
  
  if(command === 'start') {
    deviceStatus[deviceId].running = true;
    mqttClient.publish(`cam/${deviceId}/ctrl`, 'ON');
  } else if(command === 'stop') {
    deviceStatus[deviceId].running = false; 
    mqttClient.publish(`cam/${deviceId}/ctrl`, 'OFF');
  }
  
  res.json({ status: deviceStatus[deviceId] });
});

// 视频接收接口
const multer = require('multer');
const upload = multer();
app.post('/video', upload.single('frame'), (req, res) => {
  if(deviceStatus.device1.running) {
    deviceStatus.device1.lastFrame = req.file.buffer;
    broadcastToWeb(); // 转发到前端
  }
  res.sendStatus(200);
});

// WebSocket广播
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 10081 });
function broadcastToWeb() {
  wss.clients.forEach(client => {
    if(client.readyState === WebSocket.OPEN) {
      client.send(deviceStatus.device1.lastFrame);
    }
  });
}

app.listen(10188);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 前端代码

<!DOCTYPE html>
<html>
<head>
  <style>
    /* 整体容器样式 */
    .container {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 20px; /* 元素间距 */
      padding: 20px;
      background-color: #f0f0f0;
      min-height: 100vh;
    }

    /* 画面区域样式 */
    #videoFeed {
      max-width: 100%;
      border: 3px solid #333;
      border-radius: 8px;
      box-shadow: 0 4px 8px rgba(0,0,0,0.1);
    }

    /* 按钮容器 */
    .button-group {
      display: flex;
      gap: 15px; /* 按钮间距 */
    }

    /* 按钮样式 */
    button {
      padding: 12px 24px;
      font-size: 16px;
      border: none;
      border-radius: 6px;
      cursor: pointer;
      transition: all 0.3s ease;
    }

    /* 启动按钮样式 */
    #startBtn {
      background-color: #4CAF50; /* 绿色 */
      color: white;
    }

    /* 停止按钮样式 */
    #stopBtn {
      background-color: #f44336; /* 红色 */
      color: white;
    }

    /* 悬停效果 */
    button:hover {
      opacity: 0.9;
      transform: translateY(-2px);
    }
  </style>
</head>
<body>
  <div class="container">
    <img id="videoFeed" width="640" height="480">
    
    <div class="button-group">
      <button id="startBtn" onclick="sendCommand('start')">启动设备</button>
      <button id="stopBtn" onclick="sendCommand('stop')">停止设备</button>
    </div>
  </div>

  <script>
    const ws = new WebSocket('ws://127.0.0.1:10081');
    ws.binaryType = 'arraybuffer';
    const img = document.getElementById('videoFeed');

    // 接收视频流
    ws.onmessage = (event) => {
      const blob = new Blob([event.data], { type: 'image/jpeg' });
      img.src = URL.createObjectURL(blob);
    };

    // 发送控制指令(添加了错误处理)
    function sendCommand(cmd) {
      fetch('http://127.0.0.1/control', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ deviceId: 'device1', command: cmd })
      })
      .then(response => {
        if (!response.ok) throw new Error('控制失败');
        console.log(`${cmd} 指令发送成功`);
      })
      .catch(error => console.error('控制错误:', error));
    }

    // 关闭连接时清理资源
    window.addEventListener('beforeunload', () => {
      ws.close();
    });
  </script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
编辑 (opens new window)
#ESP32#Camera
ESP32-PWM驱动SG90舵机
Linux系统移植(一)--- 交叉编译工具链的配置

← ESP32-PWM驱动SG90舵机 Linux系统移植(一)--- 交叉编译工具链的配置→

最近更新
01
ESP32-PWM驱动SG90舵机
06-14
02
ESP32-实时操作系统freertos
06-14
03
ESP32-点亮OLED屏幕
06-14
更多文章>
Theme by Vdoing | Copyright © 2019-2025 DC Wang All right reserved | 辽公网安备 21021102001125号 | 吉ICP备20001966号-2
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式