Skip to content

Instantly share code, notes, and snippets.

@me-no-dev
Last active April 21, 2024 17:24
Show Gist options
  • Star 32 You must be signed in to star a gist
  • Fork 9 You must be signed in to fork a gist
  • Save me-no-dev/d34fba51a8f059ac559bf62002e61aa3 to your computer and use it in GitHub Desktop.
Save me-no-dev/d34fba51a8f059ac559bf62002e61aa3 to your computer and use it in GitHub Desktop.
#include "Arduino.h"
#include "esp_camera.h"
#include "ESPAsyncWebServer.h"
typedef struct {
camera_fb_t * fb;
size_t index;
} camera_frame_t;
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* STREAM_PART = "Content-Type: %s\r\nContent-Length: %u\r\n\r\n";
static const char * JPG_CONTENT_TYPE = "image/jpeg";
static const char * BMP_CONTENT_TYPE = "image/x-windows-bmp";
class AsyncBufferResponse: public AsyncAbstractResponse {
private:
uint8_t * _buf;
size_t _len;
size_t _index;
public:
AsyncBufferResponse(uint8_t * buf, size_t len, const char * contentType){
_buf = buf;
_len = len;
_callback = nullptr;
_code = 200;
_contentLength = _len;
_contentType = contentType;
_index = 0;
}
~AsyncBufferResponse(){
if(_buf != nullptr){
free(_buf);
}
}
bool _sourceValid() const { return _buf != nullptr; }
virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override{
size_t ret = _content(buf, maxLen, _index);
if(ret != RESPONSE_TRY_AGAIN){
_index += ret;
}
return ret;
}
size_t _content(uint8_t *buffer, size_t maxLen, size_t index){
memcpy(buffer, _buf+index, maxLen);
if((index+maxLen) == _len){
free(_buf);
_buf = nullptr;
}
return maxLen;
}
};
class AsyncFrameResponse: public AsyncAbstractResponse {
private:
camera_fb_t * fb;
size_t _index;
public:
AsyncFrameResponse(camera_fb_t * frame, const char * contentType){
_callback = nullptr;
_code = 200;
_contentLength = frame->len;
_contentType = contentType;
_index = 0;
fb = frame;
}
~AsyncFrameResponse(){
if(fb != nullptr){
esp_camera_fb_return(fb);
}
}
bool _sourceValid() const { return fb != nullptr; }
virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override{
size_t ret = _content(buf, maxLen, _index);
if(ret != RESPONSE_TRY_AGAIN){
_index += ret;
}
return ret;
}
size_t _content(uint8_t *buffer, size_t maxLen, size_t index){
memcpy(buffer, fb->buf+index, maxLen);
if((index+maxLen) == fb->len){
esp_camera_fb_return(fb);
fb = nullptr;
}
return maxLen;
}
};
class AsyncJpegStreamResponse: public AsyncAbstractResponse {
private:
camera_frame_t _frame;
size_t _index;
size_t _jpg_buf_len;
uint8_t * _jpg_buf;
uint64_t lastAsyncRequest;
public:
AsyncJpegStreamResponse(){
_callback = nullptr;
_code = 200;
_contentLength = 0;
_contentType = STREAM_CONTENT_TYPE;
_sendContentLength = false;
_chunked = true;
_index = 0;
_jpg_buf_len = 0;
_jpg_buf = NULL;
lastAsyncRequest = 0;
memset(&_frame, 0, sizeof(camera_frame_t));
}
~AsyncJpegStreamResponse(){
if(_frame.fb){
if(_frame.fb->format != PIXFORMAT_JPEG){
free(_jpg_buf);
}
esp_camera_fb_return(_frame.fb);
}
}
bool _sourceValid() const {
return true;
}
virtual size_t _fillBuffer(uint8_t *buf, size_t maxLen) override {
size_t ret = _content(buf, maxLen, _index);
if(ret != RESPONSE_TRY_AGAIN){
_index += ret;
}
return ret;
}
size_t _content(uint8_t *buffer, size_t maxLen, size_t index){
if(!_frame.fb || _frame.index == _jpg_buf_len){
if(index && _frame.fb){
uint64_t end = (uint64_t)micros();
int fp = (end - lastAsyncRequest) / 1000;
log_printf("Size: %uKB, Time: %ums (%.1ffps)\n", _jpg_buf_len/1024, fp);
lastAsyncRequest = end;
if(_frame.fb->format != PIXFORMAT_JPEG){
free(_jpg_buf);
}
esp_camera_fb_return(_frame.fb);
_frame.fb = NULL;
_jpg_buf_len = 0;
_jpg_buf = NULL;
}
if(maxLen < (strlen(STREAM_BOUNDARY) + strlen(STREAM_PART) + strlen(JPG_CONTENT_TYPE) + 8)){
//log_w("Not enough space for headers");
return RESPONSE_TRY_AGAIN;
}
//get frame
_frame.index = 0;
_frame.fb = esp_camera_fb_get();
if (_frame.fb == NULL) {
log_e("Camera frame failed");
return 0;
}
if(_frame.fb->format != PIXFORMAT_JPEG){
unsigned long st = millis();
bool jpeg_converted = frame2jpg(_frame.fb, 80, &_jpg_buf, &_jpg_buf_len);
if(!jpeg_converted){
log_e("JPEG compression failed");
esp_camera_fb_return(_frame.fb);
_frame.fb = NULL;
_jpg_buf_len = 0;
_jpg_buf = NULL;
return 0;
}
log_i("JPEG: %lums, %uB", millis() - st, _jpg_buf_len);
} else {
_jpg_buf_len = _frame.fb->len;
_jpg_buf = _frame.fb->buf;
}
//send boundary
size_t blen = 0;
if(index){
blen = strlen(STREAM_BOUNDARY);
memcpy(buffer, STREAM_BOUNDARY, blen);
buffer += blen;
}
//send header
size_t hlen = sprintf((char *)buffer, STREAM_PART, JPG_CONTENT_TYPE, _jpg_buf_len);
buffer += hlen;
//send frame
hlen = maxLen - hlen - blen;
if(hlen > _jpg_buf_len){
maxLen -= hlen - _jpg_buf_len;
hlen = _jpg_buf_len;
}
memcpy(buffer, _jpg_buf, hlen);
_frame.index += hlen;
return maxLen;
}
size_t available = _jpg_buf_len - _frame.index;
if(maxLen > available){
maxLen = available;
}
memcpy(buffer, _jpg_buf+_frame.index, maxLen);
_frame.index += maxLen;
return maxLen;
}
};
void sendBMP(AsyncWebServerRequest *request){
camera_fb_t * fb = esp_camera_fb_get();
if (fb == NULL) {
log_e("Camera frame failed");
request->send(501);
return;
}
uint8_t * buf = NULL;
size_t buf_len = 0;
unsigned long st = millis();
bool converted = frame2bmp(fb, &buf, &buf_len);
log_i("BMP: %lums, %uB", millis() - st, buf_len);
esp_camera_fb_return(fb);
if(!converted){
request->send(501);
return;
}
AsyncBufferResponse * response = new AsyncBufferResponse(buf, buf_len, BMP_CONTENT_TYPE);
if (response == NULL) {
log_e("Response alloc failed");
request->send(501);
return;
}
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void sendJpg(AsyncWebServerRequest *request){
camera_fb_t * fb = esp_camera_fb_get();
if (fb == NULL) {
log_e("Camera frame failed");
request->send(501);
return;
}
if(fb->format == PIXFORMAT_JPEG){
AsyncFrameResponse * response = new AsyncFrameResponse(fb, JPG_CONTENT_TYPE);
if (response == NULL) {
log_e("Response alloc failed");
request->send(501);
return;
}
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
return;
}
size_t jpg_buf_len = 0;
uint8_t * jpg_buf = NULL;
unsigned long st = millis();
bool jpeg_converted = frame2jpg(fb, 80, &jpg_buf, &jpg_buf_len);
esp_camera_fb_return(fb);
if(!jpeg_converted){
log_e("JPEG compression failed: %lu", millis());
request->send(501);
return;
}
log_i("JPEG: %lums, %uB", millis() - st, jpg_buf_len);
AsyncBufferResponse * response = new AsyncBufferResponse(jpg_buf, jpg_buf_len, JPG_CONTENT_TYPE);
if (response == NULL) {
log_e("Response alloc failed");
request->send(501);
return;
}
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void streamJpg(AsyncWebServerRequest *request){
AsyncJpegStreamResponse *response = new AsyncJpegStreamResponse();
if(!response){
request->send(501);
return;
}
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void getCameraStatus(AsyncWebServerRequest *request){
static char json_response[1024];
sensor_t * s = esp_camera_sensor_get();
if(s == NULL){
request->send(501);
return;
}
char * p = json_response;
*p++ = '{';
p+=sprintf(p, "\"framesize\":%u,", s->status.framesize);
p+=sprintf(p, "\"quality\":%u,", s->status.quality);
p+=sprintf(p, "\"brightness\":%d,", s->status.brightness);
p+=sprintf(p, "\"contrast\":%d,", s->status.contrast);
p+=sprintf(p, "\"saturation\":%d,", s->status.saturation);
p+=sprintf(p, "\"sharpness\":%d,", s->status.sharpness);
p+=sprintf(p, "\"special_effect\":%u,", s->status.special_effect);
p+=sprintf(p, "\"wb_mode\":%u,", s->status.wb_mode);
p+=sprintf(p, "\"awb\":%u,", s->status.awb);
p+=sprintf(p, "\"awb_gain\":%u,", s->status.awb_gain);
p+=sprintf(p, "\"aec\":%u,", s->status.aec);
p+=sprintf(p, "\"aec2\":%u,", s->status.aec2);
p+=sprintf(p, "\"denoise\":%u,", s->status.denoise);
p+=sprintf(p, "\"ae_level\":%d,", s->status.ae_level);
p+=sprintf(p, "\"aec_value\":%u,", s->status.aec_value);
p+=sprintf(p, "\"agc\":%u,", s->status.agc);
p+=sprintf(p, "\"agc_gain\":%u,", s->status.agc_gain);
p+=sprintf(p, "\"gainceiling\":%u,", s->status.gainceiling);
p+=sprintf(p, "\"bpc\":%u,", s->status.bpc);
p+=sprintf(p, "\"wpc\":%u,", s->status.wpc);
p+=sprintf(p, "\"raw_gma\":%u,", s->status.raw_gma);
p+=sprintf(p, "\"lenc\":%u,", s->status.lenc);
p+=sprintf(p, "\"hmirror\":%u,", s->status.hmirror);
p+=sprintf(p, "\"vflip\":%u,", s->status.vflip);
p+=sprintf(p, "\"dcw\":%u,", s->status.dcw);
p+=sprintf(p, "\"colorbar\":%u", s->status.colorbar);
*p++ = '}';
*p++ = 0;
AsyncWebServerResponse * response = request->beginResponse(200, "application/json", json_response);
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
void setCameraVar(AsyncWebServerRequest *request){
if(!request->hasArg("var") || !request->hasArg("val")){
request->send(404);
return;
}
String var = request->arg("var");
const char * variable = var.c_str();
int val = atoi(request->arg("val").c_str());
sensor_t * s = esp_camera_sensor_get();
if(s == NULL){
request->send(501);
return;
}
int res = 0;
if(!strcmp(variable, "framesize")) res = s->set_framesize(s, (framesize_t)val);
else if(!strcmp(variable, "quality")) res = s->set_quality(s, val);
else if(!strcmp(variable, "contrast")) res = s->set_contrast(s, val);
else if(!strcmp(variable, "brightness")) res = s->set_brightness(s, val);
else if(!strcmp(variable, "saturation")) res = s->set_saturation(s, val);
else if(!strcmp(variable, "sharpness")) res = s->set_sharpness(s, val);
else if(!strcmp(variable, "gainceiling")) res = s->set_gainceiling(s, (gainceiling_t)val);
else if(!strcmp(variable, "colorbar")) res = s->set_colorbar(s, val);
else if(!strcmp(variable, "awb")) res = s->set_whitebal(s, val);
else if(!strcmp(variable, "agc")) res = s->set_gain_ctrl(s, val);
else if(!strcmp(variable, "aec")) res = s->set_exposure_ctrl(s, val);
else if(!strcmp(variable, "hmirror")) res = s->set_hmirror(s, val);
else if(!strcmp(variable, "vflip")) res = s->set_vflip(s, val);
else if(!strcmp(variable, "awb_gain")) res = s->set_awb_gain(s, val);
else if(!strcmp(variable, "agc_gain")) res = s->set_agc_gain(s, val);
else if(!strcmp(variable, "aec_value")) res = s->set_aec_value(s, val);
else if(!strcmp(variable, "aec2")) res = s->set_aec2(s, val);
else if(!strcmp(variable, "denoise")) res = s->set_denoise(s, val);
else if(!strcmp(variable, "dcw")) res = s->set_dcw(s, val);
else if(!strcmp(variable, "bpc")) res = s->set_bpc(s, val);
else if(!strcmp(variable, "wpc")) res = s->set_wpc(s, val);
else if(!strcmp(variable, "raw_gma")) res = s->set_raw_gma(s, val);
else if(!strcmp(variable, "lenc")) res = s->set_lenc(s, val);
else if(!strcmp(variable, "special_effect")) res = s->set_special_effect(s, val);
else if(!strcmp(variable, "wb_mode")) res = s->set_wb_mode(s, val);
else if(!strcmp(variable, "ae_level")) res = s->set_ae_level(s, val);
else {
log_e("unknown setting %s", var.c_str());
request->send(404);
return;
}
log_d("Got setting %s with value %d. Res: %d", var.c_str(), val, res);
AsyncWebServerResponse * response = request->beginResponse(200);
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
}
AsyncWebServer server(80);
void setup(){
Serial.begin(115200);
Serial.setDebugOutput(true);
server.on("/bmp", HTTP_GET, sendBMP);
server.on("/capture", HTTP_GET, sendJpg);
server.on("/stream", HTTP_GET, streamJpg);
server.on("/control", HTTP_GET, setCameraVar);
server.on("/status", HTTP_GET, getCameraStatus);
server.begin();
}
@andrempo
Copy link

andrempo commented Sep 1, 2020

@me-no-dev Is there any way to limit the framerate? I am kind confused how could a do that without messing with the main loop..
Thanks

@me-no-dev
Copy link
Author

you can use lower XCLK :)

@Hoeby
Copy link

Hoeby commented Sep 5, 2020

how to use this cpp file?
I don't see a .ino file or how it connects to wifi

@davybruyere
Copy link

Hi, can you explain me, how I can use this file with platfromIO! Thanks
I have tryed to add this cpp file in "SRC" folder with "main.cpp" and i have a trouble when i want to compile it!

@dnzunal
Copy link

dnzunal commented May 4, 2021

could not run
could you please share full project

@Hoeby
Copy link

Hoeby commented May 5, 2021

Have a look at my project, which is build on this code.

https://github.com/Hoeby/ESP32-Doorbell

@B4stl3r
Copy link

B4stl3r commented Jan 13, 2022

good example, thx 4 that.

wouldn't it be a good idea, do store the images in a ringbuffer (maybe even PSRAM?) and serve the clients from there?
Then it might be possible to serve multiple clients (without breaking up the image data) ?

Also the framerate is currently really limited (1024 ~5-8fps).
is there a possibility to increase the amount of data which is transferred on each async call?

@tranvnhan
Copy link

Thanks for this amazing gist. I can stream the camera successfully.
However, the code to control doesn't work for me.
The execution stops at this condition:

if (!request->hasArg("var") || !request->hasArg("val")) {
request->send(404);
return;
}

Do you have any idea of what the problem could be? Thanks.

@me-no-dev
Copy link
Author

Thanks for this amazing gist. I can stream the camera successfully. However, the code to control doesn't work for me. The execution stops at this condition:

if (!request->hasArg("var") || !request->hasArg("val")) { request->send(404); return; }

Do you have any idea of what the problem could be? Thanks.

It's an old example. You might need to adjust the control code depending on what the web page sends and things that could have changed in the camera driver.

@tranvnhan
Copy link

Thanks for this amazing gist. I can stream the camera successfully. However, the code to control doesn't work for me. The execution stops at this condition:
if (!request->hasArg("var") || !request->hasArg("val")) { request->send(404); return; }
Do you have any idea of what the problem could be? Thanks.

It's an old example. You might need to adjust the control code depending on what the web page sends and things that could have changed in the camera driver.

Thanks. It is resolved by modifying the HTML and javascript code.

I am facing another issue here :D
It appears that multi-clients streaming is not working. When I open the stream in one tab, I cannot open the stream on any additional tabs. As I understand, the purpose of asynchronous webserver is to be able to serve multiple clients. May I know what could be the problem?
Sorry that I'm not very familiar with webserver.

@06GitHub
Copy link

06GitHub commented Apr 14, 2023

Indeed the streaming works OK, many thanks to me-no-dev for development 👍 and to tranvnhan for highlighting this.

Note that :

  • Wifi has to be started and camera initiated, see example code below (works for me, should be added in setup())
  • parameters "var" and "val" are needed in url for "/control", example url "http://<IP>/control?var=quality&val=15" :
 if(!request->hasArg("var") || !request->hasArg("val")){
        request->send(404);
        return;
  }
  • code works also for format "PIXFORMAT_GRAYSCALE" (not only for "PIXFORMAT_JPEG")
  • loop() must be added

Configuration
ESP32-S3 development board (16Mb Flash 8Mb PSRAM)
Arduino IDE 1.8.18
arduino-esp32 core 2.0.7
camera module 0V2640

Wifi start and camera init (added in setup())

  // ==== UPDATE : start Wifi and init camera ====
    // WiFi credentials
    const char* ssid = "MYSSID";
    const char* password = "MYPASSWORD";
    
    // Camera pins
    #define PWDN_GPIO_NUM    42
    #define RESET_GPIO_NUM   -1
    #define XCLK_GPIO_NUM    -1 // XLCL is embedded on OV2640 breakout board
    #define SIOD_GPIO_NUM    40
    #define SIOC_GPIO_NUM    41
    #define Y9_GPIO_NUM      46
    #define Y8_GPIO_NUM      39 
    #define Y7_GPIO_NUM      38
    #define Y6_GPIO_NUM      2
    #define Y5_GPIO_NUM      6
    #define Y4_GPIO_NUM      14
    #define Y3_GPIO_NUM      15 
    #define Y2_GPIO_NUM      47
    #define VSYNC_GPIO_NUM   4
    #define HREF_GPIO_NUM    17
    #define PCLK_GPIO_NUM    45
    
    // Start Wifi
    WiFi.begin(ssid, password);
    WiFi.setSleep(false);
  
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      log_d(".");
    }
    log_d("WiFi connected");

    // init camera
    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.xclk_freq_hz = 10000000;  
    config.frame_size = FRAMESIZE_QVGA;
    //config.pixel_format = PIXFORMAT_GRAYSCALE;    
    config.pixel_format = PIXFORMAT_JPEG;
    config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
    config.fb_location = CAMERA_FB_IN_PSRAM;
    config.jpeg_quality = 12;
    config.fb_count = 1;

    // camera init
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) {
      log_e("ERROR : Camera init failed with error 0x%x\n", err);
      return;
    }
    else {
      log_d("Camera init OK");
    }
  
    sensor_t * s = esp_camera_sensor_get();
    log_d("Sensor PID : %d\n",s->id.PID);
      
    // =============================================

Main loop
// ==== UPDATE : main loop ====
void loop(){
vTaskDelay(5000 / portTICK_PERIOD_MS);
}
// ============================

@mmame
Copy link

mmame commented Jul 19, 2023

Here some changes to get it running with opencv.
Hint: this requires that AsyncAbstractResponse's private member _head is changed to protected.
https://gist.github.com/mmame/f4c8c095197dbc15c7ccf033a8a1f895/revisions?diff=split

@spawn451
Copy link

spawn451 commented Aug 6, 2023

Thanks it work perfect, but how can I make it works with VLC ? The endpoint url with /stream doesnt work.

VLC Log:

HTTP/1.1 200 OK

Content-Type: multipart/x-mixed-replace; boundary=123456789000000000000987654321

Access-Control-Allow-Origin: *

Connection: close

Accept-Ranges: none

Transfer-Encoding: chunked

@spawn451
Copy link

spawn451 commented Aug 7, 2023

Seems _chunked = false; allows to make it works with VLC

@zekageri
Copy link

zekageri commented Sep 9, 2023

Can it work on websockets?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment