Skip to main content

UVC Camera Stream Example

This example demonstrates how to capture video frames from a USB camera using the USB Video Class (UVC) protocol. The example shows basic camera initialization, frame callbacks, and stream control.

What This Example Demonstrates

  • Initializing USB camera streaming
  • Allocating frame buffers efficiently
  • Configuring camera resolution and frame rate
  • Receiving frames via callback function
  • Controlling camera stream (suspend/resume)

Hardware Setup

Required Components:
  • ESP32-S2 or ESP32-S3 development board
  • USB camera with UVC support (most webcams work)
  • USB OTG cable or adapter
Connections:
  • Connect the USB camera to the ESP32’s USB port
  • Ensure proper power supply (cameras can draw significant current)
  • Connect ESP32 to computer via serial for monitoring

Complete Code

#include <Arduino.h>
#include "USB_STREAM.h"

/* Define the camera frame callback function implementation */
static void onCameraFrameCallback(uvc_frame *frame, void *user_ptr)
{
    Serial.printf("uvc callback! frame_format = %d, seq = %" PRIu32 ", width = %" PRIu32", height = %" PRIu32 ", length = %u, ptr = %d\n",
             frame->frame_format, frame->sequence, frame->width, frame->height, frame->data_bytes, (int)user_ptr);
}

void setup()
{
    Serial.begin(115200);
    // Instantiate an object
    USB_STREAM *usb = new USB_STREAM();

    // allocate memory
    uint8_t *_xferBufferA = (uint8_t *)malloc(55 * 1024);
    assert(_xferBufferA != NULL);
    uint8_t *_xferBufferB = (uint8_t *)malloc(55 * 1024);
    assert(_xferBufferB != NULL);
    uint8_t *_frameBuffer = (uint8_t *)malloc(55 * 1024);
    assert(_frameBuffer != NULL);

    // Config the parameter
    usb->uvcConfiguration(FRAME_RESOLUTION_ANY, FRAME_RESOLUTION_ANY, FRAME_INTERVAL_FPS_15, 55 * 1024, _xferBufferA, _xferBufferB, 55 * 1024, _frameBuffer);

    //Register the camera frame callback function
    usb->uvcCamRegisterCb(&onCameraFrameCallback, NULL);

    usb->start();

    usb->connectWait(1000);
    delay(5000);

    usb->uvcCamSuspend(NULL);
    delay(5000);

    usb->uvcCamResume(NULL);

    /*Dont forget to free the allocated memory*/
    // free(_xferBufferA);
    // free(_xferBufferB);
    // free(_frameBuffer);
}

void loop()
{
    vTaskDelay(100);
}

Code Explanation

1. Frame Callback Function

static void onCameraFrameCallback(uvc_frame *frame, void *user_ptr)
{
    Serial.printf("uvc callback! frame_format = %d, seq = %" PRIu32 ", width = %" PRIu32", height = %" PRIu32 ", length = %u, ptr = %d\n",
             frame->frame_format, frame->sequence, frame->width, frame->height, frame->data_bytes, (int)user_ptr);
}
Line-by-line breakdown:
  • static void onCameraFrameCallback(uvc_frame *frame, void *user_ptr) - Callback function triggered when a new frame arrives
  • frame->frame_format - Format of the frame (MJPEG, YUYV, etc.)
  • frame->sequence - Frame sequence number (useful for detecting dropped frames)
  • frame->width and frame->height - Actual frame dimensions
  • frame->data_bytes - Size of frame data in bytes
  • user_ptr - Custom user data passed during registration

2. Buffer Allocation

uint8_t *_xferBufferA = (uint8_t *)malloc(55 * 1024);
assert(_xferBufferA != NULL);
uint8_t *_xferBufferB = (uint8_t *)malloc(55 * 1024);
assert(_xferBufferB != NULL);
uint8_t *_frameBuffer = (uint8_t *)malloc(55 * 1024);
assert(_frameBuffer != NULL);
Why three buffers?
  • _xferBufferA and _xferBufferB - Double buffering for USB transfers (prevents frame drops)
  • _frameBuffer - Assembled complete frame buffer
  • Size: 55KB - Supports MJPEG frames at moderate resolutions
  • assert() ensures allocation succeeded (critical for stability)

