DEV Community

Zishan Ghaniwala for Video SDK

Posted on • Originally published at videosdk.live

Build ESP32 Video & Audio Calling With VideoSDK

ESP32 video calling runs two-way WebRTC audio and video directly on an ESP32-S3 microcontroller. The VideoSDK IoT SDK ships as an ESP-IDF component that joins your board to the same VideoSDK room a browser joins, with no signaling server of your own. Set a token, flash the firmware, and join from the web.

Until recently, ESP32 video calling meant building, hosting, and maintaining your own signaling and media backend before a single frame moved. The VideoSDK IoT SDK cuts out that infrastructure. It provides a native ESP-IDF component that hooks an ESP32-S3 directly into a WebRTC room, the exact same room a browser or mobile app joins.

Your board streams camera and microphone input into the room, and on boards with a screen and speaker, it draws remote video on the LCD and plays remote audio back. By the end of this guide you will have an ESP32-S3 holding a live video and audio call with a web application, without managing a custom media server.

What Is ESP32 Video Calling?

ESP32 video calling is defined as real-time, two-way audio and video streaming that originates and terminates on an ESP32 microcontroller rather than on a phone or laptop. It works by capturing frames from an onboard camera and samples from a microphone, encoding them on-device, and publishing them over WebRTC to every other participant in a room.

The VideoSDK IoT SDK provides this capability as a native ESP-IDF component for the ESP32-S3 family, published on Espressif's official component registry under the MIT license. It handles the room connection logic, WebRTC encryption (DTLS and SRTP), and media stream lifecycle, so your firmware manages calls through a handful of function calls.

The board is a first-class participant, not a camera feeding a gateway. It appears in the participant list, it can be muted, and it shares the same room ID your React or JavaScript client already uses.

Architecture

What You Need Before You Start

The board you choose decides whether your device can only send media or handle full two-way playback, so pick it before writing any code.

Board Send (Mic & Camera) Receive (Speaker & LCD) Hardware Detail
XIAO ESP32-S3 (Sense) Yes No Includes camera & mic, but lacks onboard display & speaker
ESP32-S3-Korvo-2 v3.0 Yes Yes Includes camera, mic, LCD, and speaker for full two-way A/V

On the XIAO board, calling startSubscribeVideo() or startSubscribeAudio() returns DEVICE_NOT_SUPPORTED. That is expected behavior for missing hardware, not a bug. Pick the Korvo-2 if you want the board itself to display and play back the far end.

Prerequisites:

  • ESP-IDF 5.4.4 or newer with the environment exported. The component declares 5.4.4 as its minimum.
  • Python 3.11+, required by the ESP-IDF build tools.
  • 8 MB of flash and PSRAM on the board. The application image does not fit in 4 MB.
  • A VideoSDK account for your API key, secret, and room tokens.

How ESP32 Video Calling Works Under the Hood

Running WebRTC on an ESP32 is a memory problem before it is a CPU problem. The WebRTC stack, the DTLS handshake state, video frames, and audio buffers all compete for internal SRAM, which is why supported boards carry external PSRAM and why the SDK depends on it for stable operation.

WebRTC security runs end to end. The device performs a DTLS handshake and transmits media over encrypted SRTP streams, the same transport a browser uses.

Receiving is where the SDK saves the most work. Publishing is comparatively simple, but subscribing means decoding incoming video and audio, absorbing network jitter, drawing to an LCD, and driving speaker hardware in real time inside tight SRAM limits. On the ESP32-S3-Korvo-2, the publish and subscribe calls for both audio and video run concurrently.

Wi-Fi quality shows up directly in frame rate and audio clarity. Keep the antenna clear, connect to 2.4 GHz, and test on the network the hardware will actually run on in production.

When Not to Use the IoT SDK

Espressif ships its own esp-webrtc-solution, and it wins in a specific case: you already run your own signaling server and media infrastructure, or you need to peer directly with a device that is not in a VideoSDK room. The IoT SDK assumes VideoSDK rooms and VideoSDK tokens, which is exactly the assumption that removes the backend work. If you want the board on an existing SFU you operate yourself, the managed path is the wrong trade.

Step 1: Set Up the ESP-IDF Environment

Install Espressif's ESP-IDF toolchain. The commands below cover macOS; see the official ESP-IDF getting started guide for Linux and Windows.

Install build dependencies:

