ESP32 S3 - Camera

This tutorial instructs you how to use the ESP32 S3 with a camera. The ESP32 S3 is powerful enough to take photos and send live video over WiFi, so you can watch the video in a web browser on your phone or your computer. This tutorial works with three camera sensors: the OV2640, the OV3660 and the OV5640. The same code works for all of them.

ESP32 S3 - Camera

What you'll build:

  1. An ESP32 S3 camera board connected to your WiFi network
  2. Code that detects your camera sensor automatically (OV2640, OV3660 or OV5640)
  3. A small web server that shows live video in any web browser
  4. A second, shorter program that takes a big, sharp JPEG photo

Hardware Preparation

1×ESP32 S3 N16R8 OV5640 Camera
1×Alternatively, ESP32 S3 N16R8 OV2640 Camera
1×USB Cable Type-A to Type-C (for USB-A PC)
1×USB Cable Type-C to Type-C (for USB-C PC)
1×Optionally, DC Power Jack
1×Optionally, ESP32 S3 44-Pin Screw Terminal Block
1×Optionally, ESP32 S3 44-Pin Breakout Board
1×Recommended: Screw Terminal Expansion Board for ESP32 S3
1×Recommended: Breakout Expansion Board for ESP32 S3
1×Recommended: Power Splitter for ESP32 S3

Or you can buy the following kits:

1×DIYables Sensor Kit (18 sensors/displays)
Disclosure: Some of the links provided in this section are Amazon affiliate links. We may receive a commission for any purchases made through these links at no additional cost to you.
Additionally, some of these links are for products from our own brand, DIYables .

※ NOTE THAT:

The ESP32 S3 camera board has 40 pins, but the screw terminal block and the breakout board have 44 pins. They still work together. Put the camera board in the center of the 44-pin board, so 2 pins stay free on the left side and 2 pins stay free on the right side.

The screw terminal block lets you connect wires with a screwdriver, with no soldering. The breakout board gives you easy pin headers for a breadboard.

Overview of ESP32 S3 Camera

The ESP32 S3 camera board is an ESP32 S3 N16R8 board with a camera connector on it. The camera itself is a small module that plugs into this connector with a flat cable. The board has 16 MB of flash memory and 8 MB of PSRAM. The PSRAM is very important, because a photo is too big to fit in the normal memory of the chip.

ESP32 S3 Camera Module

Key Specifications

ItemValue
MicrocontrollerESP32 S3 (dual-core, 240 MHz)
Flash memory16 MB
PSRAM8 MB (OPI)
WiFi2.4 GHz, 802.11 b/g/n
Camera interfaceDVP, 8-bit parallel
Image formatJPEG, RGB565, YUV422, Grayscale
Power5V from USB Type-C

Camera Sensor Comparison

Three camera sensors are available for this board. They all use the same connector, the same pins, and the same code. They are different only in image size and image quality.

SensorResolutionLargest ImageGood For
OV26402 MPUXGA 1600×1200Fast video, simple projects
OV36603 MPQXGA 2048×1536Better detail, still fast
OV56405 MPQSXGA 2592×1944Best photo quality

※ NOTE THAT:

A bigger image is not always better. A bigger image takes more time to send over WiFi, so the video becomes slower. For live video, a small image such as SVGA (800×600) is usually the best choice, even if you have the OV5640.

How ESP32 S3 Camera Works

The camera sensor takes a picture and sends it to the ESP32 S3. This happens in three steps:

  1. The ESP32 S3 sends a clock signal to the camera on the XCLK pin. This clock makes the camera work.
  2. The ESP32 S3 sends settings to the camera on the SIOD and SIOC pins. These two pins are a control bus (similar to I2C).
  3. The camera sends the image data back on 8 data pins (D0 to D7), together with the PCLK, HREF and VSYNC pins. These three pins tell the ESP32 S3 where each pixel, each line, and each new image starts.

The camera can compress the image into the JPEG format by itself. This is important, because a JPEG image is about 10 times smaller than a raw image. A smaller image means faster video over WiFi.

The ESP32 S3 stores each image in the PSRAM. One stored image is called a frame buffer. Your code asks for a frame buffer, uses it, and then gives it back to the camera.

