ESP32-CAM 使用 socketIO 将视频输出流式传输到 nodejs 服务器的最快方法

Posted

技术标签:

【中文标题】ESP32-CAM 使用 socketIO 将视频输出流式传输到 nodejs 服务器的最快方法【英文标题】:ESP32-CAM fastest way to stream video output to nodejs server with socketIO 【发布时间】:2021-03-09 15:17:03 【问题描述】:

我想将摄像机从 ESP32-CAM 流式传输到网络浏览器。 为此,我使用 nodejs 服务器(广播视频和提供 html)和 SocketIO 进行通信(在 ESP32-CAM -> nodejs 和 nodejs -> Web 浏览器之间)。 这样可以避免多个客户端直接连接到 ESP32-CAM 并避免处理 NAT/路由器配置。它充当中继器/中继器而不是代理。

我实际上成功地将视频数据(通过jpg base64)发送到nodejs并在网络浏览器中查看。

代码如下:

ESP32-CAM:


#include "WiFi.h"
#include "esp_camera.h"
#include "base64.h"

#include <ArduinoJson.h>
#include <WebSocketsClient.h>
#include <SocketIOclient.h>

// Pin definition for CAMERA_MODEL_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


// Replace with your network credentials
const char* hostname = "ESP32CAM";
const char* ssid = "ssid";
const char* password = "pass";
SocketIOclient socketIO;


void socketIOEvent(socketIOmessageType_t type, uint8_t * payload, size_t length) 
    switch(type) 
        case sIOtype_DISCONNECT:
            Serial.printf("[IOc] Disconnected!\n");
            break;
        case sIOtype_CONNECT:
            Serial.printf("[IOc] Connected to url: %s\n", payload);

            // join default namespace (no auto join in Socket.IO V3)
            socketIO.send(sIOtype_CONNECT, "/");
            break;
        case sIOtype_EVENT:
            Serial.printf("[IOc] get event: %s\n", payload);
            break;
        case sIOtype_ACK:
            Serial.printf("[IOc] get ack: %u\n", length);
            break;
        case sIOtype_ERROR:
            Serial.printf("[IOc] get error: %u\n", length);
            break;
        case sIOtype_BINARY_EVENT:
            Serial.printf("[IOc] get binary: %u\n", length);
            break;
        case sIOtype_BINARY_ACK:
            Serial.printf("[IOc] get binary ack: %u\n", length);
            break;
    


void setupCamera()


    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;
    
    config.frame_size = FRAMESIZE_CIF; // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
    config.jpeg_quality = 10;
    config.fb_count = 2;
  
    // Init Camera
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) 
      Serial.printf("Camera init failed with error 0x%x", err);
      return;
    
  
  


void setup()
  Serial.begin(115200);
  
  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) 
    delay(1000);
    Serial.println("Connecting to WiFi..");
  

  // Print ESP32 Local IP Address
  Serial.println(WiFi.localIP());

  setupCamera();
  
  // server address, port and URL
  // without ssl to test speed may change later
  socketIO.begin("server", port,"/socket.io/?EIO=4");

  // event handler
  socketIO.onEvent(socketIOEvent);
    





unsigned long messageTimestamp = 0;
void loop() 
    socketIO.loop();

    uint64_t now = millis();

    if(now - messageTimestamp > 10) 
        messageTimestamp = now;

        camera_fb_t * fb = NULL;

        // Take Picture with Camera
        fb = esp_camera_fb_get();  
        if(!fb) 
          Serial.println("Camera capture failed");
          return;
        
        
        //Slow
        String picture_encoded = base64::encode(fb->buf,fb->len);

        // create JSON message for Socket.IO (event)
        DynamicJsonDocument doc(15000);
        JsonArray array = doc.to<JsonArray>();
        
        // add event name
        // Hint: socket.on('event_name', ....
        array.add("jpgstream_server");

        // add payload (parameters) for the event
        JsonObject param1 = array.createNestedObject();
        param1["hostname"] = hostname;
        param1["picture"] = String((char *)fb->buf);

        // JSON to String (serializion)
        String output;
        serializeJson(doc, output);

        // Send event        
        socketIO.sendEVENT(output);
        Serial.println("Image sent");
        Serial.println(output);
        esp_camera_fb_return(fb); 
    