brew install cmake ninja dfu-util ccache git wget flex bison gperf
brew install openssl libffi
Enter fullscreen mode Exit fullscreen mode

Clone ESP-IDF at v5.4.4 or newer and run the installer. Older branches will not build this component:

mkdir -p ~/esp && cd ~/esp
git clone --recursive -b v5.4.4 https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh esp32s3
Enter fullscreen mode Exit fullscreen mode

Export the environment variables into your shell session:

source ~/esp/esp-idf/export.sh
Enter fullscreen mode Exit fullscreen mode

Step 2: Generate Your VideoSDK Token

VideoSDK uses token-based authentication. For building and testing you do not need a signing server: the dashboard generates a token for you in about a minute.

  1. Open the VideoSDK dashboard and sign up or log in.
  2. Copy your API key and secret.
  3. Generate a temporary token and save it.

Generate a API key

You also need a room (meeting) ID, and the order here matters. The React client in Step 7 mints one for you when you click New Meeting, and that is the ID you flash onto the board.

Firmware is harder to rotate than a web bundle, so a long-lived token flashed into a device is a liability. The VideoSDK authentication and token guide covers token lifetimes and permission scopes.

Step 3: Create the Project From the Example

Fetch the project structure from the official video_call example, then extend it to carry audio as well.

idf.py create-project-from-example "videosdk/iot-sdk=0.3.1:video_call"
Enter fullscreen mode Exit fullscreen mode

The stock video_call example captures camera input and draws incoming video on supported hardware such as the ESP32-S3-Korvo-2, running with videoCodec = VIDEO_CODEC_JPEG. Step 5 adds the audio streams.

Step 4: Configure the Build

Set the target chip to esp32s3 and open menuconfig to enter credentials and Wi-Fi settings.

idf.py set-target esp32s3
idf.py menuconfig
Enter fullscreen mode Exit fullscreen mode

menuconfig interface

In the menuconfig interface:

  • Microcontroller board: Select ESP32-S3-Korvo-2 or ESP32-S3-XIAO (XIAO is the default).
  • VideoSDK Configuration: Paste the token and the meeting / room ID. If you are following the repo flow, get the room ID from the React client in Step 7 first, then come back here.
  • Example Connection Configuration: Enter your 2.4 GHz Wi-Fi SSID and password.
  • Flash and Partitions: Set flash size to 8 MB and keep the partition table the example ships.

Keep sdkconfig out of public repositories. It holds your Wi-Fi credentials and your VideoSDK token in plain text.

Step 5: Understand the Code

The firmware flow connects to Wi-Fi, fills an init_config_t structure, calls init(), and starts the media streams. The example ships as a video-only call, so its app_main sets only the video codec and starts only the two video streams:

#include "videosdk.h"
#include "sdkconfig.h"

