ESP32-CAM 实时视频监控实战
ESP32-CAM 是一款小巧的摄像头模组,支持独立工作,尺寸仅 2740.54.5mm。它集成了低功耗双核 32 位 CPU、PSRAM 以及 OV2640/OV7670 摄像头,非常适合家庭智能设备、工业无线控制及物联网监控场景。

硬件特性与接口
该模块主频高达 240MHz,内置 520KB SRAM 并支持外置 8MB PSRAM。通信方面支持 UART/SPI/I2C/PWM/ADC/DAC 等丰富接口,内嵌 LwIP 和 FreeRTOS,支持 STA/AP/STA+AP 多种工作模式及 Smart Config 配网。

开发环境搭建
安装 Arduino IDE
前往官网下载对应操作系统的 IDE 并安装。
配置 ESP32 开发板支持
Arduino IDE 默认不包含 ESP32 核心库,需手动添加。
- 打开 工具 > 开发板 > 开发板管理器。
- 搜索
ESP32并安装ESP32 Wrover Module或相关包。 - 在 文件 > 首选项 中,将以下 URL 填入'附加开发板管理器网址':
https://dl.espressif.com/dl/package_esp32_index.json

局域网视频流查看
利用 ESP32 自带的 WebServer 功能,可以快速实现局域网内的视频预览。
调用示例代码
在 Arduino IDE 中选择 文件 > 示例 > ESP32 > Camera > CameraWebServer。这个示例已经封装好了相机初始化和 HTTP 服务逻辑。

关键参数修改
上传前需根据实际网络环境和硬件型号调整代码:
- WiFi 信息:修改
ssid和password为你的路由器账号密码。 - 摄像头型号:确保宏定义正确。对于常见的 AI-Thinker 版本,取消注释
#define CAMERA_MODEL_AI_THINKER。
// 选择摄像头模型
#define CAMERA_MODEL_AI_THINKER // Has PSRAM
// #define CAMERA_MODEL_WROVER_KIT // Has PSRAM
// ... 其他型号注释掉

编译与烧录
点击左上角的编译验证按钮,成功后点击上传。上传完成后,按下开发板上的 RST 按键重启。
打开串口监视器(波特率 115200),查看输出的 IP 地址。在浏览器输入该 IP 即可看到实时画面。


