You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

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);
}

方案说明

  1. 异步架构AsyncWebServerAsyncWebSocket均为非阻塞实现,流请求与WebSocket消息处理互不干扰
  2. 独立任务:摄像头流发送在单独的FreeRTOS任务中运行,与主逻辑完全分离,避免阻塞WebSocket响应
  3. 双核优化:将流任务分配到核心1,主逻辑(WiFi、WebSocket)保留在核心0,充分利用ESP32双核性能
  4. 资源管理:客户端断开流连接时自动标记流状态,释放相关资源,避免内存泄漏

内容的提问来源于stack exchange,提问作者KamilG

火山引擎 最新活动