This project is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License (CC BY-NC-SA 3.0)
- ❌ Commercial Use Prohibited: This project cannot be used for any commercial purposes
- ✅ **Non-Commercial Use Allowed **: Personal learning, research, education, and other non-commercial purposes
- ✅ Modification and Distribution Allowed: But must use the same license
- ✅ Attribution Required: Must retain the original author's attribution
For details, please visit: https://creativecommons.org/licenses/by-nc-sa/3.0/
This project is modified from the following open-source projects:
- Freenove ESP32-S3 Camera Example (CC BY-NC-SA 3.0)
- Espressif Camera Library Example (Apache License 2.0) - from Arduino ESP32 Board Package
This project is based on the ESP32-S3-CAM (Guoyun version) development board, implementing a basic network video monitoring system. The system supports real-time video streaming and photo capture, suitable for home monitoring, security monitoring, and other scenarios.
- Implement real-time video streaming display on the Web (Manual Control)
- Implement photo capture control on the Web
- Implement opening video streaming in a new tab
- Implement basic HTTP server
- Development Framework: Arduino Framework
- Camera Driver: ESP32 Camera Library (Official)
- Web Server: ESP-IDF httpd component
- Video Encoding: MJPEG
┌─────────────────────────────────────────────────────────────┐
│ ESP32-S3-CAM │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ │
│ │ Camera Module │ │
│ │ OV5640 │ │
│ └──────────────┘ │
│ │ │
│ ┌──────┴─────────────────────────────────────────────┐ │
│ │ Core Control Module │ │
│ │ - Video capture and encoding │ │
│ │ - Video streaming │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────┴─────────────────────────────────────────────┐ │
│ │ Web Server Module │ │
│ │ - Video streaming service │ │
│ │ - Photo capture API │ │
│ │ - Web interface │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────┴─────────────────────────────────────────────┐ │
│ │ WiFi Communication Module │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ WiFi
│
┌──────────────────┴──────────────────┐
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ Web Browser │ │ Mobile Device │
└───────────┘ └───────────┘
- Responsible for OV5640 camera initialization and configuration
- Responsible for video frame capture
- Responsible for JPEG encoding
- Supports multiple resolutions
- Provides video streaming interface
- Provides photo capture API
- Provides web interface
- Responsible for WiFi connection
- Provides network communication support
Display video stream captured by ESP32-S3-CAM in real-time on the web, supporting basic video display functions.
- Use MJPEG format for video streaming
- Use multipart/x-mixed-replace protocol
- Use ESP-IDF httpd component to implement HTTP server
- Support video stream retry mechanism
GET /stream
Response Format:
Content-Type: multipart/x-mixed-replace;boundary=123456789000000000000987654321
--123456789000000000000987654321
Content-Type: image/jpeg
Content-Length: 12345
X-Timestamp: 1234567890.123456
[JPEG data]
--123456789000000000000987654321
...
- Resolution: According to camera configuration
- JPEG Quality: According to camera configuration
// Video stream handler function
static esp_err_t stream_handler(httpd_req_t *req) {
// Set response type
httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
// Loop to send video frames
while (true) {
// Get camera frame
camera_fb_t *fb = esp_camera_fb_get();
// Convert to JPEG format
// ...
// Send frame data
httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
// Release frame buffer
esp_camera_fb_return(fb);
}
}Control camera to capture photos on the web, obtaining current camera image.
- Use ESP32 Camera Library to capture single frame image
- Use JPEG format for encoding
- Return image data through HTTP response
GET /capture
Response Format:
- Content-Type: image/jpeg
- Response body: JPEG image data
- Image Format: JPEG
- Resolution: According to camera configuration
- JPEG Quality: According to camera configuration
// Photo capture handler function
static esp_err_t capture_handler(httpd_req_t *req) {
// Get camera frame
camera_fb_t *fb = esp_camera_fb_get();
// Set response type
httpd_resp_set_type(req, "image/jpeg");
// Send image data
httpd_resp_send(req, (const char *)fb->buf, fb->len);
// Release frame buffer
esp_camera_fb_return(fb);
return ESP_OK;
}Open video stream in a new tab, providing clearer video viewing experience.
- Use JavaScript's window.open() function
- Build complete video stream URL
- Load video stream in new tab
function openStreamInNewTab() {
const streamUrl = 'http://' + window.location.hostname + ':81/stream';
window.open(streamUrl, '_blank');
}#define CAMERA_MODEL_ESP32S3_EYE
// Camera pin configuration
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 15
#define SIOD_GPIO_NUM 4
#define SIOC_GPIO_NUM 5
#define Y9_GPIO_NUM 16
#define Y8_GPIO_NUM 17
#define Y7_GPIO_NUM 18
#define Y6_GPIO_NUM 12
#define Y5_GPIO_NUM 10
#define Y4_GPIO_NUM 8
#define Y3_GPIO_NUM 9
#define Y2_GPIO_NUM 11
#define VSYNC_GPIO_NUM 6
#define HREF_GPIO_NUM 7
#define PCLK_GPIO_NUM 13camera_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 = 22000000; // 22MHz clock
config.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
// If PSRAM is available, use higher resolution and quality
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// Limit frame size when no PSRAM
config.frame_size = FRAMESIZE_XGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}- ESP32-S3-CAM's SD card library supports exFAT file system by default
- No additional configuration needed, use SD library directly
- Supports large files (>4GB)
┌─────────────────────────────────────────────────────────────┐
│ ESP32-S3 Monitoring Console │
├─────────────────────────────────────────────────────────────┤
│ Language: [English ▼] Theme: [Light ▼] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Video Display Area │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [Capture Photo] [Getstream] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
- Supported Languages: English, Chinese
- Default Language: English
- Location: Top-right corner of the page
- Persistence: Use localStorage to save user selection
- Auto Restore: Automatically restore last selected language on page load
Technical Implementation:
- Use JavaScript's translations object to store translations for all UI elements
- Identify elements that need translation through data-lang attribute
- Use changeLanguage() function to switch language
- Use localStorage.setItem('preferredLanguage', currentLanguage) to save preference
- Use localStorage.getItem('preferredLanguage') to restore preference
Translation Object Structure:
const translations = {
en: {
title: "ESP32-S3 Monitoring Console",
language: "Language",
theme: "Theme",
lightTheme: "Light",
darkTheme: "Dark",
liveVideo: "Live Video",
loadingStream: "Loading video stream...",
capturePhoto: "Capture Photo",
openStreamNewTab: "Open Stream in New Tab",
sdCardInfo: "SD Card Storage Info",
totalSpace: "Total Space",
usedSpace: "Used Space",
freeSpace: "Free Space",
systemUptime: "System Uptime",
uptimeUnit: "Uptime",
cameraSettings: "Camera Settings",
resolution: "Resolution",
imageQuality: "Image Quality",
lowQuality: "Low Quality (63)",
mediumQuality: "Medium Quality (31)",
highQuality: "High Quality (10)",
veryHighQuality: "Very High Quality (5)",
brightness: "Brightness",
contrast: "Contrast",
saturation: "Saturation"
},
zh: {
title: "ESP32-S3监控控制台",
language: "语言",
theme: "主题",
lightTheme: "浅色",
darkTheme: "深色",
liveVideo: "实时视频",
loadingStream: "正在加载视频流...",
capturePhoto: "拍照",
openStreamNewTab: "在新标签页中打开视频流",
sdCardInfo: "SD卡存储信息",
totalSpace: "总空间",
usedSpace: "已用空间",
freeSpace: "剩余空间",
systemUptime: "系统运行时长",
uptimeUnit: "运行时间",
cameraSettings: "摄像头设置",
resolution: "分辨率",
imageQuality: "图像质量",
lowQuality: "低质量 (63)",
mediumQuality: "中等质量 (31)",
highQuality: "高质量 (10)",
veryHighQuality: "极高质量 (5)",
brightness: "亮度",
contrast: "对比度",
saturation: "饱和度"
}
};- Supported Themes: Light, Dark
- Default Theme: Light
- Location: Top-right corner of the page (next to language selector)
- Persistence: Use localStorage to save user selection
- Auto Restore: Automatically restore last selected theme on page load
- Transition Animation: 0.3 seconds smooth transition
Technical Implementation:
- Use CSS variables to define theme colors
- Switch theme through data-theme attribute
- Use changeTheme() function to switch theme
- Use localStorage.setItem('preferredTheme', currentTheme) to save preference
- Use localStorage.getItem('preferredTheme') to restore preference
CSS Variable Definition:
:root {
/* Light theme variables */
--bg-color: #f5f5f5;
--container-bg: #ffffff;
--text-color: #333333;
--section-bg: #f9f9f9;
--section-title-color: #555555;
--border-color: #dddddd;
--btn-primary-bg: #007bff;
--btn-primary-hover: #0056b3;
--btn-success-bg: #28a745;
--btn-success-hover: #218838;
--btn-danger-bg: #dc3545;
--btn-danger-hover: #c82333;
--btn-warning-bg: #ffc107;
--btn-warning-hover: #e0a800;
--btn-warning-text: #212529;
--info-label-color: #666666;
--info-value-color: #333333;
--sd-info-value-color: #333333;
--sd-info-unit-color: #999999;
--setting-label-color: #666666;
--setting-border-color: #dddddd;
--setting-border-hover: #007bff;
--uptime-value-color: #333333;
--uptime-unit-color: #666666;
--language-label-color: #666666;
--loading-text-color: #666666;
--shadow-color: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
/* Dark theme variables */
--bg-color: #1a1a1a;
--container-bg: #2d2d2d;
--text-color: #e0e0e0;
--section-bg: #383838;
--section-title-color: #b0b0b0;
--border-color: #555555;
--btn-primary-bg: #007bff;
--btn-primary-hover: #0056b3;
--btn-success-bg: #28a745;
--btn-success-hover: #218838;
--btn-danger-bg: #dc3545;
--btn-danger-hover: #c82333;
--btn-warning-bg: #ffc107;
--btn-warning-hover: #e0a800;
--btn-warning-text: #212529;
--info-label-color: #b0b0b0;
--info-value-color: #e0e0e0;
--sd-info-value-color: #e0e0e0;
--sd-info-unit-color: #888888;
--setting-label-color: #b0b0b0;
--setting-border-color: #555555;
--setting-border-hover: #007bff;
--uptime-value-color: #e0e0e0;
--uptime-unit-color: #b0b0b0;
--language-label-color: #b0b0b0;
--loading-text-color: #b0b0b0;
--shadow-color: rgba(0, 0, 0, 0.3);
}
/* Global transition effect */
* {
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
}Theme Switching Function:
function changeTheme() {
const themeSelect = document.getElementById('theme-select');
currentTheme = themeSelect.value;
// Set data-theme attribute
if (currentTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
// Save theme preference to localStorage
localStorage.setItem('preferredTheme', currentTheme);
}- Capture Photo: Take a photo and open it in a new window
- Open Stream in New Tab: Open video stream in a new tab
- Video stream status
- Photo capture status
- preferredLanguage: Save user selected language ('en' or 'zh')
- preferredTheme: Save user selected theme ('light' or 'dark')
// Restore language and theme preferences on page load
window.onload = function() {
// Restore language preference
const savedLanguage = localStorage.getItem('preferredLanguage');
if (savedLanguage) {
document.getElementById('language-select').value = savedLanguage;
currentLanguage = savedLanguage;
changeLanguage();
}
// Restore theme preference
const savedTheme = localStorage.getItem('preferredTheme');
if (savedTheme) {
document.getElementById('theme-select').value = savedTheme;
currentTheme = savedTheme;
changeTheme();
}
};GET /stream
Response:
Content-Type: multipart/x-mixed-replace;boundary=123456789000000000000987654321
--123456789000000000000987654321
Content-Type: image/jpeg
Content-Length: 12345
X-Timestamp: 1234567890.123456
[JPEG data]
--123456789000000000000987654321
...
GET /capture
Response:
- Content-Type: image/jpeg
- Response body: JPEG image data
GET /status
Response:
{
"xclk": 22,
"pixformat": 3,
"framesize": 7,
"quality": 10,
"brightness": 1,
"contrast": 0,
"saturation": 0,
"sharpness": 0,
"special_effect": 0,
"wb_mode": 0,
"awb": 1,
"awb_gain": 1,
"aec": 1,
"aec2": 1,
"ae_level": 0,
"aec_value": 300,
"agc": 1,
"agc_gain": 0,
"gainceiling": 0,
"bpc": 0,
"wpc": 1,
"raw_gma": 1,
"lenc": 1,
"hmirror": 0,
"dcw": 1,
"colorbar": 0,
"led_intensity": -1
}ESP32-S3_Camera_Monitor/
├── ESP32_S3_Camera_Monitor.ino # Main program file
├── camera_pins.h # Camera pin configuration
├── camera_index.h # Web interface HTML
├── app_httpd.cpp # HTTP server implementation
├── partitions.csv # Partition table file
├── readme/
│ ├── PROJECT_REQUIREMENTS.md
│ ├── TECHNOLOGY_STACK.md
│ ├── DEVELOPMENT.md
│ ├── IMPLEMENTATION.md
│ ├── FEATURE_UPDATES.md
│ └── examples/
│ ├── Sketch_07.1_CameraWebServer.ino
│ ├── app_httpd.cpp
│ ├── camera_index.h
│ ├── camera_pins.h
│ └── partitions.csv
// WiFi configuration
const char* ssid = "your-ssid"; // WiFi SSID
const char* password = "your-password"; // WiFi password// Camera configuration
#define CAMERA_MODEL_ESP32S3_EYE // Camera model
#define CAMERA_XCLK_FREQ 20000000 // Camera clock frequency: 22MHz
#define CAMERA_FRAME_SIZE FRAMESIZE_UXGA // Resolution: UXGA
#define CAMERA_JPEG_QUALITY 12 // JPEG quality: 12 (1-31, lower is better)
#define CAMERA_FB_COUNT 1 // Frame buffer count: 1
// If PSRAM is available, use higher resolution and quality
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// Limit frame size when no PSRAM
config.frame_size = FRAMESIZE_XGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}// Camera initialization failure
if (esp_camera_init(&config) != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
// Frame capture failure
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
ESP_LOGE(TAG, "Camera capture failed");
httpd_resp_send_500(req);
return ESP_FAIL;
}// WiFi connection
WiFi.begin(ssid, password);
WiFi.setSleep(false);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");- Use PSRAM to store camera frame buffers
- Reasonably set frame buffer count (2)
- Release unused memory in time
- Avoid memory leaks
- Optimize JPEG encoding quality to balance image quality and performance
- Reasonably allocate CPU resources
- Use WiFi keep-alive to avoid frequent reconnections
- Support video stream retry mechanism
- Test if video stream displays normally
- Test video stream retry mechanism
- Test opening video stream in new tab function
- Test if photo capture works normally
- Test photo quality
- Test photo capture response speed
- Test video stream long-term stability
- Test WiFi connection stability
- Test camera continuous operation stability
- Download Arduino IDE 2.x
- Install ESP32 board support
// Install the following libraries in Arduino IDE:
ArduinoJson- Select board: ESP32S3 Dev Module
- Configure partition scheme: Use provided partitions.csv
- Configure PSRAM: OPI PSRAM
- Configure Flash mode: QIO
- Configure Flash size: 8MB (according to actual hardware)
- Connect ESP32-S3-CAM to computer
- Select correct serial port
- Click upload button
- Wait for compilation and upload to complete
- Power on ESP32-S3-CAM
- Wait for WiFi connection
- Check serial output to get IP address
- Access IP address in browser
- Test video stream display function
- Test photo capture function
- Test opening video stream in new tab function
- Check system logs
- Monitor device operation status
- Monitor SD card storage space
- System automatically cleans up invalid video files (0KB) on startup
- Regularly check video recording quality and file integrity
- SD card space automatic management:
- Dynamic threshold detection: triggers cleanup when free space < 5GB
- Delete oldest files by time: prioritizes deleting the oldest files
- Cleanup target: releases approximately 2GB space per cleanup
- Cleanup priority: configurable (videos only, videos then photos, photos only)
- Space check interval: checks every 5 seconds by default
- Check WiFi connection
- Check camera initialization
- Check browser compatibility
- Check network bandwidth
- Check if camera is working normally
- Check network connection
- Download new firmware version
- Upload new firmware to ESP32-S3-CAM via OTA
- Device will automatically reboot after successful update
- Test all functions
- Access the device web interface
- Navigate to the OTA upgrade page
- Select the firmware.bin file
- Click upload
- Wait for the upload and verification to complete
- Device will automatically reboot
- Device must be connected to WiFi
- User must be authenticated
- Sufficient free space in flash memory
- Stable network connection during upload
- Use strong WiFi password
- Change WiFi password regularly
- Consider using WPA3 encryption
- Avoid using in public networks
- Avoid exposing private information in video
- Ensure device is installed in a secure location
- Avoid exposing device to harsh environments
- Check device status regularly
- Ensure stable power supply
- Add motion detection function
- Add OTA Update
- Support higher resolution
- Support higher frame rate
- Optimize video streaming performance
- Create an assembly tutorial and upload it to Bilibili
本项目采用 Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License (CC BY-NC-SA 3.0)
- ❌ 禁止商业用途:本项目不能用于任何商业目的
- ✅ 允许非商业用途:个人学习、研究、教育等非商业用途
- ✅ 允许修改和分发:但必须使用相同的许可证
- ✅ 必须署名:必须保留原作者的署名
详情请查看:https://creativecommons.org/licenses/by-nc-sa/3.0/
本项目基于以下开源项目修改而来:
- Freenove ESP32-S3 Camera Example (CC BY-NC-SA 3.0)
- Espressif Camera Library Example (Apache License 2.0) - 来自 Arduino ESP32 Board Package
本项目基于ESP32-S3-CAM(果云版)开发板,实现一个基础的网络视频监控系统。系统支持实时视频流传输和拍照功能,适用于家庭监控、安防监控等场景。
- 实现Web端实时视频流显示
- 实现Web端控制拍照功能
- 实现在新标签页中打开视频流功能
- 实现基础HTTP服务器
- 开发框架:Arduino框架
- 摄像头驱动:ESP32 Camera库(官方)
- Web服务器:ESP-IDF的httpd组件
- 视频编码:MJPEG
┌─────────────────────────────────────────────────────────────┐
│ ESP32-S3-CAM │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ │
│ │ 摄像头模块 │ │
│ │ OV5640 │ │
│ └──────────────┘ │
│ │ │
│ ┌──────┴─────────────────────────────────────────────┐ │
│ │ 核心控制模块 │ │
│ │ - 视频采集与编码 │ │
│ │ - 视频流传输 │ │
│ └─────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────┴─────────────────────────────────────────────┐ │
│ │ Web服务器模块 │ │
│ │ - 视频流服务 │ │
│ │ - 拍照控制API │ │
│ │ - Web界面 │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────┴─────────────────────────────────────────────┐ │
│ │ WiFi通信模块 │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
│ WiFi
│
┌──────────────────┴──────────────────┐
│ │
┌─────┴─────┐ ┌─────┴─────┐
│ Web浏览器 │ │ 移动设备 │
└───────────┘ └───────────┘
- 负责OV5640摄像头的初始化和配置
- 负责视频帧的采集
- 负责JPEG编码
- 支持多种分辨率
- 提供视频流接口
- 提供拍照控制API
- 提供Web界面
- 负责WiFi连接
- 提供网络通信支持
在Web端实时显示ESP32-S3-CAM采集的视频流,支持基本的视频显示功能。
- 使用MJPEG格式进行视频流传输
- 使用multipart/x-mixed-replace协议
- 使用ESP-IDF的httpd组件实现HTTP服务器
- 支持视频流重试机制
GET /stream
响应格式:
Content-Type: multipart/x-mixed-replace;boundary=123456789000000000000987654321
--123456789000000000000987654321
Content-Type: image/jpeg
Content-Length: 12345
X-Timestamp: 1234567890.123456
[JPEG数据]
--123456789000000000000987654321
...
- 分辨率:根据摄像头配置
- JPEG质量:根据摄像头配置
// 视频流处理函数
static esp_err_t stream_handler(httpd_req_t *req) {
// 设置响应类型
httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
// 循环发送视频帧
while (true) {
// 获取摄像头帧
camera_fb_t *fb = esp_camera_fb_get();
// 转换为JPEG格式
// ...
// 发送帧数据
httpd_resp_send_chunk(req, (const char *)_jpg_buf, _jpg_buf_len);
// 释放帧缓冲区
esp_camera_fb_return(fb);
}
}在Web端控制摄像头拍照,获取当前摄像头画面。
- 使用ESP32 Camera库捕获单帧图像
- 使用JPEG格式编码
- 通过HTTP响应返回图像数据
GET /capture
响应格式:
- Content-Type: image/jpeg
- 响应体:JPEG图像数据
- 图片格式:JPEG
- 分辨率:根据摄像头配置
- JPEG质量:根据摄像头配置
// 拍照处理函数
static esp_err_t capture_handler(httpd_req_t *req) {
// 获取摄像头帧
camera_fb_t *fb = esp_camera_fb_get();
// 设置响应类型
httpd_resp_set_type(req, "image/jpeg");
// 发送图像数据
httpd_resp_send(req, (const char *)fb->buf, fb->len);
// 释放帧缓冲区
esp_camera_fb_return(fb);
return ESP_OK;
}在新标签页中打开视频流,提供更清晰的视频观看体验。
- 使用JavaScript的window.open()函数
- 构建完整的视频流URL
- 在新标签页中加载视频流
function openStreamInNewTab() {
const streamUrl = 'http://' + window.location.hostname + ':81/stream';
window.open(streamUrl, '_blank');
}#define CAMERA_MODEL_ESP32S3_EYE
// 摄像头引脚配置
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 15
#define SIOD_GPIO_NUM 4
#define SIOC_GPIO_NUM 5
#define Y9_GPIO_NUM 16
#define Y8_GPIO_NUM 17
#define Y7_GPIO_NUM 18
#define Y6_GPIO_NUM 12
#define Y5_GPIO_NUM 10
#define Y4_GPIO_NUM 8
#define Y3_GPIO_NUM 9
#define Y2_GPIO_NUM 11
#define VSYNC_GPIO_NUM 6
#define HREF_GPIO_NUM 7
#define PCLK_GPIO_NUM 13camera_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 = 22000000; // 22MHz时钟
config.frame_size = FRAMESIZE_UXGA;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.jpeg_quality = 12;
config.fb_count = 1;
// 如果有PSRAM,使用更高的分辨率和质量
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// 没有PSRAM时限制帧大小
config.frame_size = FRAMESIZE_XGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}- ESP32-S3-CAM的SD卡库默认支持exFAT文件系统
- 无需额外配置,直接使用SD库即可
- 支持大文件(>4GB)
┌─────────────────────────────────────────────────────────────┐
│ ESP32-S3监控控制台 │
├─────────────────────────────────────────────────────────────┤
│ 语言: [English ▼] 主题: [Light ▼] │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 视频显示区域 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ [拍照] [在新标签页中打开视频流] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
- 支持语言:English(英语)、中文
- 默认语言:English
- 位置:页面右上角
- 持久化:使用localStorage保存用户选择
- 自动恢复:页面加载时自动恢复上次选择的语言
技术实现:
- 使用JavaScript的translations对象存储所有UI元素的翻译
- 通过data-lang属性标识需要翻译的元素
- 使用changeLanguage()函数切换语言
- 使用localStorage.setItem('preferredLanguage', currentLanguage)保存偏好
- 使用localStorage.getItem('preferredLanguage')恢复偏好
翻译对象结构:
const translations = {
en: {
title: "ESP32-S3 Monitoring Console",
language: "Language",
theme: "Theme",
lightTheme: "Light",
darkTheme: "Dark",
liveVideo: "Live Video",
loadingStream: "Loading video stream...",
capturePhoto: "Capture Photo",
openStreamNewTab: "Open Stream in New Tab",
sdCardInfo: "SD Card Storage Info",
totalSpace: "Total Space",
usedSpace: "Used Space",
freeSpace: "Free Space",
systemUptime: "System Uptime",
uptimeUnit: "Uptime",
cameraSettings: "Camera Settings",
resolution: "Resolution",
imageQuality: "Image Quality",
lowQuality: "Low Quality (63)",
mediumQuality: "Medium Quality (31)",
highQuality: "High Quality (10)",
veryHighQuality: "Very High Quality (5)",
brightness: "Brightness",
contrast: "Contrast",
saturation: "Saturation"
},
zh: {
title: "ESP32-S3监控控制台",
language: "语言",
theme: "主题",
lightTheme: "浅色",
darkTheme: "深色",
liveVideo: "实时视频",
loadingStream: "正在加载视频流...",
capturePhoto: "拍照",
openStreamNewTab: "在新标签页中打开视频流",
sdCardInfo: "SD卡存储信息",
totalSpace: "总空间",
usedSpace: "已用空间",
freeSpace: "剩余空间",
systemUptime: "系统运行时长",
uptimeUnit: "运行时间",
cameraSettings: "摄像头设置",
resolution: "分辨率",
imageQuality: "图像质量",
lowQuality: "低质量 (63)",
mediumQuality: "中等质量 (31)",
highQuality: "高质量 (10)",
veryHighQuality: "极高质量 (5)",
brightness: "亮度",
contrast: "对比度",
saturation: "饱和度"
}
};- 支持主题:Light(浅色)、Dark(深色)
- 默认主题:Light
- 位置:页面右上角(语言选择器旁边)
- 持久化:使用localStorage保存用户选择
- 自动恢复:页面加载时自动恢复上次选择的主题
- 过渡动画:0.3秒平滑过渡
技术实现:
- 使用CSS变量定义主题颜色
- 通过data-theme属性切换主题
- 使用changeTheme()函数切换主题
- 使用localStorage.setItem('preferredTheme', currentTheme)保存偏好
- 使用localStorage.getItem('preferredTheme')恢复偏好
CSS变量定义:
:root {
/* 浅色主题变量 / Light theme variables */
--bg-color: #f5f5f5;
--container-bg: #ffffff;
--text-color: #333333;
--section-bg: #f9f9f9;
--section-title-color: #555555;
--border-color: #dddddd;
--btn-primary-bg: #007bff;
--btn-primary-hover: #0056b3;
--btn-success-bg: #28a745;
--btn-success-hover: #218838;
--btn-danger-bg: #dc3545;
--btn-danger-hover: #c82333;
--btn-warning-bg: #ffc107;
--btn-warning-hover: #e0a800;
--btn-warning-text: #212529;
--info-label-color: #666666;
--info-value-color: #333333;
--sd-info-value-color: #333333;
--sd-info-unit-color: #999999;
--setting-label-color: #666666;
--setting-border-color: #dddddd;
--setting-border-hover: #007bff;
--uptime-value-color: #333333;
--uptime-unit-color: #666666;
--language-label-color: #666666;
--loading-text-color: #666666;
--shadow-color: rgba(0, 0, 0, 0.1);
}
[data-theme="dark"] {
/* 深色主题变量 / Dark theme variables */
--bg-color: #1a1a1a;
--container-bg: #2d2d2d;
--text-color: #e0e0e0;
--section-bg: #383838;
--section-title-color: #b0b0b0;
--border-color: #555555;
--btn-primary-bg: #007bff;
--btn-primary-hover: #0056b3;
--btn-success-bg: #28a745;
--btn-success-hover: #218838;
--btn-danger-bg: #dc3545;
--btn-danger-hover: #c82333;
--btn-warning-bg: #ffc107;
--btn-warning-hover: #e0a800;
--btn-warning-text: #212529;
--info-label-color: #b0b0b0;
--info-value-color: #e0e0e0;
--sd-info-value-color: #e0e0e0;
--sd-info-unit-color: #888888;
--setting-label-color: #b0b0b0;
--setting-border-color: #555555;
--setting-border-hover: #007bff;
--uptime-value-color: #e0e0e0;
--uptime-unit-color: #b0b0b0;
--language-label-color: #b0b0b0;
--loading-text-color: #b0b0b0;
--shadow-color: rgba(0, 0, 0, 0.3);
}
/* 全局过渡效果 / Global transition effect */
* {
transition: background-color 0.3s, color 0.3s, border-color 0.3s;
}主题切换函数:
function changeTheme() {
const themeSelect = document.getElementById('theme-select');
currentTheme = themeSelect.value;
// 设置data-theme属性 / Set data-theme attribute
if (currentTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
} else {
document.documentElement.removeAttribute('data-theme');
}
// 保存主题偏好到localStorage / Save theme preference to localStorage
localStorage.setItem('preferredTheme', currentTheme);
}- 拍照:拍摄一张照片,在新窗口打开
- 在新标签页中打开视频流:在新标签页中打开视频流
- 视频流状态
- 拍照状态
- preferredLanguage:保存用户选择的语言('en'或'zh')
- preferredTheme:保存用户选择的主题('light'或'dark')
// 页面加载时恢复语言和主题偏好 / Restore language and theme preferences on page load
window.onload = function() {
// 恢复语言偏好 / Restore language preference
const savedLanguage = localStorage.getItem('preferredLanguage');
if (savedLanguage) {
document.getElementById('language-select').value = savedLanguage;
currentLanguage = savedLanguage;
changeLanguage();
}
// 恢复主题偏好 / Restore theme preference
const savedTheme = localStorage.getItem('preferredTheme');
if (savedTheme) {
document.getElementById('theme-select').value = savedTheme;
currentTheme = savedTheme;
changeTheme();
}
};GET /stream
响应:
Content-Type: multipart/x-mixed-replace;boundary=123456789000000000000987654321
--123456789000000000000987654321
Content-Type: image/jpeg
Content-Length: 12345
X-Timestamp: 1234567890.123456
[JPEG数据]
--123456789000000000000987654321
...
GET /capture
响应:
- Content-Type: image/jpeg
- 响应体:JPEG图像数据
GET /status
响应:
{
"xclk": 22,
"pixformat": 3,
"framesize": 7,
"quality": 10,
"brightness": 1,
"contrast": 0,
"saturation": 0,
"sharpness": 0,
"special_effect": 0,
"wb_mode": 0,
"awb": 1,
"awb_gain": 1,
"aec": 1,
"aec2": 1,
"ae_level": 0,
"aec_value": 300,
"agc": 1,
"agc_gain": 0,
"gainceiling": 0,
"bpc": 0,
"wpc": 1,
"raw_gma": 1,
"lenc": 1,
"hmirror": 0,
"dcw": 1,
"colorbar": 0,
"led_intensity": -1
}ESP32-S3监控/
├── ESP32_S3_Camera_Monitor.ino # 主程序文件
├── camera_pins.h # 摄像头引脚配置
├── camera_index.h # Web界面HTML
├── app_httpd.cpp # HTTP服务器实现
├── partitions.csv # 分区表文件
├── readme/
│ ├── 项目需求.md
│ ├── 技术栈.md
│ ├── 开发文档.md
│ ├── 实施文档.md
│ ├── 功能更新.md
│ └── 例程/
│ ├── Sketch_07.1_CameraWebServer.ino
│ ├── app_httpd.cpp
│ ├── camera_index.h
│ ├── camera_pins.h
│ └── partitions.csv
// WiFi配置
const char* ssid = "your-ssid"; // WiFi名称
const char* password = "your-password"; // WiFi密码// 摄像头配置
#define CAMERA_MODEL_ESP32S3_EYE // 摄像头型号
#define CAMERA_XCLK_FREQ 20000000 // 摄像头时钟频率:22MHz
#define CAMERA_FRAME_SIZE FRAMESIZE_UXGA // 分辨率:UXGA
#define CAMERA_JPEG_QUALITY 12 // JPEG质量:12(1-31,越小越好)
#define CAMERA_FB_COUNT 1 // 帧缓冲区数量:1
// 如果有PSRAM,使用更高的分辨率和质量
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// 没有PSRAM时限制帧大小
config.frame_size = FRAMESIZE_XGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}// 摄像头初始化失败
if (esp_camera_init(&config) != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
return;
}
// 获取帧失败
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
ESP_LOGE(TAG, "Camera capture failed");
httpd_resp_send_500(req);
return ESP_FAIL;
}// WiFi连接
WiFi.begin(ssid, password);
WiFi.setSleep(false);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");- 使用PSRAM存储摄像头帧缓冲区
- 合理设置帧缓冲区数量(2个)
- 及时释放不再使用的内存
- 避免内存泄漏
- 优化JPEG编码质量平衡画质和性能
- 合理分配CPU资源
- 使用WiFi保持连接,避免频繁重连
- 支持视频流重试机制
- 测试视频流是否正常显示
- 测试视频流重试机制
- 测试在新标签页中打开视频流功能
- 测试拍照是否正常
- 测试照片质量
- 测试拍照响应速度
- 测试视频流长时间稳定性
- 测试WiFi连接稳定性
- 测试摄像头持续运行稳定性
- 下载Arduino IDE 2.x
- 安装ESP32开发板支持
// 在Arduino IDE中安装以下库:
ArduinoJson
- 选择开发板:ESP32S3 Dev Module
- 配置分区方案:使用提供的partitions.csv
- 配置PSRAM:OPI PSRAM
- 配置Flash模式:QIO
- 配置Flash大小:8MB(根据实际硬件)
- 连接ESP32-S3-CAM到电脑
- 选择正确的串口
- 点击上传按钮
- 等待编译和上传完成
- 给ESP32-S3-CAM上电
- 等待WiFi连接
- 查看串口输出获取IP地址
- 在浏览器中访问IP地址
- 测试视频流显示功能
- 测试拍照功能
- 测试在新标签页中打开视频流功能
- 检查系统日志
- 监控设备运行状态
- 监控SD卡存储空间
- 系统启动时自动清理无效视频文件(大小为0KB)
- 定期检查视频录制质量和文件完整性
- SD卡空间自动管理:
- 动态阈值检测:当剩余空间 < 5GB时触发清理
- 按时间删除最旧文件:优先删除最旧的文件
- 清理目标:每次清理释放约2GB空间
- 清理优先级:可配置(只删除视频、优先删除视频再删除照片、只删除照片)
- 空间检测频率:默认5秒检测一次
- 检查WiFi连接
- 检查摄像头初始化
- 检查浏览器兼容性
- 检查网络带宽
- 检查摄像头是否正常工作
- 检查网络连接
- 下载新版本固件
- 通过OTA上传新固件到ESP32-S3-CAM
- 更新成功后设备会自动重启
- 测试各项功能
- 访问设备Web界面
- 进入OTA升级页面
- 选择firmware.bin文件
- 点击上传
- 等待上传和验证完成
- 设备会自动重启
- 设备必须连接WiFi
- 用户必须已认证
- flash内存必须有足够的空闲空间
- 上传期间网络连接必须稳定
- 使用强WiFi密码
- 定期更换WiFi密码
- 考虑使用WPA3加密
- 避免在公共网络中使用
- 避免在视频中暴露隐私信息
- 确保设备安装在安全位置
- 避免设备暴露在恶劣环境中
- 定期检查设备状态
- 确保电源供应稳定
- 添加移动侦测功能
- 添加OTA更新功能
- 支持更高分辨率
- 支持更高帧率
- 优化视频流性能
- 制作组装教程发布到B站
文档版本: 1.3 创建日期: 2026-01-26 最后更新: 2026-02.3