|
| 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