Skip to content

Commit ba4ab07

Browse files
committed
MJPEG Cam Video streaming over WebSockets using AI-Thinker ESP32-CAM module
This example implements a WebCam streaming in browser using cheap ESP32-cam module. The feature here is that frames are trasfered to the browser via WebSockets, it has several advantages over traditional streamed multipart content via HTTP - websockets delivers each frame in a separate message - webserver is not blocked when stream is flowing, you can still send/receive other data via HTTP - websockets can multiplex vide data and control messages, in this example you can also get memory stats / frame sizing from controller along with video stream - WSocketServer can easily replicate stream to multiple connected clients here in this example you can connect 2-3 clients simultaneously and get smooth stream (limited by wifi bandwidth)
1 parent 8c56d01 commit ba4ab07

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
3+
4+
//
5+
// MJPEG Cam Video streaming over WebSockets using AI-Thinker ESP32-CAM module
6+
// you would need a module with camera to run this example https://www.espboards.dev/esp32/esp32cam/
7+
8+
/*
9+
This example implements a WebCam streaming in browser using cheap ESP32-cam module.
10+
The feature here is that frames are trasfered to the browser via WebSockets,
11+
it has several advantages over traditional streamed multipart content via HTTP
12+
- websockets delivers each frame in a separate message
13+
- webserver is not blocked when stream is flowing, you can still send/receive other data via HTTP
14+
- websockets can multiplex vide data and control messages, in this example you can also get memory
15+
stats / frame sizing from controller along with video stream
16+
- WSocketServer can easily replicate stream to multiple connected clients
17+
here in this example you can connect 2-3 clients simultaneously and get smooth stream (limited by wifi bandwidth)
18+
19+
Change you WiFi creds below, build and flash the code.
20+
Connect to console to monitor for debug messages
21+
Figure out IP of your board and access the board via web browser, watch for video stream
22+
*/
23+
24+
#include <AsyncWSocket.h>
25+
#include <WiFi.h>
26+
#include "esp_camera.h"
27+
#include "mjpeg.h"
28+
29+
30+
#define WIFI_SSID "your_ssid"
31+
#define WIFI_PASSWD "your_pass"
32+
33+
// AI-Thinker ESP32-CAM config - for more details see https://github.com/rzeldent/esp32cam-rtsp/ project
34+
camera_config_t cfg {
35+
.pin_pwdn = 32,
36+
.pin_reset = -1,
37+
.pin_xclk = 0,
38+
39+
.pin_sscb_sda = 26,
40+
.pin_sscb_scl = 27,
41+
42+
// Note: LED GPIO is apparently 4 not sure where that goes
43+
// per https://github.com/donny681/ESP32_CAMERA_QR/blob/e4ef44549876457cd841f33a0892c82a71f35358/main/led.c
44+
.pin_d7 = 35,
45+
.pin_d6 = 34,
46+
.pin_d5 = 39,
47+
.pin_d4 = 36,
48+
.pin_d3 = 21,
49+
.pin_d2 = 19,
50+
.pin_d1 = 18,
51+
.pin_d0 = 5,
52+
.pin_vsync = 25,
53+
.pin_href = 23,
54+
.pin_pclk = 22,
55+
.xclk_freq_hz = 20000000,
56+
.ledc_timer = LEDC_TIMER_1,
57+
.ledc_channel = LEDC_CHANNEL_1,
58+
.pixel_format = PIXFORMAT_JPEG,
59+
// .frame_size = FRAMESIZE_UXGA, // needs 234K of framebuffer space
60+
// .frame_size = FRAMESIZE_SXGA, // needs 160K for framebuffer
61+
// .frame_size = FRAMESIZE_XGA, // needs 96K or even smaller FRAMESIZE_SVGA - can work if using only 1 fb
62+
.frame_size = FRAMESIZE_SVGA,
63+
.jpeg_quality = 15, //0-63 lower numbers are higher quality
64+
.fb_count = 2, // if more than one i2s runs in continous mode. Use only with jpeg
65+
.fb_location = CAMERA_FB_IN_PSRAM,
66+
.grab_mode = CAMERA_GRAB_LATEST,
67+
.sccb_i2c_port = 0
68+
};
69+
70+
// camera frame buffer pointer
71+
camera_fb_t *fb{nullptr};
72+
73+
// WS Event server callback declaration
74+
void wsEvent(WSocketClient *client, WSocketClient::event_t event);
75+
76+
// Our WebServer
77+
AsyncWebServer server(80);
78+
// WSocket Server URL and callback function
79+
WSocketServer ws("/wsstream", wsEvent);
80+
// a unique steam id - it is used to avoid sending dublicate frames when multiple clients connected to stream
81+
uint32_t client_id{0};
82+
83+
void sendFrame(uint32_t token){
84+
if (client_id == 0){
85+
// first client connected grabs the stream token
86+
client_id = token;
87+
} else if (token != client_id) {
88+
// we will send next frame only upon delivery the previous one to client owning the token, others we ignore
89+
// this is to avoid stacking frames clones in the Q when multiple clients are connected to the server
90+
return;
91+
}
92+
93+
//return the frame buffer back to the driver for reuse
94+
esp_camera_fb_return(fb);
95+
// get 2nd buffer from camera
96+
fb = esp_camera_fb_get();
97+
98+
// generate metadata info frame
99+
// this text frame contains memory stat and will be displayed along video stream
100+
char buff[100];
101+
snprintf(buff, 100, "FrameSize:%u, Mem:%lu, PSRAM:%lu", fb->len, ESP.getFreeHeap(), ESP.getFreePsram());
102+
ws.textAll(buff);
103+
104+
// here we MUST ensure that client owning the stream is able to send data, otherwise recursion would crash controller
105+
if (ws.clientState(client_id) == WSocketClient::err_t::ok){
106+
/*
107+
for video frame sending we will use WSMessageStaticBlob object.
108+
It can send large memory buffers directly to websocket peers without intermediate buffering and data copies
109+
and it is the most efficient way to send static data
110+
*/
111+
auto m = std::make_shared<WSMessageStaticBlob>(
112+
WSFrameType_t::binary, // binary message
113+
true, // final message
114+
reinterpret_cast<const char*>(fb->buf), fb->len, // buffer to transfer
115+
// the key here to understand when frame buffer completes delivery - for this we set
116+
// the callback back to ourself, so that when when frame delivery would be completed,
117+
// this function is called again to obtain a new frame buffer from camera
118+
[](WSMessageStatus_t s, uint32_t t){ sendFrame(t); }, // a callback executed on message delivery
119+
client_id // stream token
120+
);
121+
// replicate frame to ALL peers
122+
ws.messageAll(m);
123+
} else {
124+
// current client can't receive stream (maybe he disconnected), we reset token here so that other client
125+
// can reconnect and take the ownership of the stream
126+
client_id = 0;
127+
}
128+
129+
/*
130+
Note! Though this example is able to send video stream to multiple clients simultaneously, it has one gap -
131+
when same buffer is streamed to multiple peers and the 'owner' of stream completes transfer, others might
132+
still be in-progress. The buffer pointer is switched to next one from camera only upon full delivery on
133+
message object destruction. It does not allow pipelining and slower clients could affect the others.
134+
The question of synchronization multiple clients is out of scope of this simple example. It's just a
135+
demonstarion of working with WebSockets.
136+
*/
137+
}
138+
139+
void wsEvent(WSocketClient *client, WSocketClient::event_t event){
140+
switch (event){
141+
// new client connected
142+
case WSocketClient::event_t::connect : {
143+
Serial.printf("Client id:%lu connected\n", client->id);
144+
if (fb)
145+
sendFrame(client->id);
146+
else
147+
ws.text(client->id, "Cam init failed!");
148+
break;
149+
}
150+
151+
// client diconnected
152+
case WSocketClient::event_t::disconnect : {
153+
Serial.printf("Client id:%lu disconnected\n", client->id);
154+
if (client_id == client->id)
155+
// reset stream token
156+
client_id = 0;
157+
break;
158+
}
159+
160+
// any other events
161+
default:;
162+
// incoming messages could be used for controls or any other functionality
163+
// not implemented in this example but should be considered
164+
// If not discaded messages will overflow incoming Q
165+
client->dequeueMessage();
166+
break;
167+
}
168+
}
169+
170+
void setup() {
171+
Serial.begin(115200);
172+
173+
WiFi.mode(WIFI_STA);
174+
WiFi.begin(WIFI_SSID, WIFI_PASSWD);
175+
176+
// Wait for connection
177+
while (WiFi.status() != WL_CONNECTED) {
178+
delay(250);
179+
Serial.print(".");
180+
}
181+
Serial.println();
182+
Serial.print("Connected to ");
183+
Serial.println(WIFI_SSID);
184+
Serial.print("IP address: ");
185+
Serial.println(WiFi.localIP());
186+
Serial.println();
187+
Serial.printf("to access VideoStream pls open http://%s/\n", WiFi.localIP().toString().c_str());
188+
189+
// init camera
190+
esp_err_t err = esp_camera_init(&cfg);
191+
if (err != ESP_OK)
192+
{
193+
Serial.printf("Camera probe failed with error 0x%x\n", err);
194+
} else
195+
fb = esp_camera_fb_get();
196+
197+
// server serves index page
198+
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
199+
// need to cast to uint8_t*
200+
// if you do not, the const char* will be copied in a temporary String buffer
201+
request->send(200, "text/html", (uint8_t *)htmlPage, std::string_view(htmlPage).length());
202+
});
203+
204+
// attach our WSocketServer
205+
server.addHandler(&ws);
206+
server.begin();
207+
}
208+
209+
210+
void loop() {
211+
// nothing to do here at all
212+
vTaskDelete(NULL);
213+
}

0 commit comments

Comments
 (0)