void app_main(void)
{
    // Initialize NVS, network interfaces, event loops, and Wi-Fi...

    init_config_t cfg = {
        .meetingID     = CONFIG_VIDEOSDK_MEETING_ID,
        .token         = CONFIG_VIDEOSDK_TOKEN,
        .displayName   = "ESP32S3-AV-Device",
        .participantId = "",
        .audioCodec    = AUDIO_CODEC_PCMA,
        .videoCodec    = VIDEO_CODEC_JPEG, // Video only, as shipped
    };

    if (init(&cfg) != RESULT_OK) {
        return;
    }

    startPublishVideo();     // Stream the onboard camera
    startSubscribeVideo();   // Draw to the LCD (Korvo-2 only)

    while (1) {
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}
Enter fullscreen mode Exit fullscreen mode

To turn this into a full audio and video call, add audio yourself. It is just one change. Start the two audio streams alongside the video ones:

    init_config_t cfg = {
        .meetingID     = CONFIG_VIDEOSDK_MEETING_ID,
        .token         = CONFIG_VIDEOSDK_TOKEN,
        .displayName   = "ESP32S3-AV-Device",
        .participantId = "",
        .audioCodec    = AUDIO_CODEC_PCMA,
        .videoCodec    = VIDEO_CODEC_JPEG,
    };

    // ...after init() returns RESULT_OK:

    startPublishAudio();     // Add: stream the onboard microphone
    startPublishVideo();
    startSubscribeAudio();   // Add: play to the speaker (Korvo-2 only)
    startSubscribeVideo();
Enter fullscreen mode Exit fullscreen mode

That gives you all four streams for full A/V. Every SDK function returns a result_t status code, where RESULT_OK equals 0. On send-only hardware like the XIAO, startSubscribeAudio() returns DEVICE_NOT_SUPPORTED, which is expected. Check return codes in production firmware rather than assuming the stream came up.

Step 6: Build, Flash, and Verify the Join

Compile the firmware, flash the board, and open the serial monitor:

idf.py build
idf.py -p <PORT> flash monitor
Enter fullscreen mode Exit fullscreen mode

A successful boot and room join produces this sequence. Values are redacted; yours will show your own network details:

I (20151) wifi:connected with <YOUR_SSID>, aid = 7, channel 7, BW20, bssid = <REDACTED>
I (20152) wifi:security: WPA2-PSK, phy: bgn, rssi: -49
I (21194) esp_netif_handlers: example_netif_sta ip: <LOCAL_IP>, mask: 255.255.255.0
I (21205) example_common: Connected to example_netif_sta
I (21211) IOT-SDK-AUDIO: Device ID: e-ccba970e9220
I (21215) videosdk: participantId (peerId): e-ccba970e9220
I (21256) IOT-SDK-AUDIO: init: 0
I (23312) protoo: decrypted 4 ICE server(s)
I (23313) protoo: connecting websocket to in1.rm.videosdk.live
I (23562) esp-x509-crt-bundle: Certificate validated
I (24094) protoo: websocket connected
I (24113) protoo: protoo client ready (peerId=e-ccba970e9220)
I (26178) videosdk: using 1 STUN server(s) for ICE
I (29220) peer: Created inbound SRTP session
I (29220) peer: Created outbound SRTP session
I (29474) videosdk: DataChannel transport up (ICE+DTLS)
I (29534) videosdk: SCTP data channel connected after 60 ms
I (29807) videosdk: audio RTP producer id=4bb13f4c-b453-4445-8924-ab09215dfd1b
I (29807) videosdk: bringing up mic
Result:0
Enter fullscreen mode Exit fullscreen mode

The two lines that matter are Created outbound SRTP session and the RTP producer id. Together they confirm the DTLS handshake completed and the board is publishing. A few example_connect: Wi-Fi disconnected, trying to reconnect... lines before the association are normal on a busy 2.4 GHz channel.

Step 7: Join the Same Room From a React App

Nothing about the web side is ESP32-specific, which is the point: the board is just another participant. VideoSDK publishes a companion repository, videosdk-live/videosdk-rtc-iot-sdk-example, that carries the firmware under IoT/ and two ready-to-run browser clients under web/js (vanilla JavaScript) and web/react. Both talk to the same room, so pick whichever matches your stack. This guide uses the React client.

Clone the repository and install the client:

git clone https://github.com/videosdk-live/videosdk-rtc-iot-sdk-example.git
cd videosdk-rtc-iot-sdk-example/web/react
npm install
Enter fullscreen mode Exit fullscreen mode

Create the environment file and drop in the token from Step 2:

cp .env.example .env
Enter fullscreen mode Exit fullscreen mode
REACT_APP_VIDEOSDK_TOKEN=<your token>
Enter fullscreen mode Exit fullscreen mode

The token must carry the allow_join permission. Create React App inlines .env at build time, so restart the dev server after any change to it, otherwise you will keep authenticating with the old value and blame the board.

npm start
Enter fullscreen mode Exit fullscreen mode

The app comes up on http://localhost:3000. Browsers only grant camera and microphone access on a secure context, and localhost counts as one, so no TLS setup is needed for local development.

Joining Screen

Click New Meeting. The app creates the room and shows you the meeting ID. That ID goes into idf.py menuconfig under VideoSDK Configuration > Meeting / room ID back in Step 4. Reflash the board, and once it joins, the ESP32-S3 appears as its own video tile under the displayName you set in Step 5.

Meeting UI

What the React Client Adds for IoT Participants

MeetingView.js is an ordinary VideoSDK meeting view built on the same hooks as the React SDK quickstart, with no device-specific logic in it. Everything that knows about hardware lives in src/iot, which is a useful separation to copy if you are bolting device support onto an app you already have.

Export Purpose
<IOTBridge /> The one piece that has to be mounted for device support to work
<IOTVideoPlayer participantId={id} /> Renders a device participant's video and audio
useIOTMessages() Hook for receiving messages sent from the board's data channel
sendIOTMessage() Sends a message to the device, 15 KiB maximum per message
isIOTDevice() Tells a device participant apart from a browser participant

useIOTMessages() and sendIOTMessage() are the browser half of the setDataMessageHandler() and sendMessage() calls from Step 5. That pairing is what turns a call into a control channel: stream the camera for the human, and send a JSON command over the same connection to move a servo or read a sensor, with no second transport to operate.

On a Korvo-2, this is also where the loop closes. Your browser webcam draws to the board's LCD and your voice plays through its speaker, while its camera and microphone come back to you in the tile.

Common Errors

  • DEVICE_NOT_SUPPORTED on a subscribe call: Expected on boards without LCD or speaker output, such as the XIAO. Use a Korvo-2 for playback on the device.
  • INIT_NOT_CALLED: A start* function ran before init() returned RESULT_OK. Check the init() return code before starting streams.
  • Build fails or the image will not fit: Confirm ESP-IDF is on v5.4 or newer, flash size is set to 8 MB, and PSRAM is enabled for the selected board.
  • Wi-Fi connects but the room join fails: Verify the room ID formatting and confirm the token carries allow_join permission and has not expired.
  • Board joins but no media arrives: Confirm the browser used the same room ID, and check the serial log for Created outbound SRTP session. Its absence points at the DTLS handshake, usually a blocked UDP path on the network.

ESP32 Video Calling Glossary

Room: A VideoSDK meeting space identified by a unique room ID that participants join to share media streams. The ESP32-S3 and your browser join the same room ID, which is what makes them see each other.

Participant: Any client connected to a room with its own audio and video streams. The IoT SDK registers the board as a participant with a displayName and a participantId.

Meeting Token: A JWT authorizing a participant to join a room, generated from the VideoSDK dashboard for testing or minted server-side for production.

SRTP: Secure Real-time Transport Protocol, the encrypted media transport WebRTC uses. The IoT SDK negotiates SRTP keys through a DTLS handshake, so board media is encrypted on the wire exactly as browser media is.

ESP-IDF component: A reusable package in Espressif's IoT Development Framework, resolvable from the component registry. The VideoSDK IoT SDK ships as one, so idf.py pulls it in as a dependency rather than you vendoring source.

Frequently Asked Questions

Can an ESP32 join real-time calls?

Yes. An ESP32-S3 running the VideoSDK IoT SDK connects directly to a VideoSDK room over WebRTC and streams video and audio with browsers, phones, and other devices in real time. The board is a full participant in the room, not a feed proxied through a gateway.

Which ESP32 boards are supported?

The IoT SDK targets the ESP32-S3 family. The XIAO ESP32-S3 (Sense) handles camera and microphone transmission, while the ESP32-S3-Korvo-2 v3.0 supports full two-way A/V with its integrated LCD and speaker driver.

Does the IoT SDK use standard WebRTC?

Yes. The VideoSDK IoT SDK uses standard WebRTC protocols end to end, establishing encrypted SRTP media channels through DTLS handshakes.

Can two ESP32 devices call each other?

Yes. Two-way interactive calling works when both ESP32 devices have media output hardware, such as two ESP32-S3-Korvo-2 boards equipped with speakers and displays. If a send-only board like the XIAO ESP32-S3 is used, it can publish its streams into the room but cannot receive or play back incoming media.

Why does the ESP32 send JPEG instead of H.264?

JPEG compresses each frame independently, so it needs no reference frames and far less RAM than a video codec. On the supported boards the camera sensor emits JPEG directly, which keeps the ESP32-S3 out of the encoding path entirely.

Is the IoT SDK free to use?

The IoT SDK component is open source under the MIT license. VideoSDK provides a free monthly credit tier covering room connectivity and media routing.

Conclusion

ESP32 video calling stops being an infrastructure project once the board can join a managed room directly. The IoT SDK reduces it to a token, a room ID, and four function calls, and the browser side is a repository you clone. The remaining engineering is where it should be: your product's camera placement, power budget, and reconnect behavior.

Flash it once and watching a microcontroller appear in a browser participant list is genuinely strange in a good way. What are you building with VideoSDK? Drop a comment, I would love to hear what kind of embedded calling use case you are working on.

Top comments (0)