Stream Images from ESP32-CAM to MQTTfy Dashboard via MQTT

November 21, 2025

The ESP32-CAM is a powerful, low-cost microcontroller with an integrated camera, making it a perfect choice for a wide range of IoT projects, from home security to remote monitoring. When combined with the MQTT protocol and a real-time dashboard like MQTTfy, you can create a surprisingly capable live image streaming system.

This comprehensive guide will walk you through the entire process: setting up your Arduino IDE, programming the ESP32-CAM to capture images, encoding them, and publishing them over MQTT. We'll then show you how to configure an Image Widget in MQTTfy to display your live feed.

Core Concepts: How It Works

Before diving into the code, let's understand the data flow. The browser-based MQTTfy dashboard cannot directly receive a raw image file. Instead, we must encode the image into a text format that can be sent within a standard MQTT message. The best format for this is a Base64 Data URI.

The process is as follows:

  1. The ESP32-CAM captures an image from its camera sensor.
  2. The firmware encodes the raw image data (JPEG) into a Base64 string.
  3. This string is prepended with data:image/jpeg;base64, to form a complete Data URI.
  4. The entire Data URI string is published as the payload of an MQTT message to a specific topic.
  5. The Image Widget in MQTTfy, subscribed to that topic, receives the Data URI and natively renders the image in your browser.
Architecture diagram showing ESP32-CAM sending image data via an MQTT Broker to the MQTTfy dashboard.

Prerequisites

  1. Hardware: An ESP32-CAM board and an FTDI programmer to upload code.
  2. Software: Arduino IDE with the ESP32 board manager installed.
  3. Libraries:
    • PubSubClient: For MQTT communication.
    • Arduino_JSON: For handling JSON data (optional but good practice).
    • base64.h: For encoding the image.
  4. MQTT Broker: A publicly accessible MQTT broker that supports WebSockets (WSS). For testing, you can use broker.hivemq.com on port 8884.
  5. MQTTfy Dashboard: An active MQTTfy dashboard to display the images.

Step 1: Setting Up the Arduino IDE

If you haven't already, make sure your Arduino IDE is set up for ESP32 development.

  1. In Arduino IDE, go to File > Preferences.
  2. Add the following URL to "Additional Boards Manager URLs": https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
  3. Go to Tools > Board > Boards Manager..., search for "esp32", and install the package by Espressif Systems.
  4. Install the PubSubClient library from Tools > Manage Libraries....

For Base64 encoding, we will use a simple and effective header file. Create a new tab in your Arduino sketch named base64.h and paste the following code into it:

// In a new tab named "base64.h"
#ifndef BASE64_H
#define BASE64_H

const char b64_alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                           "abcdefghijklmnopqrstuvwxyz"
                           "0123456789+/";

int base64_encode(char *output, const char *input, int inputLen) {
    int i = 0, j = 0;
    int encLen = 0;
    unsigned char a3[3];
    unsigned char a4[4];

    while(inputLen--) {
        a3[i++] = *(input++);
        if(i == 3) {
            a4[0] = (a3[0] & 0xfc) >> 2;
            a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4);
            a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6);
            a4[3] = a3[2] & 0x3f;

            for(i = 0; i < 4; i++) {
                output[encLen++] = b64_alphabet[a4[i]];
            }
            i = 0;
        }
    }

    if(i) {
        for(j = i; j < 3; j++) {
            a3[j] = '\0';
        }

        a4[0] = (a3[0] & 0xfc) >> 2;
        a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4);
        a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6);
        a4[3] = a3[2] & 0x3f;

        for(j = 0; j < i + 1; j++) {
            output[encLen++] = b64_alphabet[a4[j]];
        }

        while((i++ < 3)) {
            output[encLen++] = '=';
        }
    }
    output[encLen] = '\0';
    return encLen;
}

#endif // BASE64_H

Step 2: The Full ESP32-CAM Arduino Code

Now, in your main sketch file, paste the following code. Remember to update the WiFi and MQTT configuration variables with your own credentials.

#include "WiFi.h"
#include "esp_camera.h"
#include "PubSubClient.h"
#include "base64.h"

// --- WiFi & MQTT Configuration ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "broker.hivemq.com";
const int mqtt_port = 1883;
const char* mqtt_image_topic = "esp32/cam/image";