Image Size Names

The camera library uses short names for image sizes. You will see these names in the code:

Name in codeSize in pixelsWorks with
FRAMESIZE_QVGA320 × 240All sensors
FRAMESIZE_VGA640 × 480All sensors
FRAMESIZE_SVGA800 × 600All sensors
FRAMESIZE_HD1280 × 720All sensors
FRAMESIZE_UXGA1600 × 1200All sensors
FRAMESIZE_QXGA2048 × 1536OV3660 and OV5640 only
FRAMESIZE_QSXGA2592 × 1944OV5640 only

Wiring Diagram between Camera and ESP32 S3

The camera connects to the ESP32 S3 board with a flat ribbon cable. You do not need a breadboard or any jumper wire.

  • How to connect the camera module to the ESP32 S3 board
The wiring diagram between ESP32 S3 Camera

This image is created using Fritzing. Click to enlarge image

  1. Turn off the power. Unplug the USB cable first.
  2. Find the black connector on the board. Pull the small black bar up. Pull it gently.
  3. Push the flat cable into the connector. The blue side of the cable faces up, and the metal contacts face down.
  4. Push the small black bar down again to lock the cable.
  5. Plug in the USB cable.

Safety Notes

Never plug or unplug the camera while the board has power. This can damage the camera sensor. Always unplug the USB cable first. Also, do not pull the flat cable hard, because it breaks easily.

The table below shows how the camera is connected inside the board. You do not need to wire these pins yourself, but you need them in your code:

Camera PinESP32 S3 PinMeaning
XCLKGPIO15Clock to the camera
SIODGPIO4Control data
SIOCGPIO5Control clock
VSYNCGPIO6Start of a new image
HREFGPIO7Start of a new line
PCLKGPIO13Pixel clock
D0GPIO11Data bit 0
D1GPIO9Data bit 1
D2GPIO8Data bit 2
D3GPIO10Data bit 3
D4GPIO12Data bit 4
D5GPIO18Data bit 5
D6GPIO17Data bit 6
D7GPIO16Data bit 7

※ NOTE THAT:

These pins are used by the camera. Do not use them for other parts in your project.

Arduino IDE Settings for ESP32 S3 Camera

The camera does not work with the default settings of the Arduino IDE. You must change some items in the Tools menu before you upload the code.

Tools Menu ItemSelect This
BoardESP32S3 Dev Module
Flash Size16MB (128Mb)
PSRAMOPI PSRAM
Partition SchemeHuge APP (3MB No OTA/1MB SPIFFS)
CPU Frequency240MHz (WiFi)
USB ModeHardware CDC and JTAG
Upload ModeUART0 / Hardware CDC
Upload Speed921600
USB CDC On BootEnabled

※ NOTE THAT:

Two settings are the most important:

  • PSRAM must be OPI PSRAM. If it is Disabled, the camera cannot start.
  • Partition Scheme must be Huge APP. The camera code is big, and the default setting is too small for it.

Change Flash Size first, because the list of Partition Scheme options changes after that.

Install DIYables ESP32 WebServer Library

This tutorial uses the DIYables ESP32 WebServer library. This library makes the web server part very short and easy to read.

  1. Open the Arduino IDE.
  2. Click the Libraries icon on the left bar.
  3. Type DIYables ESP32 WebServer into the search box.
  4. Find the library by DIYables.io and click INSTALL.
  • Search for DIYables ESP32 WebServer created by DIYables.io and click the Install button.
