Examples / Camera stream

Camera stream

Live video from an ESP32 camera lands on a p5.js canvas, pixels and all — ready for creative coding.

AdvancedVisionp5.js
Camera stream — the example running

Camera stream

Streams live MJPEG video from an ESP32 camera module into a p5.js canvas over WiFi.

Video is served over a separate HTTP connection — it does not flow through the WebSocket control channel, so camera streaming and Arduino control can run simultaneously without interfering with each other.


Arduino
camera-stream.ino — the Arduino side
#define CAMERA_MODEL_WROVER_KIT
// #define CAMERA_MODEL_ESP_EYE
// #define CAMERA_MODEL_M5STACK_PSRAM
// #define CAMERA_MODEL_M5STACK_V2_PSRAM
// #define CAMERA_MODEL_M5STACK_WIDE
// #define CAMERA_MODEL_M5STACK_ESP32CAM
// #define CAMERA_MODEL_M5STACK_UNITCAM
// #define CAMERA_MODEL_M5STACK_CAMS3_UNIT
// #define CAMERA_MODEL_AI_THINKER
// #define CAMERA_MODEL_TTGO_T_JOURNAL
// #define CAMERA_MODEL_XIAO_ESP32S3
// #define CAMERA_MODEL_ESP32_CAM_BOARD
// #define CAMERA_MODEL_ESP32S3_CAM_LCD
// #define CAMERA_MODEL_ESP32S2_CAM_BOARD
// #define CAMERA_MODEL_ESP32S3_EYE
// #define CAMERA_MODEL_DFRobot_FireBeetle2_ESP32S3
// #define CAMERA_MODEL_DFRobot_Romeo_ESP32S3

#include <Pardalote.h>
#include <PardaloteCamera.h>

void setup() {
    Pardalote.begin();
}

void loop() {
    Pardalote.run();
}
JS
sketch.js — the browser side
const CAMERA_PORT = 82;

// Camera frame size — bigger is sharper but slower and uses more bandwidth.
// Options: FRAMESIZE_QVGA 320×240 · FRAMESIZE_HVGA 480×320 · FRAMESIZE_VGA 640×480
//          · FRAMESIZE_SVGA 800×600 · FRAMESIZE_HD 1280×720 (needs PSRAM)
const FRAME_SIZE = FRAMESIZE_VGA;   // 640×480 — matches the canvas

let arduino, camEl = null;   // <img> pointed at the MJPEG stream

function setup() {
    createCanvas(640, 480);

    arduino = new Arduino();
    arduino.add('cam', new Camera());
    setupConnection(arduino, { store: 'pardalote-camera-stream' });

    // Start the camera once the board is ready (device state resets on reconnect).
    arduino.on('ready', () => {
        arduino.cam.setResolution(FRAME_SIZE);
        arduino.cam.attach(CAMERA_PORT);
    });

    // The stream URL arrives here — point a hidden <img> at it for image().
    arduino.cam.on('stream', ({ url }) => {
        if (camEl) camEl.remove();
        camEl = createImg(url, '');
        camEl.hide();
    });
    arduino.on('disconnect', () => { if (camEl) { camEl.remove(); camEl = null; } });
}

function draw() {
    background(255);

    if (camEl) {
        try {
            // p5 remembers the image's size from the stream's FIRST frame; keep
            // it current, or a resolution change leaves a band at the bottom.
            camEl.width  = camEl.elt.naturalWidth;
            camEl.height = camEl.elt.naturalHeight;
            image(camEl, 0, 0, width, height);
        } catch (e) {
            // img entered broken state (stream dropped) — clear and show placeholder
            camEl.remove();
            camEl = null;
        }
    } else {
        // Waiting for stream — show a placeholder message (house cream card)
        fill('#F2E9D8'); noStroke();
        rect(0, 0, width, height);
        fill('#6d6a5f');
        textAlign(CENTER, CENTER); textSize(16);
        text(arduino.connected ? 'Starting camera…' : 'Connecting…', width / 2, height / 2);
        textAlign(LEFT, BASELINE);
    }

    // Connection status dot — top-right corner (teal = connected)
    noStroke();
    fill(arduino.connected ? '#3FA9A0' : '#D3542B');
    circle(width - 16, 16, 12);
}

What you need


Files

File Purpose
index.html Loads p5.js, Pardalote, and the sketch
sketch.js p5.js sketch — connects, streams, draws
style.css Minimal page reset

Arduino setup

1. Open the library example

In Arduino IDE: File → Examples → Pardalote → camera-stream. The sketch looks like:

Arduino
// Uncomment ONE:
#define CAMERA_MODEL_WROVER_KIT               // Freenove ESP32-WROVER-DEV
// #define CAMERA_MODEL_AI_THINKER            // AI-Thinker ESP32-CAM
// #define CAMERA_MODEL_XIAO_ESP32S3          // Seeed XIAO ESP32S3 Sense
// #define CAMERA_MODEL_ESP32S3_EYE           // Espressif ESP32-S3-EYE
// #define CAMERA_MODEL_M5STACK_PSRAM         // M5Stack PSRAM
// #define CAMERA_MODEL_M5STACK_WIDE          // M5Stack Wide
// ... (see PardaloteCamera.h for all supported models)