// --- Camera Pin Definitions (AI-Thinker Model) ---
#define PWDN_GPIO_NUM     32
#define RESET_GPIO_NUM    -1
#define XCLK_GPIO_NUM      0
#define SIOD_GPIO_NUM     26
#define SIOC_GPIO_NUM     27
#define Y9_GPIO_NUM       35
#define Y8_GPIO_NUM       34
#define Y7_GPIO_NUM       39
#define Y6_GPIO_NUM       36
#define Y5_GPIO_NUM       21
#define Y4_GPIO_NUM       19
#define Y3_GPIO_NUM       18
#define Y2_GPIO_NUM        5
#define VSYNC_GPIO_NUM    25
#define HREF_GPIO_NUM     23
#define PCLK_GPIO_NUM     22

WiFiClient wifiClient;
PubSubClient client(wifiClient);

unsigned long lastCaptureTime = 0;
const long captureInterval = 5000; // Capture image every 5 seconds

void setup() {
  Serial.begin(115200);
  Serial.setDebugOutput(true);
  Serial.println();

  // Initialize Camera
  initCamera();

  // Connect to Wi-Fi
  connectWifi();

  // Configure MQTT
  client.setServer(mqtt_server, mqtt_port);
}

void loop() {
  if (!client.connected()) {
    reconnectMQTT();
  }
  client.loop();

  unsigned long currentMillis = millis();
  if (currentMillis - lastCaptureTime >= captureInterval) {
    lastCaptureTime = currentMillis;
    captureAndPublishImage();
  }
}

void 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_sscb_sda = SIOD_GPIO_NUM;
  config.pin_sscb_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;
  
  // For better quality, use UXGA. For faster streaming, use smaller resolutions.
  config.frame_size = FRAMESIZE_VGA; // 640x480
  config.jpeg_quality = 12; // 0-63, lower number means higher quality
  config.fb_count = 1;

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init failed with error 0x%x", err);
    return;
  }
}

void connectWifi() {
  Serial.print("Connecting to WiFi...");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
}

void reconnectMQTT() {
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    String clientId = "ESP32-CAM-Client-";
    clientId += String(random(0xffff), HEX);
    if (client.connect(clientId.c_str())) {
      Serial.println("connected");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

void captureAndPublishImage() {
  camera_fb_t * fb = NULL;
  fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Camera capture failed");
    return;
  }

  Serial.printf("Picture file name: /image.jpg, size: %zu bytes\n", fb->len);

  // Allocate memory for the Base64-encoded image
  // Base64 output is approx. 4/3 the size of the input
  char *base64_data = (char*) malloc((fb->len * 4 / 3) + 4);
  if (!base64_data) {
      Serial.println("Malloc for base64 data failed");
      esp_camera_fb_return(fb);
      return;
  }
  
  // Encode the image
  int encoded_len = base64_encode(base64_data, (const char*)fb->buf, fb->len);
  
  // The full data URI prefix
  const char* data_uri_prefix = "data:image/jpeg;base64,";
  
  // We need to publish in chunks due to MQTT packet size limits (default 128 bytes in PubSubClient)
  client.beginPublish(mqtt_image_topic, strlen(data_uri_prefix) + encoded_len, false);
  client.print(data_uri_prefix);
  
  // Send the Base64 data in chunks
  int chunkSize = 256; 
  for (int i = 0; i < encoded_len; i += chunkSize) {
    client.write((uint8_t*)(base64_data + i), min(chunkSize, encoded_len - i));
  }
  
  client.endPublish();

  free(base64_data);
  esp_camera_fb_return(fb);
  
  Serial.println("Image published to MQTT");
}

Step 3: Configure MQTTfy Dashboard

The final step is to set up a widget in MQTTfy to display the image stream.

  1. Add an Image Widget: In your MQTTfy dashboard, click to add a new widget and select the Image Display widget.
  2. Configure the Data Source:
    • Set the Data Source Type to MQTT.
    • Enter the details for your MQTT broker (e.g., broker.hivemq.com, port 8884, protocol wss).
    • In the MQTT Topic field, enter the exact topic you defined in the Arduino code: esp32/cam/image.
  3. Configure Image Settings:
    • Ensure the Image Source Type is set to MQTT (Base64 Data URI). This is crucial for the widget to correctly interpret the incoming message.
  4. Save the Widget: Save the configuration, and the widget will appear on your dashboard.

Within a few seconds of your ESP32-CAM connecting and publishing its first image, you should see the live feed appear directly in your MQTTfy dashboard, updating automatically at the interval you set.

Animation showing the image widget on the MQTTfy dashboard updating with new pictures from the ESP32-CAM.

This setup provides a powerful and flexible foundation. You can now expand on this by adding buttons in your dashboard to trigger a photo on demand, integrate multiple cameras on different topics, or even pass the image data to an AI widget for analysis.