节点:

const express = require('express');
const app = express();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const port = 3000;

const express_config= require('./config/express.js');

express_config.init(app);

var cameraArray=;


app.get('/', (req, res) => 
    res.render('index', );
);

io.on('connection', (socket) => 
  socket.on('jpgstream_server', (msg) => 
    io.to('webusers').emit('jpgstream_client', msg);
  );
  
  socket.on('webuser', (msg) => 
      socket.join('webusers');      
  );
  
  
);

http.listen(port, () => 
      console.log(`App listening at http://localhost:$port`)
)

网络浏览器:

<!DOCTYPE html>
<html>
<%- include('./partials/head.ejs') %>
<body class="page_display">
    <div class="main_content">
        <div class="page_title"><h1 class="tcenter">Camera relay</h1></div>
        <div class="tcenter">
            <img id="jpgstream" class="jpgstream" src="" />
        </div>
    </div>
    
    <script src="/socket.io/socket.io.js"></script>
    <script>
    var socket = io();
    
    socket.emit("webuser",);

    socket.on('jpgstream_client', function(msg) 
        console.log(msg);
        var x = document.getElementsByTagName("img").item(0);
        x.setAttribute("src", 'data:image/jpg;base64,'+msg.picture);        
    );
    </script>
</body>
</html>

由于硬件限制,我不希望视频流畅清晰,但我什至没有 10fps 的荒谬分辨率。瓶颈似乎来自 base64 编码。 ESP32-CAM 网络服务器示例速度更快(https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/Camera/CameraWebServer/CameraWebServer.ino),但需要直接访问 ESP32-CAM。

是否有优化base64编码的解决方案或通过socketIO发送数据以提高速度的其他方式?

【问题讨论】:

JSON 不是流视频的好解决方案。 JSON 旨在传输大量结构化数据。正如您所指出的,base64 中的编码会影响性能,所以......您是否尝试过不这样做并且不将帧嵌入 JSON 中? @romkey 我可以在没有 JSON 的情况下使用 socketIO 吗?你有例子吗? 当您有这样的问题时,要做的一件好事是 - 编写一个简单的程序,尝试通过 socketIO 发布不是 JSON 的内容,看看它是否有效。或者阅读 SocketIO 文档。 【参考方案1】:

说实话,ESP32 中的所有数据转换(raw->base64->JSON->WebSockets)都不是性能的绝佳选择。但假设您的诊断正确并使用this base64 library,问题可能来自这样一个事实:虽然 ESP32 内核运行速度非常快(240MHz),但其所有代码和数据都来自外部 SPI 连接的闪存。您可以猜到,从那里获取任何东西。它有一个 32KB 的 Flash 缓存,但是 base64 编码的东西很可能在连续帧之间过期。

首先要确保您的内核和闪存的 SPI 总线以最大频率(240MHz、80MHz)运行。不知道它是如何在 Arduino 领域完成的,抱歉。在 ESP-IDF 下,它是通过 idf.py menuconfig 完成的。

其次,您可以通过将代码和数据从闪存移动到 RAM 来调整 base64 库。通过添加IRAM_ATTR 将编码函数移动到instruction RAM。字符表标记为constexpr,这使得编译器将其放入Flash。我怀疑删除 constexpr 会将其加载到数据 RAM 中。

【讨论】:

【参考方案2】:

根据@Tarmo 的回答,SocketIO 似乎不是性能的好选择。

即使 socketIO 有二进制支持,似乎仍然需要转换。

我改用二进制 websocket,性能好多了。

此处提供项目示例:https://github.com/Inglebard/esp32cam-relay

【讨论】:

以上是关于ESP32-CAM 使用 socketIO 将视频输出流式传输到 nodejs 服务器的最快方法的主要内容,如果未能解决你的问题,请参考以下文章

esp32cam接错烧了

ESP32CAM 视频小车

ESP32-CAM带OV2640摄像头视频显示

esp32 cam如何设置静态的IP

ESP32-cam使用-智能家居云端视频监控实现

ESP32-cam使用-智能家居云端视频监控实现