完整代码参考
以下是经过格式化的标准示例代码,重点在于初始化配置和缓冲区管理:
#include "esp_camera.h"
#include <WiFi.h>
// 注意:UXGA 分辨率需要 PSRAM
// 确保选择了带有 PSRAM 的模块,如 ESP32 Wrover
// 选择摄像头型号
// #define CAMERA_MODEL_WROVER_KIT
// #define CAMERA_MODEL_ESP_EYE
// #define CAMERA_MODEL_M5STACK_PSRAM
// #define CAMERA_MODEL_M5STACK_V2_PSRAM
// #define CAMERA_MODEL_M5STACK_WIDE
// #define CAMERA_MODEL_M5STACK_ESP32CAM
#define CAMERA_MODEL_AI_THINKER
// #define CAMERA_MODEL_TTGO_T_JOURNAL
#include "camera_pins.h"
const char* ssid = "TP-LINK_1760";
const char* password = "987654321";
void startCameraServer();
void setup() {
Serial.begin(115200);
Serial.setDebugOutput(true);
Serial.println();
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.pixel_format = PIXFORMAT_JPEG;
// 如果检测到 PSRAM,使用高分辨率和大帧缓冲
if (psramFound()) {
config.frame_size = FRAMESIZE_UXGA;
config.jpeg_quality = 10;
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_SVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
}
#if defined(CAMERA_MODEL_ESP_EYE)
pinMode(13, INPUT_PULLUP);
pinMode(14, INPUT_PULLUP);
#endif
// 初始化相机
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();
// 部分传感器默认翻转且色彩饱和度高,需校正
if (s->id.PID == OV3660_PID) {
s->set_vflip(s, 1);
s->set_brightness(s, 1);
s->set_saturation(s, -2);
}
// 降低初始帧率以提高启动速度
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
WiFi.begin(ssid, password);
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");
}
void loop() {
delay(10000);
}
外网视频流传输方案
若需在外网访问,简单的 WebServer 可能受限于 NAT 穿透。更灵活的方式是通过 TCP 协议将视频数据推送到公网服务器,由服务端接收并展示。
架构说明
- ESP32 端:作为 TCP Client,负责采集图像分包发送。
- 服务器端:运行 Python 程序,作为 TCP Server 接收数据流并解码显示。
ESP32 端代码逻辑
这段代码的核心在于 TCP 分包发送。JPEG 图片较大,直接发送容易丢包,因此我们设定了最大缓存大小(MTU),并在数据包前后加上标记(Frame Begin / Frame Over)以便服务端识别边界。
#include <Arduino.h>
#include <WiFi.h>
#include "esp_camera.h"
#include <vector>
const char* ssid = "TP-LINK_1760";
const char* password = "987654321";
// 目标服务器 IP 和端口
const IPAddress serverIP(192, 168, 1, 104);
uint16_t serverPort = 18080;
#define MAX_CACHE 1430
WiFiClient client;
// AI-Thinker 摄像头引脚定义
#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
static camera_config_t camera_config = {
.pin_pwdn = PWDN_GPIO_NUM,
.pin_reset = RESET_GPIO_NUM,
.pin_xclk = XCLK_GPIO_NUM,
.pin_sscb_sda = SIOD_GPIO_NUM,
.pin_sscb_scl = SIOC_GPIO_NUM,
.pin_d7 = Y9_GPIO_NUM,
.pin_d6 = Y8_GPIO_NUM,
.pin_d5 = Y7_GPIO_NUM,
.pin_d4 = Y6_GPIO_NUM,
.pin_d3 = Y5_GPIO_NUM,
.pin_d2 = Y4_GPIO_NUM,
.pin_d1 = Y3_GPIO_NUM,
.pin_d0 = Y2_GPIO_NUM,
.pin_vsync = VSYNC_GPIO_NUM,
.pin_href = HREF_GPIO_NUM,
.pin_pclk = PCLK_GPIO_NUM,
.xclk_freq_hz = 20000000,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0,
.pixel_format = PIXFORMAT_JPEG,
.frame_size = FRAMESIZE_QVGA, // 根据带宽调整分辨率
.jpeg_quality = 24,
.fb_count = 1,
};
void wifi_init() {
WiFi.mode(WIFI_STA);
WiFi.setSleep(false); // 关闭休眠提高响应
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("WiFi Connected!");
Serial.print("IP Address:");
Serial.println(WiFi.localIP());
}
esp_err_t camera_init() {
esp_err_t err = esp_camera_init(&camera_config);
if (err != ESP_OK) {
Serial.println("Camera Init Failed");
return err;
}
sensor_t * s = esp_camera_sensor_get();
if (s->id.PID == OV2640_PID) {
// 可选:校正翻转和亮度
}
Serial.println("Camera Init OK!");
return ESP_OK;
}
void setup() {
Serial.begin(115200);
wifi_init();
camera_init();
}
void loop() {
Serial.println("Try To Connect TCP Server!");
if (client.connect(serverIP, serverPort)) {
Serial.println("Connect Tcp Server Success!");
while (1) {
camera_fb_t * fb = esp_camera_fb_get();
uint8_t* temp = fb->buf; // 保存指针,防止内存泄漏
if (!fb) {
Serial.println("Camera Capture Failed");
} else {
client.print("Frame Begin"); // 起始标志
int len = fb->len;
int times = len / MAX_CACHE;
int extra = len % MAX_CACHE;
for (int j = 0; j < times; j++) {
client.write(fb->buf, MAX_CACHE);
fb->buf += MAX_CACHE;
}
if (extra > 0) {
client.write(fb->buf, extra);
}
client.print("Frame Over"); // 结束标志
Serial.print("This Frame Length:");
Serial.print(fb->len);
Serial.println(". Succes To Send Image For TCP!");
// 归还缓冲区供驱动复用
fb->buf = temp;
esp_camera_fb_return(fb);
}
delay(20);
}
} else {
Serial.println("Connect To Tcp Server Failed! After 10 Seconds Try Again!");
client.stop();
delay(10000);
}
}
服务器端 Python 程序
服务端使用 Socket 监听,接收到数据后通过 OpenCV 进行解码显示。关键点在于解析 Frame Begin 和 Frame Over 标记,重组完整的 JPEG 字节流。
import socket
import threading
import time
import numpy as np
import cv2
begin_data = b'Frame Begin'
end_data = b'Frame Over'
def handle_sock(sock, addr):
temp_data = b''
t1 = int(round(time.time() * 1000))
while True:
data = sock.recv(1430)
if not data:
break
# 检测起始标志
if data[0:len(begin_data)] == begin_data:
# 去除起始标志
data = data[len(begin_data):]
# 循环接收直到遇到结束标志
while data[-len(end_data):] != end_data:
temp_data = temp_data + data
data = sock.recv(1430)
# 处理最后一包,去除结束标志
temp_data = temp_data + data[0:(len(data)-len(end_data))]
# 解码显示
receive_data = np.frombuffer(temp_data, dtype='uint8')
r_img = cv2.imdecode(receive_data, cv2.IMREAD_COLOR)
t2 = int(round(time.time() * 1000))
fps = 1000 // (t2 - t1)
cv2.putText(r_img, "FPS: " + str(fps), (50, 50), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 0, 0), 2)
cv2.imshow('server_frame', r_img)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
t1 = t2
print("接收到的数据包大小:" + str(len(temp_data)))
temp_data = b''
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('192.168.1.104', 18080))
server.listen(5)
print('Server started on 192.168.1.104:18080')
while True:
sock, addr = server.accept()
print('Connect--{}'.format(addr))
client_thread = threading.Thread(target=handle_sock, args=(sock, addr))
client_thread.start()
总结
通过上述步骤,我们实现了从本地局域网到远程外网的视频流传输。局域网方案简单快捷,适合调试;外网方案则提供了更大的灵活性,但需要注意网络带宽和安全性。在实际应用中,建议根据具体需求调整分辨率和帧率以平衡画质与流畅度。