3. Camera Configuration

usb->uvcConfiguration(FRAME_RESOLUTION_ANY, FRAME_RESOLUTION_ANY, FRAME_INTERVAL_FPS_15, 55 * 1024, _xferBufferA, _xferBufferB, 55 * 1024, _frameBuffer);
Parameters explained:
  1. FRAME_RESOLUTION_ANY (width) - Accept any resolution width from camera
  2. FRAME_RESOLUTION_ANY (height) - Accept any resolution height from camera
  3. FRAME_INTERVAL_FPS_15 - Request 15 frames per second
  4. 55 * 1024 - Transfer buffer size (55KB)
  5. _xferBufferA, _xferBufferB - Double buffer pointers
  6. 55 * 1024 - Frame buffer size (55KB)
  7. _frameBuffer - Complete frame buffer pointer

4. Stream Control

usb->start();                    // Start USB host
usb->connectWait(1000);          // Wait up to 1000ms for camera connection
delay(5000);                     // Stream for 5 seconds
usb->uvcCamSuspend(NULL);        // Pause streaming
delay(5000);                     // Wait 5 seconds
usb->uvcCamResume(NULL);         // Resume streaming
This demonstrates dynamic stream control without reinitializing the camera.

Expected Serial Output

uvc callback! frame_format = 6, seq = 0, width = 640, height = 480, length = 12543, ptr = 0
uvc callback! frame_format = 6, seq = 1, width = 640, height = 480, length = 12891, ptr = 0
uvc callback! frame_format = 6, seq = 2, width = 640, height = 480, length = 12654, ptr = 0
uvc callback! frame_format = 6, seq = 3, width = 640, height = 480, length = 12432, ptr = 0
...
Output field meanings:
  • frame_format = 6 - MJPEG format
  • seq - Increments with each frame
  • width/height - Actual resolution negotiated with camera
  • length - Varies due to MJPEG compression

Performance Considerations

Buffer Size Selection:
  • 55KB handles most MJPEG frames at 640x480
  • For higher resolutions (720p+), increase to 100KB or more
  • Monitor length in callback to verify frames fit
Frame Rate:
  • 15 FPS is conservative for stability
  • Higher rates (30 FPS) require faster processing in callback
  • Keep callback execution time minimal

Next Steps: Processing Frames

Save Frame to SPIFFS

static void onCameraFrameCallback(uvc_frame *frame, void *user_ptr)
{
    File file = SPIFFS.open("/frame.jpg", FILE_WRITE);
    if (file) {
        file.write(frame->data, frame->data_bytes);
        file.close();
        Serial.println("Frame saved!");
    }
}

Send Frame Over WiFi

static void onCameraFrameCallback(uvc_frame *frame, void *user_ptr)
{
    // Send to HTTP client
    HTTPClient http;
    http.begin("http://server.com/upload");
    http.addHeader("Content-Type", "image/jpeg");
    http.POST(frame->data, frame->data_bytes);
    http.end();
}

Decode and Process Pixels

// For YUYV format, extract RGB values
void processYUYV(uvc_frame *frame) {
    uint8_t *data = frame->data;
    for (int i = 0; i < frame->data_bytes; i += 4) {
        int y1 = data[i];
        int u = data[i+1];
        int y2 = data[i+2];
        int v = data[i+3];
        // Convert YUV to RGB...
    }
}

Troubleshooting

No frames received:
  • Check USB cable connection
  • Verify camera is UVC-compatible
  • Increase buffer sizes if frames are too large
  • Check power supply (some cameras need external power)
Frame drops (sequence numbers skip):
  • Reduce frame rate
  • Minimize callback processing time
  • Increase buffer sizes
  • Use lower resolution
Memory allocation fails:
  • Reduce buffer sizes
  • Free unused memory before initialization
  • Use PSRAM if available on ESP32-S3

Combined Streaming

Use UVC camera with UAC audio simultaneously

UAC Microphone

Capture audio from USB microphones

Build docs developers (and LLMs) love