Camera capture
A camera on a five pound board, and one hard limit - a frame has to exist in memory before it can be sent. That is why the resolution you pick decides whether the thing works at all, with no error worth reading.
Memory decides the resolution
esp_camera_fb_return() — miss it once and the next fb_get returns null for ever — and take JPEG unless you are doing pixel work, because the sensor compresses it for free and the chip never has to.Take JPEG unless you have a reason not to
The sensor compresses on its own, before the data ever reaches the chip. A VGA JPEG is around 25 kB where the raw frame is 600 kB, and there is no CPU cost because the ESP32 never does the work.
Take raw RGB565 only when you need pixels — face detection, motion detection, drawing on the frame — and then expect to drop the resolution.
PSRAM is not optional
Without it you are limited to about QVGA JPEG with one buffer. That is a board-buying decision, not a code one: check for PSRAM before ordering, and enable it in the board menu, because a board that has it and is configured without it behaves exactly like a board that does not.
Streaming to a browser
The classic approach is multipart/x-mixed-replace — a boundary, a JPEG, a
boundary, forever. It is a few dozen lines on top of the async web server, and
it is what every ESP32-CAM example does. Two viewers at once will halve the
frame rate; three will make it unusable.
The code
Init, grab, return. The return is the line people forget, and forgetting it once means every later capture returns null - which looks like a camera that died.
#include "esp_camera.h"
void setup() {
Serial.begin(115200);
camera_config_t c = {};
// ... pin assignments for your board go here ...
c.pixel_format = PIXFORMAT_JPEG; // let the sensor compress it
c.frame_size = FRAMESIZE_VGA; // 640x480
c.jpeg_quality = 12; // lower number, bigger file
c.fb_count = psramFound() ? 2 : 1;
c.grab_mode = CAMERA_GRAB_LATEST;
esp_err_t err = esp_camera_init(&c);
if (err != ESP_OK) {
Serial.printf("init failed 0x%x (0x101 = out of memory)\n", err);
return;
}
}
void loop() {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) { Serial.println("capture failed"); return; }
Serial.printf("%u bytes, %ux%u\n", fb->len, fb->width, fb->height);
// ... send fb->buf somewhere ...
esp_camera_fb_return(fb); // ALWAYS. Miss it once and it stops.
delay(1000);
}grab_mode LATEST rather than WHEN_EMPTY gives you the newest frame instead of a stale one from the queue. It matters the moment anything is looking at the picture.
Camera support in MicroPython comes from community builds rather than the standard firmware. The API is smaller and the same rules apply - JPEG, and check for PSRAM.
import camera
camera.init(0, format=camera.JPEG, framesize=camera.FRAME_VGA)
camera.quality(12)
buf = camera.capture()
print(len(buf), 'bytes')
with open('/sd/snap.jpg', 'wb') as f:
f.write(buf)
camera.deinit()The official MicroPython build has no camera driver. You need a firmware built with one, and which resolutions work depends on that build.
When it does not work
Out of memory. The frame does not fit. Lower the resolution, use one frame buffer instead of two, or check that PSRAM is actually enabled in the board menu.
A frame buffer was never returned. Every esp_camera_fb_get needs a matching esp_camera_fb_return, on every path including the error ones.
Frames are queued. Use CAMERA_GRAB_LATEST, or grab twice and discard the first.
Nearly always the ribbon connector - not seated, or dirty. Reseat it. If it persists, the XCLK frequency may be wrong for that sensor.
A camera fills memory with pictures. The last page is about getting them onto a screen big enough to matter.
MIPI displays →Edit this page — content/esp32/camera-capture.mdx
Discuss this article
Ask about this page. The answer stays here, on the page it belongs to, for whoever hits the same wall next.