Created
May 27, 2023 18:59
-
-
Save dsseng/b609c80117a183088cdef8ed3ebf9ba3 to your computer and use it in GitHub Desktop.
A snippet to stream from ESP32-CAM using local UDP transport (no error correction etc). Just sends JPEG in chunks, it turns out to be the most performant way. Is not a complete example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <Arduino.h> | |
#include <esp_camera.h> | |
#include <ESPAsyncWebServer.h> | |
#include <AsyncUDP.h> | |
#include <stdint.h> | |
AsyncUDP vid_udp; | |
IPAddress client_ip; | |
uint16_t client_port; | |
// MTU for data in packets | |
uint16_t chunk_size = 1350; | |
unsigned long prev_req = 0; | |
AsyncWebServer camera_ctl_server(8080); | |
// Mutex for locking capture while params are being modified from async http | |
bool cap_mutex = false; | |
// Awaiting to change props, please don't capture | |
bool want_mod = false; | |
// Zero-copy chunk and send the buffer, no allocations needed | |
// Allows to read out PSRAM | |
void send_chunked(const uint8_t* buf, uint16_t len) { | |
const uint8_t* ptr = buf; | |
uint16_t left = len; | |
while (left > chunk_size) { | |
vid_udp.writeTo(ptr, chunk_size, client_ip, client_port); | |
ptr += chunk_size; | |
left -= chunk_size; | |
} | |
// Tail less than a chunk/last chunk | |
if (left) | |
vid_udp.writeTo(ptr, left, client_ip, client_port); | |
} | |
void vid_listener(AsyncUDPPacket packet) { | |
client_ip = packet.remoteIP(); | |
client_port = packet.remotePort(); | |
prev_req = millis(); | |
} | |
void startCameraServer() { | |
camera_config_t config; | |
config.pin_pwdn = 32; | |
config.pin_reset = -1; | |
config.pin_xclk = 0; | |
config.pin_sscb_sda = 26; | |
config.pin_sscb_scl = 27; | |
config.pin_d7 = 35; | |
config.pin_d6 = 34; | |
config.pin_d5 = 39; | |
config.pin_d4 = 36; | |
config.pin_d3 = 21; | |
config.pin_d2 = 19; | |
config.pin_d1 = 18; | |
config.pin_d0 = 5; | |
config.pin_vsync = 25; | |
config.pin_href = 23; | |
config.pin_pclk = 22; | |
config.ledc_channel = LEDC_CHANNEL_0; | |
config.ledc_timer = LEDC_TIMER_0; | |
config.xclk_freq_hz = 20000000; | |
// hw codec | |
config.pixel_format = PIXFORMAT_JPEG; | |
// Expected to run on Ai-Thinker board with PSRAM | |
config.grab_mode = CAMERA_GRAB_LATEST; | |
config.fb_location = CAMERA_FB_IN_PSRAM; | |
// Has to be max at start, dropped to default SVGA later | |
config.frame_size = FRAMESIZE_UXGA; | |
config.jpeg_quality = 10; | |
config.fb_count = 2; | |
esp_err_t err = esp_camera_init(&config); | |
if (err != ESP_OK) { | |
Serial.printf("ERR 0x%x\n", err); | |
return; | |
} | |
sensor_t* s = esp_camera_sensor_get(); | |
s->set_framesize(s, FRAMESIZE_SVGA); | |
camera_ctl_server.on("/control", HTTP_GET, [](AsyncWebServerRequest* request) { | |
want_mod = true; | |
unsigned long to = millis(); | |
while (millis() < to + 5000) { | |
delay(5); | |
if (!cap_mutex) | |
break; | |
} | |
want_mod = false; | |
if (cap_mutex) { | |
request->send(502, "text/plain", "Timeout"); | |
return; | |
} else { | |
cap_mutex = true; | |
} | |
String v = "empty"; | |
/// TODO: expose more sensor controls | |
if (request->hasParam("framesize")) { | |
v = request->getParam("framesize")->value(); | |
sensor_t* s = esp_camera_sensor_get(); | |
s->set_framesize(s, (framesize_t)v.toInt()); | |
} | |
cap_mutex = false; | |
request->send(200, "text/plain", v); | |
}); | |
camera_ctl_server.begin(); | |
if (vid_udp.listen(55181)) | |
vid_udp.onPacket(vid_listener); | |
} | |
void cameraLoop() { | |
// Do not send to 0.0.0.0:0, stop capture after 15s since last request | |
if (!client_port || millis() > prev_req + 15000) { | |
delay(50); | |
return; | |
} | |
// Also skip frames while mutex is locked and settings are being modified | |
if (cap_mutex || want_mod) { | |
delay(10); | |
return; | |
} | |
cap_mutex = true; | |
camera_fb_t* fb = NULL; | |
fb = esp_camera_fb_get(); | |
if (!fb) { | |
Serial.print("Error swapping fb: "); | |
Serial.println((uint32_t)fb); | |
// Not returning invalid fb | |
cap_mutex = false; | |
return; | |
} | |
if (fb->format != PIXFORMAT_JPEG) { | |
Serial.print("Unsupported framebuffer format, expected JPEG: "); | |
Serial.println(fb->format); | |
// Still swap it back to the chain | |
esp_camera_fb_return(fb); | |
cap_mutex = false; | |
return; | |
} | |
send_chunked((const uint8_t*)fb->buf, fb->len); | |
esp_camera_fb_return(fb); | |
cap_mutex = false; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment