I would like to stream video camera from ESP32-CAM to web browser. To do so, I use a nodejs server (to broadcast video and serve html) and SocketIO to communicate (between ESP32-CAM -> nodejs and nodejs -> web browser). In this way, this avoid multiple clients to be connected directly to ESP32-CAM and avoid to deal with NAT/router configuration. It acts as relay/repeater not as proxy.
I actually succeed to send video data (throught jpg base64) to nodejs and view it in the web browser.
Here is the code :
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);
}
}
nodejs :
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}`)
})
Web browser :
<!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>
I don't expect the video to be smooth and clear due to hardware limitation, but I don't even have 10fps with ridiculous resolution. The bottleneck seems to come from base64 encode. The ESP32-CAM webserver example is faster ( https://github.com/espressif/arduino-esp32/blob/master/libraries/ESP32/examples/Camera/CameraWebServer/CameraWebServer.ino ) but requires direct access to ESP32-CAM.
Is there a solution to optimise base64 encoding or another way to send data throught socketIO to improve speed ?