#include <Pardalote.h>
#include <PardaloteCamera.h>

void setup() { Pardalote.begin(); }
void loop()  { Pardalote.run();   }

Pick the CAMERA_MODEL_* define that matches your hardware before the include.

2. Upload

Flash the sketch to your board. Once connected to WiFi, the IP address is printed in the Serial Monitor (115200 baud).


Browser setup

1. Connect on the page

Open index.html, then in the Board row enter the Arduino's IP (or switch to USB) and press Connect — no code editing needed, and the setting is remembered per browser. The camera's stream port is fixed by the firmware (82), so there's nothing else to set.

2. Open the example

Serve the folder from a local web server and open index.html in a browser. You can use any static server — for example:

npx serve .

If you are using a code editor you can run the web server there.

Why a server? Browsers block WebSocket connections from file:// pages on some platforms. A local server avoids this.


How it works

Arduino                          Browser
────────────────────────────────────────────────────────
WebSocket :81  ←──── control ────→  pardalote.js (Camera extension)
HTTP      :82  ──── MJPEG stream ──→  <img src="http://ip:82/stream">
  1. arduino.connect() opens a WebSocket on port 81.
  2. On ready, arduino.cam.attach(CAMERA_PORT) sends a CMD_CAMERA_INIT frame to the Arduino over the WebSocket. The Arduino initialises the camera hardware and starts an HTTP server on the requested port.
  3. The Arduino echoes the confirmed port back. The Camera extension sets an <img> element's src to http://<ip>:<port>/stream and emits the stream event.
  4. The browser fetches the MJPEG stream directly over HTTP — the WebSocket is only used for the initial handshake and any subsequent control commands (resolution, quality).
  5. In draw(), image(camEl, 0, 0, width, height) renders the current frame to the p5.js canvas. Just before it, the sketch copies the frame's real size into camEl.width / camEl.height: p5 records an image's size only when its first frame loads, so without this a resolution change leaves a black band at the bottom.

Adjusting the stream

JS
// Resolution — call before or after attach()
arduino.cam.setResolution(FRAMESIZE_QVGA);   // 320×240  ← default
arduino.cam.setResolution(FRAMESIZE_VGA);    // 640×480
arduino.cam.setResolution(FRAMESIZE_HD);     // 1280×720

// JPEG quality: 0 = best image / highest bandwidth
//              63 = worst image / lowest bandwidth
arduino.cam.setQuality(12);  // default
Constant Resolution
FRAMESIZE_QQVGA 160×120
FRAMESIZE_QVGA 320×240
FRAMESIZE_HVGA 480×320
FRAMESIZE_VGA 640×480
FRAMESIZE_SVGA 800×600
FRAMESIZE_HD 1280×720

Higher resolutions require more bandwidth and may reduce frame rate. QVGA is a good starting point for most WiFi environments. Anything above QQVGA also needs PSRAM enabled, and the largest sizes don't work on every sensor — see Troubleshooting below.


Snapshots

Capture a single JPEG still without interrupting the stream:

JS
const url = await arduino.cam.snapshot();
// url is a blob: URL — use as an <img> src or save it
// revoke when done to free memory:
URL.revokeObjectURL(url);

The snapshot is served from http://<ip>:<port+1>/snapshot — its own small web server on the next port (83 for this example), so it answers even while the stream is running.


Pixel manipulation

Because the camera HTTP server sends Access-Control-Allow-Origin: *, p5.js can read pixel data from the stream:

JS
arduino.cam.on('stream', ({ url }) => {
    camEl = createImg(url, '');
    camEl.hide();
});

function draw() {
    if (camEl) {
        camEl.width  = camEl.elt.naturalWidth;   // keep p5's idea of the size current
        camEl.height = camEl.elt.naturalHeight;
        image(camEl, 0, 0, width, height);
        loadPixels();
        // pixels[] is now available — RGBA values for every canvas pixel
        for (let i = 0; i < pixels.length; i += 4) {
            pixels[i] = 255 - pixels[i];     // invert red
            pixels[i+1] = 255 - pixels[i+1]; // invert green
            pixels[i+2] = 255 - pixels[i+2]; // invert blue
        }
        updatePixels();
    }
}

Note: loadPixels() on every frame is CPU-intensive. For heavy processing, consider drawing to an off-screen createGraphics() buffer and only calling loadPixels() on that.


Troubleshooting

Stream doesn't appear

"Camera Init failed: 0x106 (ESP_ERR_NOT_SUPPORTED)"

Low frame rate

Image is stuck at 160×120 (QQVGA) and setResolution() won't change it

FRAMESIZE_HD stutters or drops the stream (cam_hal: FB-OVF / net::ERR_INCOMPLETE_CHUNKED_ENCODING)

XIAO ESP32S3 won't connect to WiFi

Canvas size doesn't match stream

Black band at the bottom after changing resolution

Only one browser can receive the stream at a time