ESP32-CAM摄像头流与WebSocket无法同时工作问题求助
问题解决:ESP32-CAM摄像头流与WebSocket同时工作方案
核心问题分析
你的代码中handleStream函数是阻塞式的:当客户端请求/stream时,该函数进入无限循环持续发送帧,直到客户端断开连接。在此期间,主循环loop()中的server.handleClient()和webSocket.loop()完全无法执行,导致WebSocket请求被积压,直到流关闭后才批量处理。
解决方案
采用异步Web服务器+异步WebSocket组合,配合独立的摄像头流任务,让两者并行运行互不阻塞。利用ESP32双核架构,将摄像头任务分配到核心1,主逻辑保留在核心0,最大化硬件性能。
关键修改点
- 替换阻塞式
WebServer为非阻塞AsyncWebServer - 替换
WebSocketsClient为与异步服务器适配的AsyncWebSocket - 用FreeRTOS创建独立任务处理摄像头帧的捕获与发送,避免阻塞主线程
- 优化流连接的资源管理,客户端断开时自动清理
修改后的完整代码
#include <WiFi.h> #include <AsyncWebServer.h> #include <AsyncWebSocket.h> #include "esp_camera.h" // 摄像头引脚定义 #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 // WiFi配置 const char* ssid = "你的WiFi名称"; const char* password = "你的WiFi密码"; // 异步服务器与WebSocket AsyncWebServer server(80); AsyncWebSocket ws("/ws"); const char* tankID = "1"; // 摄像头全局变量 camera_fb_t *fb = NULL; bool streamActive = false; AsyncWebServerResponse *streamResponse = NULL; // 初始化摄像头 void startCamera() { 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.pixel_format = PIXFORMAT_JPEG; if (psramFound()) { config.frame_size = FRAMESIZE_VGA; config.jpeg_quality = 12; config.fb_count = 2; } else { config.frame_size = FRAMESIZE_QVGA; config.jpeg_quality = 12; config.fb_count = 1; } esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("摄像头初始化失败: 0x%x", err); while (true); } } // WebSocket事件处理 void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len) { switch (type) { case WS_EVT_CONNECT: Serial.printf("WebSocket客户端[%u]连接\n", client->id()); // 发送注册消息 client->text("{\"type\":\"register\", \"tank\":\"" + String(tankID) + "\"}"); break; case WS_EVT_DISCONNECT: Serial.printf("WebSocket客户端[%u]断开连接\n", client->id()); break; case WS_EVT_DATA: // 处理收到的命令 String payload = String((char*)data, len); Serial.printf("收到命令: %s\n", payload.c_str()); if (payload == "{\"command\":\"forward\"}") { Serial.println("前进!"); } else if (payload == "{\"command\":\"backward\"}") { Serial.println("后退!"); } else if (payload == "{\"command\":\"left\"}") { Serial.println("左转!"); } else if (payload == "{\"command\":\"right\"}") { Serial.println("右转!"); } break; case WS_EVT_PONG: case WS_EVT_ERROR: break; } } // 摄像头流发送任务(运行在核心1) void streamTask(void *pvParameters) { while (true) { if (streamActive && streamResponse) { fb = esp_camera_fb_get(); if (fb) { // 发送帧边界与数据 streamResponse->sendContent("--frame\r\nContent-Type: image/jpeg\r\nContent-Length: " + String(fb->len) + "\r\n\r\n"); streamResponse->sendContent((const char*)fb->buf, fb->len); streamResponse->sendContent("\r\n"); esp_camera_fb_return(fb); fb = NULL; } vTaskDelay(pdMS_TO_TICKS(20)); // 控制帧率约50fps } else { vTaskDelay(pdMS_TO_TICKS(100)); // 无流时降低任务调度频率 } } } // 处理流请求 void handleStream(AsyncWebServerRequest *request) { // 发送响应头 streamResponse = request->beginResponseStream("multipart/x-mixed-replace; boundary=frame"); request->send(streamResponse); streamActive = true; // 客户端断开时清理资源 request->onDisconnect([](){ streamActive = false; streamResponse = NULL; Serial.println("流连接断开"); }); } void setup() { Serial.begin(115200); // 连接WiFi WiFi.begin(ssid, password); Serial.print("连接WiFi..."); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println("\n连接成功!"); Serial.print("IP地址: "); Serial.println(WiFi.localIP()); // 初始化摄像头 startCamera(); // 配置WebSocket ws.onEvent(onWsEvent); server.addHandler(&ws); // 配置流路由 server.on("/stream", HTTP_GET, handleStream); // 启动异步服务器 server.begin(); Serial.println("异步HTTP服务器启动!"); // 创建摄像头流任务(分配到核心1) xTaskCreatePinnedToCore(streamTask, "StreamTask", 4096, NULL, 1, NULL, 1); } void loop() { ws.cleanupClients(); // 清理断开的WebSocket客户端 delay(10); }
方案说明
- 异步架构:
AsyncWebServer和AsyncWebSocket均为非阻塞实现,流请求与WebSocket消息处理互不干扰 - 独立任务:摄像头流发送在单独的FreeRTOS任务中运行,与主逻辑完全分离,避免阻塞WebSocket响应
- 双核优化:将流任务分配到核心1,主逻辑(WiFi、WebSocket)保留在核心0,充分利用ESP32双核性能
- 资源管理:客户端断开流连接时自动标记流状态,释放相关资源,避免内存泄漏
内容的提问来源于stack exchange,提问作者KamilG