Newbiely | Arduino IDE 2.3.8
──
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Library Manager
Type:
All
Topic:
All
DIYables ESP32 WebServer by DIYables.io
DIYables ESP32 WebServer library designed for ESP32 boards. It includes multi-page web server capabilities and WebSocket support for real-time communication, perfect for IoT projects and DIYables ESP32 boards. More info
1.0.1
INSTALL
Newbiely.ino
···
1 void setup() {
Output
Serial Monitor
Ln 1, Col 1
ESP32S3 Dev Module on COM15
1

The camera library (esp_camera.h) is already inside the ESP32 board package. You do not need to install that one.

How To Program ESP32 S3 Camera

  • Include the two libraries at the top of your code.
#include <WiFi.h> #include <DIYables_ESP32_WebServer.h> #include "esp_camera.h"
  • Put your camera settings into a camera_config_t variable. The most important settings are the image format, the image size, and the image quality.
camera_config_t config; config.pixel_format = PIXFORMAT_JPEG; // JPEG is small and fast config.frame_size = FRAMESIZE_SVGA; // 800x600 config.jpeg_quality = 10; // 0 to 63, a lower number means a better image config.fb_count = 2; // use 2 frame buffers for smooth video
  • Start the camera with the esp_camera_init() function. It returns ESP_OK when the camera starts correctly.
esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x\n", err); }
  • Find out which camera sensor is on your board. Each sensor has its own ID number, called a PID.
sensor_t *s = esp_camera_sensor_get(); if (s->id.PID == OV2640_PID) Serial.println("This is an OV2640 camera"); else if (s->id.PID == OV3660_PID) Serial.println("This is an OV3660 camera"); else if (s->id.PID == OV5640_PID) Serial.println("This is an OV5640 camera");
  • Take one photo with the esp_camera_fb_get() function. This function gives you a frame buffer. The photo is inside fb->buf, and its size in bytes is inside fb->len.
camera_fb_t *fb = esp_camera_fb_get(); if (fb) { Serial.printf("Photo size: %u bytes\n", fb->len); esp_camera_fb_return(fb); // give the frame buffer back }

※ NOTE THAT:

You must call esp_camera_fb_return() after every esp_camera_fb_get(). If you forget it, the camera runs out of memory and stops after a few photos.

  • Change how the image looks. For example, turn the image upside down or make it brighter.
sensor_t *s = esp_camera_sensor_get(); s->set_vflip(s, 1); // turn the image upside down s->set_hmirror(s, 1); // mirror the image left to right s->set_brightness(s, 1); // -2 to 2 s->set_saturation(s, 0); // -2 to 2

※ NOTE THAT:

The OV3660 shows the image upside down by default. The code below fixes this with s->set_vflip(s, 1). If your own image is upside down or mirrored, change these two lines.

※ NOTE THAT:

The image size is different. Choose it one time in config.frame_size, before esp_camera_init(). Do not change it later while the code runs, or the camera will show the error cam_hal: FB-OVF and stop giving you images.

Now the web server part. The DIYables ESP32 WebServer library needs only three things: create the server, add your addresses, and call handleClient() again and again.

  • Create the web server on port 80.
DIYables_ESP32_WebServer server(80);
  • Write one function for each address. Every function must have exactly these five parameters.
void handleHome(WiFiClient &client, const String &method, const String &request, const QueryParams &params, const String &jsonData) { server.sendResponse(client, INDEX_HTML, "text/html"); }
  • Add your addresses inside setup() and start the server.
server.addRoute("/", handleHome); server.addRoute("/stream", handleStream); server.begin();

※ NOTE THAT:

Use server.begin() and connect to the WiFi yourself. Do not use server.begin(ssid, password) in this project, because that version sets the Serial Monitor speed to 9600, and then you cannot read the messages at 115200.

  • To send live video, send many photos one after another, and put a separator text between them. This is called an MJPEG stream. Every web browser understands it. An image is not text, so you must use client.write() and not client.print().
while (client.connected()) { camera_fb_t *fb = esp_camera_fb_get(); size_t len = fb->len; client.print("--" BOUNDARY "\r\n"); client.print("Content-Type: image/jpeg\r\n"); client.printf("Content-Length: %u\r\n\r\n", len); client.write(fb->buf, len); client.print("\r\n"); esp_camera_fb_return(fb); }
  • Call handleClient() inside loop(). Without this line, the web server does nothing.
void loop() { server.handleClient(); }

※ NOTE THAT:

While one person watches the video, the ESP32 S3 is busy and cannot answer a second person. This is normal for a small board. Open the video on one device at a time.

ESP32 S3 Code - Example 1: Live Video

This is the simple example. It starts the camera, connects to your WiFi network, and shows the live video in a web browser. Start with this one.

The web server has two addresses:

  • http://<IP>/ shows a web page with the live video
  • http://<IP>/stream is the live video only

The code reads the sensor ID and sets the correct settings for the OV2640, the OV3660 or the OV5640 automatically. You do not need to change the code for your camera.

/* * This ESP32 S3 code was developed by newbiely.com * * This ESP32 S3 code is made available for public use without any restriction * * For comprehensive instructions and wiring diagrams, please visit: * https://newbiely.com/tutorials/esp32-s3/esp32-s3-camera */ #include <WiFi.h> #include <DIYables_ESP32_WebServer.h> // DIYables ESP32 WebServer library #include "esp_camera.h" const char *WIFI_SSID = "YOUR_WIFI_SSID"; // CHANGE IT const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; // CHANGE IT // Camera pins of the ESP32-S3-N16R8 camera board. // The same pins work for the OV2640, OV3660 and OV5640 versions. #define PWDN_GPIO_NUM -1 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 15 #define SIOD_GPIO_NUM 4 #define SIOC_GPIO_NUM 5 #define Y9_GPIO_NUM 16 #define Y8_GPIO_NUM 17 #define Y7_GPIO_NUM 18 #define Y6_GPIO_NUM 12 #define Y5_GPIO_NUM 10 #define Y4_GPIO_NUM 8 #define Y3_GPIO_NUM 9 #define Y2_GPIO_NUM 11 #define VSYNC_GPIO_NUM 6 #define HREF_GPIO_NUM 7 #define PCLK_GPIO_NUM 13 DIYables_ESP32_WebServer server(80); // The text that separates one image from the next image in the video #define BOUNDARY "diyablesframe" const char INDEX_HTML[] = R"rawliteral( <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ESP32 S3 Camera</title> <style> body { background:#111; color:#eee; font-family:sans-serif; text-align:center; margin:0; padding:16px; } img { width:100%; max-width:800px; border-radius:8px; } </style> </head> <body> <h2>ESP32 S3 Camera</h2> <img src="/stream"> </body> </html> )rawliteral"; // Returns the name of the camera sensor that is installed on the board const char *getSensorName(int pid) { switch (pid) { case OV2640_PID: return "OV2640 (2 MP, max UXGA 1600x1200)"; case OV3660_PID: return "OV3660 (3 MP, max QXGA 2048x1536)"; case OV5640_PID: return "OV5640 (5 MP, max QSXGA 2592x1944)"; default: return "Unknown sensor"; } } bool initCamera() { 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_sccb_sda = SIOD_GPIO_NUM; config.pin_sccb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; config.frame_size = FRAMESIZE_SVGA; // 800x600 config.jpeg_quality = 12; // 0 to 63, a lower number means a better image config.fb_count = 1; config.fb_location = CAMERA_FB_IN_DRAM; config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; if (psramFound()) { config.fb_location = CAMERA_FB_IN_PSRAM; config.jpeg_quality = 10; config.fb_count = 2; config.grab_mode = CAMERA_GRAB_LATEST; } else { config.frame_size = FRAMESIZE_QVGA; Serial.println("WARNING: PSRAM not found. Please set PSRAM to OPI PSRAM in the Tools menu."); } esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x\n", err); return false; } sensor_t *s = esp_camera_sensor_get(); Serial.printf("Camera sensor: %s\n", getSensorName(s->id.PID)); // Each sensor needs slightly different settings switch (s->id.PID) { case OV2640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; case OV3660_PID: s->set_vflip(s, 1); // the OV3660 image is upside down by default s->set_brightness(s, 1); // make the image a little brighter s->set_saturation(s, -2); // make the colors a little softer break; case OV5640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; default: break; } return true; } // Shows the web page with the live video void handleHome(WiFiClient &client, const String &method, const String &request, const QueryParams &params, const String &jsonData) { server.sendResponse(client, INDEX_HTML, "text/html"); } // Sends the live video to the web browser void handleStream(WiFiClient &client, const String &method, const String &request, const QueryParams &params, const String &jsonData) { Serial.println("Video stream started"); client.print("HTTP/1.1 200 OK\r\n"); client.print("Content-Type: multipart/x-mixed-replace; boundary=" BOUNDARY "\r\n"); client.print("Access-Control-Allow-Origin: *\r\n"); client.print("Connection: close\r\n\r\n"); while (client.connected()) { camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { Serial.println("Frame capture failed"); break; } size_t len = fb->len; client.print("--" BOUNDARY "\r\n"); client.print("Content-Type: image/jpeg\r\n"); client.printf("Content-Length: %u\r\n\r\n", len); size_t sent = client.write(fb->buf, len); client.print("\r\n"); esp_camera_fb_return(fb); if (sent != len) { // the web browser closed the video break; } } Serial.println("Video stream stopped"); } void setup() { Serial.begin(115200); delay(1000); if (!initCamera()) { Serial.println("Stopped"); while (true) delay(1000); } WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); WiFi.setSleep(false); // keep WiFi fast for the video server.addRoute("/", handleHome); server.addRoute("/stream", handleStream); server.begin(); Serial.print("Camera is ready. Open this address in your web browser: http://"); Serial.println(WiFi.localIP()); } void loop() { server.handleClient(); }

Detailed Instructions

  1. New to ESP32 S3? Complete our Getting Started with ESP32 S3 guide first.
  2. Connect the camera module to the board. Remember: unplug the USB cable first.
  3. Install the DIYables ESP32 WebServer library. See the steps above.
  4. Copy the above code and paste it into the Arduino IDE.
  5. Change YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD to the name and the password of your WiFi network.
  6. Change the settings in the Tools menu. See the table above.
  7. Compile and upload the code to the ESP32 S3 board by clicking the Upload button in Arduino IDE.
Arduino IDE Upload Code
  1. Open the Serial Monitor in Arduino IDE.
How to open serial monitor on Arduino IDE
  1. Press the RESET button on the board one time.
  2. Read the IP address in the Serial Monitor.
  3. Type that address into your web browser and watch the live video.
  4. Pro Tip: Your computer or phone must be on the same WiFi network as the ESP32 S3. If they are on different networks, the page will not open.

Serial Monitor

After uploading, the Serial Monitor shows the name of your camera sensor and the IP address of the camera. The following readings were captured in 2026:

Newbiely | Arduino IDE 2.3.8
──
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'ESP32S3 Dev Module' on 'COM15')
New Line
9600 baud
[2026-09-23 10:00:01] Camera sensor: OV5640 (5 MP, max QSXGA 2592x1944) [2026-09-23 10:00:01] Connecting to WiFi... [2026-09-23 10:00:04] Starting web server on IP: 192.168.0.2 [2026-09-23 10:00:04] Camera is ready. Open this address in your web browser: http://192.168.0.2 [2026-09-23 10:00:15] Method: GET [2026-09-23 10:00:15] Requested path: / [2026-09-23 10:00:15] Client disconnected [2026-09-23 10:00:15] Method: GET [2026-09-23 10:00:15] Requested path: /stream [2026-09-23 10:00:15] Video stream started
Ln 11, Col 1
ESP32S3 Dev Module on COM15
2

※ NOTE THAT:

The lines Method: GET, Requested path: / and Client disconnected come from the DIYables ESP32 WebServer library. They tell you that the web browser reached your board. This is normal.

The line with the sensor name is different for each camera:

Your CameraLine in the Serial Monitor
OV2640Camera sensor OV2640 (2 MP, max UXGA 1600x1200)
OV3660Camera sensor OV3660 (3 MP, max QXGA 2048x1536)
OV5640Camera sensor OV5640 (5 MP, max QSXGA 2592x1944)

Now open the IP address in your web browser. You will see the live video from the camera:

ESP32 S3 Camera Live Video in Web Browser

ESP32 S3 Code - Example 2: Take a Photo

This example does not use video. It only takes a photo. The code is shorter and easier to read, so it is a good place to start if the video looks too difficult.

The web server has two addresses:

  • http://<IP>/ shows the web page with the photo
  • http://<IP>/photo is the photo only

Every time the page opens, the ESP32 S3 takes a new photo. To take another photo, click the link Take a new photo.

/* * This ESP32 S3 code was developed by newbiely.com * * This ESP32 S3 code is made available for public use without any restriction * * For comprehensive instructions and wiring diagrams, please visit: * https://newbiely.com/tutorials/esp32-s3/esp32-s3-camera */ #include <WiFi.h> #include <DIYables_ESP32_WebServer.h> // DIYables ESP32 WebServer library #include "esp_camera.h" const char *WIFI_SSID = "YOUR_WIFI_SSID"; // CHANGE IT const char *WIFI_PASSWORD = "YOUR_WIFI_PASSWORD"; // CHANGE IT // Camera pins of the ESP32-S3-N16R8 camera board. // The same pins work for the OV2640, OV3660 and OV5640 versions. #define PWDN_GPIO_NUM -1 #define RESET_GPIO_NUM -1 #define XCLK_GPIO_NUM 15 #define SIOD_GPIO_NUM 4 #define SIOC_GPIO_NUM 5 #define Y9_GPIO_NUM 16 #define Y8_GPIO_NUM 17 #define Y7_GPIO_NUM 18 #define Y6_GPIO_NUM 12 #define Y5_GPIO_NUM 10 #define Y4_GPIO_NUM 8 #define Y3_GPIO_NUM 9 #define Y2_GPIO_NUM 11 #define VSYNC_GPIO_NUM 6 #define HREF_GPIO_NUM 7 #define PCLK_GPIO_NUM 13 DIYables_ESP32_WebServer server(80); const char INDEX_HTML[] = R"rawliteral( <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>ESP32 S3 Camera</title> <style> body { background:#111; color:#eee; font-family:sans-serif; text-align:center; margin:0; padding:16px; } img { width:100%; max-width:800px; border-radius:8px; } a { color:#4da3ff; font-size:18px; } </style> </head> <body> <h2>ESP32 S3 Camera</h2> <img src="/photo"> <p><a href="/">Take a new photo</a></p> </body> </html> )rawliteral"; // Returns the name of the camera sensor that is installed on the board const char *getSensorName(int pid) { switch (pid) { case OV2640_PID: return "OV2640 (2 MP, max UXGA 1600x1200)"; case OV3660_PID: return "OV3660 (3 MP, max QXGA 2048x1536)"; case OV5640_PID: return "OV5640 (5 MP, max QSXGA 2592x1944)"; default: return "Unknown sensor"; } } bool initCamera() { 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_sccb_sda = SIOD_GPIO_NUM; config.pin_sccb_scl = SIOC_GPIO_NUM; config.pin_pwdn = PWDN_GPIO_NUM; config.pin_reset = RESET_GPIO_NUM; config.xclk_freq_hz = 20000000; config.pixel_format = PIXFORMAT_JPEG; config.frame_size = FRAMESIZE_UXGA; // 1600x1200 - a big, sharp photo config.jpeg_quality = 12; // 0 to 63, a lower number means a better image config.fb_count = 1; config.fb_location = CAMERA_FB_IN_DRAM; config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; if (psramFound()) { config.fb_location = CAMERA_FB_IN_PSRAM; config.jpeg_quality = 10; config.fb_count = 2; config.grab_mode = CAMERA_GRAB_LATEST; } else { config.frame_size = FRAMESIZE_QVGA; Serial.println("WARNING: PSRAM not found. Please set PSRAM to OPI PSRAM in the Tools menu."); } esp_err_t err = esp_camera_init(&config); if (err != ESP_OK) { Serial.printf("Camera init failed with error 0x%x\n", err); return false; } sensor_t *s = esp_camera_sensor_get(); Serial.printf("Camera sensor: %s\n", getSensorName(s->id.PID)); // Each sensor needs slightly different settings switch (s->id.PID) { case OV2640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; case OV3660_PID: s->set_vflip(s, 1); // the OV3660 image is upside down by default s->set_brightness(s, 1); // make the image a little brighter s->set_saturation(s, -2); // make the colors a little softer break; case OV5640_PID: s->set_vflip(s, 0); s->set_hmirror(s, 0); break; default: break; } return true; } // Shows the web page void handleHome(WiFiClient &client, const String &method, const String &request, const QueryParams &params, const String &jsonData) { server.sendResponse(client, INDEX_HTML, "text/html"); } // Takes one photo and sends it to the web browser void handlePhoto(WiFiClient &client, const String &method, const String &request, const QueryParams &params, const String &jsonData) { camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { Serial.println("Photo capture failed"); server.sendResponse(client, "Photo capture failed", "text/plain"); return; } Serial.printf("Photo taken: %u bytes\n", fb->len); client.print("HTTP/1.1 200 OK\r\n"); client.print("Content-Type: image/jpeg\r\n"); client.print("Content-Disposition: inline; filename=photo.jpg\r\n"); client.printf("Content-Length: %u\r\n", fb->len); client.print("Connection: close\r\n\r\n"); client.write(fb->buf, fb->len); esp_camera_fb_return(fb); // give the memory back to the camera } void setup() { Serial.begin(115200); delay(1000); if (!initCamera()) { Serial.println("Stopped"); while (true) delay(1000); } WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("Connecting to WiFi"); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(); server.addRoute("/", handleHome); server.addRoute("/photo", handlePhoto); server.begin(); Serial.print("Camera is ready. Open this address in your web browser: http://"); Serial.println(WiFi.localIP()); } void loop() { server.handleClient(); }

How It Works

The web page has only two important lines:

<img src="/photo"> <p><a href="/">Take a new photo</a></p>

The web browser reads <img src="/photo"> and asks the ESP32 S3 for the address /photo. The handlePhoto() function takes one photo and sends it back as a JPEG image. The link / opens the page again, so the camera takes a new photo. You do not need any JavaScript.

※ NOTE THAT:

This example uses a big image size (FRAMESIZE_UXGA, 1600×1200), because a photo does not need to be fast. Example 1 uses a small size (FRAMESIZE_SVGA, 800×600), because a small image travels faster over WiFi, and the video needs many images every second.

※ NOTE THAT:

Do you want an even bigger photo? Change config.frame_size to FRAMESIZE_QXGA (OV3660 and OV5640 only) or to FRAMESIZE_QSXGA (OV5640 only). A bigger photo needs more PSRAM and more time.

Detailed Instructions

  1. Copy the above code and paste it into the Arduino IDE.
  2. Change YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD again.
  3. Upload the code and press the RESET button one time.
  4. Open the IP address in your web browser. The photo appears.
  5. Click Take a new photo to take another one.
  6. To keep a photo, right-click on the image and choose Save image as...
ESP32 S3 Camera Photo in Web Browser

The Serial Monitor shows the size of each photo:

Newbiely | Arduino IDE 2.3.8
──
File
Edit
Sketch
Tools
Help
ESP32S3 Dev Module
Newbiely.ino
···
8 Serial.println("Hello World!");
Output
Serial Monitor
Message (Enter to send message to 'ESP32S3 Dev Module' on 'COM15')
New Line
9600 baud
[2026-09-23 10:05:02] Camera sensor: OV5640 (5 MP, max QSXGA 2592x1944) [2026-09-23 10:05:02] Connecting to WiFi... [2026-09-23 10:05:05] Starting web server on IP: 192.168.0.2 [2026-09-23 10:05:05] Camera is ready. Open this address in your web browser: http://192.168.0.2 [2026-09-23 10:05:20] Method: GET [2026-09-23 10:05:20] Requested path: / [2026-09-23 10:05:20] Client disconnected [2026-09-23 10:05:20] Method: GET [2026-09-23 10:05:20] Requested path: /photo [2026-09-23 10:05:21] Photo taken: 184320 bytes
Ln 11, Col 1
ESP32S3 Dev Module on COM15
2

Troubleshooting

Here are the problems that beginners meet most often, and how to solve them.

The Serial Monitor shows nothing after the boot messages.

The USB CDC On Boot setting does not match your USB port. If your USB cable is in the port named UART, set USB CDC On Boot to Disabled. If your cable is in the port named USB, set it to Enabled. Then upload the code again. Also check that the speed box in the Serial Monitor shows 115200.

The Serial Monitor shows Camera init failed with error 0x105.

The camera cannot start. There are three common reasons:

  • PSRAM is Disabled. Set it to OPI PSRAM in the Tools menu.
  • The flat cable is loose or the wrong way round. Unplug the USB cable, open the connector, and put the cable in again.
  • The clock is too fast for your camera module. Change config.xclk_freq_hz from 20000000 to 10000000 in the code.

The Serial Monitor shows WARNING: PSRAM not found.

Set PSRAM to OPI PSRAM in the Tools menu and upload the code again.

The upload fails, or the COM port does not appear.

Press and hold the BOOT button, press the RESET button one time, then release the BOOT button. Now the port appears in the Tools menu. After the upload finishes, press RESET one time.

The Serial Monitor shows the IP address, but the web page does not open.

Your computer and your ESP32 S3 are probably on different networks. Both must be on the same WiFi network. Also, type http:// and not https://, because the ESP32 S3 does not use https.

The video is slow, or it stops after a few seconds.

The image is too big for your WiFi. Change config.frame_size to a smaller size, such as FRAMESIZE_VGA or FRAMESIZE_QVGA. You can also increase config.jpeg_quality to 15, which makes a smaller file.

The Serial Monitor shows cam_hal: FB-OVF and Photo capture failed.

FB-OVF means "frame buffer overflow": the image is bigger than the memory that is ready for it. Choose your image size one time, inside config.frame_size, before you call esp_camera_init(). Do not change the size later with s->set_framesize(), because that function changes the camera sensor without making the memory bigger. If you really need to change the size while the code runs, use the esp_camera_reconfigure() function instead.

The photo takes a long time, or the page waits.

A big photo needs time. FRAMESIZE_UXGA takes about one second. Use FRAMESIZE_XGA or FRAMESIZE_SVGA if you want a faster photo.

The photo fails on every try.

Your image size is too big for the free memory. Use a smaller size, such as FRAMESIZE_XGA, or set config.fb_count to 1.

The image is upside down or mirrored.

Change the two lines s->set_vflip(s, 0) and s->set_hmirror(s, 0) in the code. Use 1 instead of 0.

Applications

An ESP32 S3 camera is small, cheap, and works over WiFi, so one board replaces an expensive IP camera. Here are practical projects you can build with this code:

  1. WiFi home security camera: Watch your door or your window from your phone, with no monthly payment.
  2. Pet camera for dogs and cats: See what your pet does when you are not at home.
  3. 3D printer monitor: Watch a long print job from another room, and stop it early when it goes wrong.
  4. Video doorbell: Take a photo when somebody presses the button at your door.
  5. Baby and child room camera: Look into the room from your phone without opening the door.
  6. Plant growth recorder: Take one photo every hour and join them into a time-lapse video.
  7. Robot car camera: Give eyes to a small robot, and drive it from another room.
  8. Drone and RC camera: Send live video from a model car, a boat, or a small drone.
  9. Aquarium and greenhouse watcher: Keep an eye on your fish or your plants while you travel.
  10. Machine and tutorial monitor: Watch a machine, a 3D printer farm, or a laser cutter while it runs.
  11. Shop and reception camera: Show the door of your shop on a screen at the back of the room.
  12. Learning project for students: A complete example of a camera, a web server and WiFi in one small board.

Video Tutorial

Watch the step-by-step video walkthrough for this ESP32 S3 project below.

Challenges

Now that you have a working camera on the ESP32 S3, try extending the project with these exercises to deepen your understanding.

  1. Beginner: Change the image size in the code from FRAMESIZE_SVGA to FRAMESIZE_VGA, then to FRAMESIZE_UXGA. Watch how the speed of the video changes.
  2. Intermediate: Add a button to the board. When somebody presses the button, take one photo and save it to the SD card.
  3. Advanced: Take one photo every 10 minutes and send it to your phone with a Telegram bot, so you get a picture even when you are far from home.

※ OUR MESSAGES

  • As freelancers, We are AVAILABLE for HIRE. See how to outsource your project to us
  • Please feel free to share the link of this tutorial. However, Please do not use our content on any other websites. We invested a lot of effort and time to create the content, please respect our work!