IoT

ESP32-CAM Video Streaming: MJPEG, Face Detection & Telegram Bot

TOKEN

Buat sistem kamera pintar dengan ESP32-CAM yang bisa streaming video, deteksi wajah, dan kirim notifikasi ke Telegram

1. Overview ESP32-CAM

ESP32-CAM adalah modul kompak berbasis ESP32 dengan kamera OV2640 built-in, slot microSD, dan LED flash. Dengan harga di bawah Rp50.000, modul ini menjadi solusi paling populer untuk proyek kamera IoT murah.

Spesifikasi ESP32-CAM

SpesifikasiDetail
ProcessorESP32-S (Xtensa dual-core 240MHz)
RAM520 KB SRAM + 4 MB PSRAM (opsional)
Flash4 MB
KameraOV2640 (2MP, JPEG output)
ResolusiUXGA (1600x1200) hingga QQVGA (160x120)
WiFi802.11 b/g/n (2.4 GHz)
BluetoothBLE 4.2
GPIO9 pin (terbatas karena kamera)
SD CardMicroSD (FAT32, max 4GB)
Dimensi27mm × 40.5mm × 4.5mm
âš ī¸ Perhatian GPIO

ESP32-CAM menggunakan banyak GPIO internal untuk kamera, sehingga tersisa hanya 9 GPIO yang bisa dipakai user (GPIO 0, 1, 3, 4, 12, 13, 14, 15, 16). GPIO 0 harus HIGH saat normal boot (LOW saat flashing). GPIO 1 dan 3 adalah UART TX/RX.

📷
ESP32-CAM (AI Thinker)
Board Paling Populer
  • ✅ Harga murah (~30rb)
  • ✅ PSRAM 4MB built-in
  • ✅ LED flash on-board
  • ✅ MicroSD slot
  • ❌ Tidak ada USB-to-serial
  • ❌ Pin terbatas
📷
ESP32-S3 Cam
Generasi Baru
  • ✅ USB OTG built-in
  • ✅ RAM lebih besar (512KB + PSRAM)
  • ✅ AI acceleration
  • ✅ OV5640 (5MP) support
  • ❌ Lebih mahal (~60rb)
  • ❌ Ekosistem lebih muda

2. Setup Hardware & Wiring

ESP32-CAM tidak memiliki USB-to-serial onboard, jadi kita memerlukan FTDI adapter atau board ESP32-CAM-MB (yang sudah include USB) untuk memprogramnya.

Wiring dengan FTDI Adapter

ESP32-CAM PinFTDI PinKeterangan
5V5V / VCCPower supply
GNDGNDGround
U0R (GPIO3)TXUART receive
U0T (GPIO1)RXUART transmit
GPIO0GND (saat flash)Flash mode (lepas setelah flash)
💡 Tips Flashing

Untuk mem-flash: hubungkan GPIO0 ke GND lalu tekan RESET. Untuk menjalankan program: lepas GPIO0 dari GND lalu tekan RESET. Board ESP32-CAM-MB melakukan ini otomatis.

PlatformIO Configuration

INI (platformio.ini)
[env:esp32cam]
platform = espressif32
board = esp32cam
framework = arduino
monitor_speed = 115200
board_build.partitions = huge_app.csv

; Tambahkan PSRAM
build_flags =
    -DBOARD_HAS_PSRAM
    -mfix-esp32-psram-cache-issue

; Upload via FTDI
upload_speed = 460800
upload_port = COM3

3. OV2640 Camera Configuration

OV2640 adalah sensor kamera 2MP yang mendukung output JPEG hardware. Di ESP32-CAM, konfigurasi kamera ditangani oleh library esp_camera.h.

C++ (Arduino)
#include "esp_camera.h"

// Pin mapping untuk AI-Thinker ESP32-CAM
#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

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;  // 20 MHz clock
  config.pixel_format = PIXFORMAT_JPEG;
  config.grab_mode    = CAMERA_GRAB_LATEST;
  config.fb_location  = CAMERA_FB_IN_PSRAM;
  config.jpeg_quality = 12;
  config.fb_count     = 2;  // Double buffer untuk streaming

  // Resolusi berdasarkan PSRAM
  if (psramFound()) {
    config.frame_size   = FRAMESIZE_UXGA;  // 1600x1200
    config.jpeg_quality = 10;
    config.fb_count     = 2;
    Serial.println("PSRAM ditemukan — resolusi UXGA");
  } else {
    config.frame_size   = FRAMESIZE_SVGA;  // 800x600
    config.jpeg_quality = 12;
    config.fb_count     = 1;
    config.fb_location  = CAMERA_FB_IN_DRAM;
    Serial.println("PSRAM tidak ditemukan — resolusi SVGA");
  }

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("Camera init GAGAL: 0x%x\n", err);
    return false;
  }

  // Fine-tune sensor settings
  sensor_t *s = esp_camera_sensor_get();
  if (s) {
    s->set_brightness(s, 1);     // -2 to 2
    s->set_contrast(s, 1);       // -2 to 2
    s->set_saturation(s, 0);     // -2 to 2
    s->set_whitebal(s, 1);       // Auto white balance
    s->set_awb_gain(s, 1);       // AWB gain
    s->set_wb_mode(s, 0);        // 0=Auto, 1=Sunny, 2=Cloudy, 3=Office, 4=Home
    s->set_exposure_ctrl(s, 1);  // Auto exposure
    s->set_aec2(s, 1);           // AEC DSP
    s->set_gain_ctrl(s, 1);      // Auto gain
    s->set_agc_gain(s, 0);       // 0-30
    s->set_gainceiling(s, (gainceiling_t)6);  // 2x-128x
    s->set_bpc(s, 1);            // Black pixel correct
    s->set_wpc(s, 1);            // White pixel correct
    s->set_lenc(s, 1);           // Lens correction
    s->set_hmirror(s, 0);        // Horizontal mirror
    s->set_vflip(s, 0);          // Vertical flip
    s->set_dcw(s, 1);            // Downsize EN
  }

  Serial.println("Camera OK!");
  return true;
}

void captureAndSave() {
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    Serial.println("Capture gagal");
    return;
  }
  Serial.printf("Captured: %dx%d, %d bytes\n",
    fb->width, fb->height, fb->len);

  // Simpan ke SD card
  File file = SD_MMC.open("/photo.jpg", FILE_WRITE);
  if (file) {
    file.write(fb->buf, fb->len);
    file.close();
    Serial.println("Tersimpan di SD card!");
  }

  esp_camera_fb_return(fb);
}

Resolusi yang Didukung OV2640

FramesizeResolusiUkuran JPEGFPS (est.)
FRAMESIZE_QQVGA160×120~2-5 KB30+
FRAMESIZE_QVGA320×240~5-15 KB25-30
FRAMESIZE_VGA640×480~15-40 KB15-25
FRAMESIZE_SVGA800×600~30-70 KB10-15
FRAMESIZE_XGA1024×768~50-120 KB5-10
FRAMESIZE_UXGA1600×1200~80-200 KB2-5

4. MJPEG Video Streaming

MJPEG (Motion JPEG) adalah format streaming yang paling cocok untuk ESP32-CAM. Setiap frame dikirim sebagai JPEG individual, sehingga tidak perlu codec kompresi video yang berat.

C++ (Arduino - MJPEG Stream Handler)
#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);

// MJPEG streaming handler
void handleStream() {
  WiFiClient client = server.client();
  String response = "HTTP/1.1 200 OK\r\n"
                    "Content-Type: multipart/x-mixed-replace;boundary=frame\r\n"
                    "\r\n";
  client.print(response);

  Serial.println("Stream client terhubung");

  while (client.connected()) {
    camera_fb_t *fb = esp_camera_fb_get();
    if (!fb) {
      Serial.println("Capture gagal");
      break;
    }

    // Kirim boundary dan header JPEG
    String header = "--frame\r\n"
                    "Content-Type: image/jpeg\r\n"
                    "Content-Length: " + String(fb->len) + "\r\n"
                    "\r\n";

    client.print(header);
    client.write(fb->buf, fb->len);
    client.print("\r\n");

    esp_camera_fb_return(fb);

    // Yield untuk mencegah WDT reset
    yield();

    // Rate limiting: ~10 FPS
    delay(100);
  }

  Serial.println("Stream client terputus");
}

// Single capture handler
void handleCapture() {
  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) {
    server.send(500, "text/plain", "Capture failed");
    return;
  }

  server.sendHeader("Content-Disposition", "inline; filename=capture.jpg");
  server.send_P(200, "image/jpeg", (const char *)fb->buf, fb->len);
  esp_camera_fb_return(fb);
}

void setup() {
  Serial.begin(115200);

  // Init camera
  if (!initCamera()) {
    Serial.println("Camera init failed!");
    return;
  }

  // Connect WiFi
  WiFi.begin("SSID", "PASSWORD");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.printf("\nWiFi: %s\n", WiFi.localIP().toString().c_str());

  // Setup web routes
  server.on("/", HTTP_GET, handleRoot);
  server.on("/stream", HTTP_GET, handleStream);
  server.on("/capture", HTTP_GET, handleCapture);
  server.on("/control", HTTP_GET, handleControl);
  server.begin();

  Serial.println("Server siap di http://" + WiFi.localIP().toString());
}

void loop() {
  server.handleClient();
}

HTML Viewer untuk MJPEG Stream

HTML
<!-- Embedded HTML di ESP32-CAM -->
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title>ESP32-CAM</title>
  <style>
    body { font-family: Arial; background: #1a1a2e; color: #fff;
           text-align: center; margin: 0; padding: 20px; }
    .stream-container { margin: 20px auto; max-width: 800px; }
    img { width: 100%; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
    .controls { margin: 20px 0; }
    button { padding: 10px 20px; margin: 5px; border: none;
             border-radius: 8px; background: #4ecdc4; color: #fff;
             cursor: pointer; font-size: 16px; }
    button:hover { background: #45b7aa; }
    select { padding: 10px; border-radius: 8px; font-size: 16px; }
    .info { background: #16213e; padding: 15px; border-radius: 10px; margin: 10px; }
  </style>
</head>
<body>
  <h1>📷 ESP32-CAM Live</h1>
  <div class="stream-container">
    <img id="stream" src="/stream" alt="Live Stream">
  </div>
  <div class="controls">
    <button onclick="capturePhoto()">📸 Ambil Foto</button>
    <select id="resolution" onchange="setResolution(this.value)">
      <option value="0">UXGA (1600x1200)</option>
      <option value="5" selected>SVGA (800x600)</option>
      <option value="8">VGA (640x480)</option>
      <option value="10">QVGA (320x240)</option>
    </select>
    <select id="quality" onchange="setQuality(this.value)">
      <option value="10">Kualitas Tinggi</option>
      <option value="15" selected>Kualitas Sedang</option>
      <option value="20">Kualitas Rendah</option>
    </select>
  </div>
  <div class="info">
    <span id="fps">FPS: --</span> |
    <span id="res">Resolusi: --</span>
  </div>
  <script>
    function capturePhoto() {
      window.open('/capture', '_blank');
    }
    function setResolution(val) {
      fetch('/control?var=framesize&val=' + val);
    }
    function setQuality(val) {
      fetch('/control?var=quality&val=' + val);
    }
  </script>
</body>
</html>
)rawliteral";

5. Web Server & Control Panel

Kita bisa membangun web server lengkap di ESP32-CAM yang menyediakan halaman kontrol dengan pengaturan resolusi, brightness, contrast, dan parameter kamera lainnya.

C++ (Control Handler)
// Handler untuk parameter kamera via HTTP
void handleControl() {
  if (!server.hasArg("var") || !server.hasArg("val")) {
    server.send(400, "text/plain", "Missing parameters");
    return;
  }

  String var = server.arg("var");
  int val = server.arg("val").toInt();

  sensor_t *s = esp_camera_sensor_get();
  if (!s) {
    server.send(500, "text/plain", "Sensor not available");
    return;
  }

  int res = 0;
  if (var == "framesize") {
    res = s->set_framesize(s, (framesize_t)val);
  } else if (var == "quality") {
    res = s->set_quality(s, val);
  } else if (var == "brightness") {
    res = s->set_brightness(s, val);
  } else if (var == "contrast") {
    res = s->set_contrast(s, val);
  } else if (var == "saturation") {
    res = s->set_saturation(s, val);
  } else if (var == "hmirror") {
    res = s->set_hmirror(s, val);
  } else if (var == "vflip") {
    res = s->set_vflip(s, val);
  } else if (var == "flash") {
    // Kontrol LED flash
    int flashPin = 4;
    if (val == 0) {
      analogWrite(flashPin, 0);
    } else {
      analogWrite(flashPin, val * 25);  // 1-10 level
    }
    res = 0;
  } else {
    server.send(400, "text/plain", "Unknown variable");
    return;
  }

  if (res == 0) {
    server.send(200, "text/plain", "OK");
  } else {
    server.send(500, "text/plain", "Set failed");
  }
}

// Status handler — info sistem
void handleStatus() {
  String json = "{";
  json += "\"ip\":\"" + WiFi.localIP().toString() + "\",";
  json += "\"rssi\":" + String(WiFi.RSSI()) + ",";
  json += "\"freeHeap\":" + String(ESP.getFreeHeap()) + ",";
  json += "\"psram\":" + String(ESP.getFreePsram()) + ",";
  json += "\"uptime\":" + String(millis() / 1000);
  json += "}";
  server.send(200, "application/json", json);
}

6. Face Detection

ESP32 memiliki built-in face detection engine yang menggunakan hardware accelerator. Fitur ini tersedia melalui library fd_forward.h dan bisa mendeteksi beberapa wajah secara real-time.

C++ (Face Detection)
#include "fd_forward.h"
#include "fr_forward.h"
#include "esp_camera.h"

// Inisialisasi face detection
static mtmn_config_t initFaceDetection() {
  mtmn_config_t mtmn_config = {0};
  mtmn_config.type = FAST;
  mtmn_config.min_face = 80;       // Minimum face size (px)
  mtmn_config.pyramid = 0.707;     // Pyramid scaling factor
  mtmn_config.pyramid_times = 4;   // Pyramid levels
  mtmn_config.p_threshold.score = 0.6;
  mtdm_config.p_threshold.nms = 0.7;
  mtmn_config.p_threshold.candidate_number = 20;
  mtmn_config.r_threshold.score = 0.7;
  mtmn_config.r_threshold.nms = 0.7;
  mtmn_config.r_threshold.candidate_number = 10;
  mtmn_config.o_threshold.score = 0.7;
  mtmn_config.o_threshold.nms = 0.7;
  mtmn_config.o_threshold.candidate_number = 1;
  return mtmn_config;
}

// Detect faces dari camera frame
void detectFaces(camera_fb_t *fb) {
  static mtmn_config_t mtmn_config = initFaceDetection();

  // Konversi JPEG ke RGB888
  dl_matrix3du_t *image_matrix = dl_matrix3du_alloc(1, fb->width, fb->height, 3);
  if (!image_matrix) {
    Serial.println("Gagal alokasi matrix");
    return;
  }

  bool converted = fmt2rgb888(fb->buf, fb->len, PIXFORMAT_JPEG, image_matrix->item);
  if (!converted) {
    dl_matrix3du_free(image_matrix);
    Serial.println("Konversi gagal");
    return;
  }

  // Jalankan face detection
  box_array_t *net_boxes = face_detect(image_matrix, &mtmn_config);

  if (net_boxes && net_boxes->len > 0) {
    Serial.printf("Terdeteksi %d wajah!\n", net_boxes->len);

    for (int i = 0; i < net_boxes->len; i++) {
      int x = net_boxes->box[i].box_p[0];
      int y = net_boxes->box[i].box_p[1];
      int w = net_boxes->box[i].box_p[2] - x;
      int h = net_boxes->box[i].box_p[3] - y;
      float score = net_boxes->box[i].score[0];

      Serial.printf("  Wajah %d: x=%d y=%d %dx%d score=%.2f\n",
        i+1, x, y, w, h, score);

      // Bisa diintegrasi dengan notifikasi Telegram
      if (score > 0.8) {
        sendTelegramAlert(fb);
      }
    }

    dl_lib_free(net_boxes->box);
    dl_lib_free(net_boxes);
  }

  dl_matrix3du_free(image_matrix);
}
â„šī¸ Face Detection Performance

Face detection pada ESP32 memerlukan resolusi QVGA (320×240) atau QQVGA (160×120) untuk performa real-time. Pada resolusi VGA atau lebih tinggi, proses deteksi memerlukan waktu 200-500ms per frame. Gunakan resolusi rendah untuk face detection, resolusi tinggi untuk capture foto.

7. Telegram Bot Integration

Telegram Bot API adalah cara terbaik untuk menerima notifikasi dan foto dari ESP32-CAM. Kita bisa membuat bot yang mengirim foto ketika wajah terdeteksi atau motion terdeteksi.

Langkah Setup Telegram Bot

C++ (Telegram Bot)
#include <WiFiClientSecure.h>
#include <HTTPClient.h>

// Ganti dengan token dan chat ID kamu
const char* BOT_TOKEN = "123456789:ABCdefGhIjKlMnOpQrStUvWxYz";
const char* CHAT_ID = "987654321";

// Kirim foto ke Telegram
bool sendTelegramPhoto(camera_fb_t *fb) {
  WiFiClientSecure client;
  client.setInsecure();  // Skip SSL verification (hanya untuk development)

  if (!client.connect("api.telegram.org", 443)) {
    Serial.println("Koneksi Telegram gagal");
    return false;
  }

  // Buat multipart form data
  String boundary = "----ESP32CAM" + String(millis());

  // Header
  String header = "POST /bot" + String(BOT_TOKEN) +
                  "/sendPhoto HTTP/1.1\r\n" +
                  "Host: api.telegram.org\r\n" +
                  "Content-Type: multipart/form-data; boundary=" + boundary + "\r\n";

  // Form fields
  String chatIdField = "--" + boundary + "\r\n" +
    "Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n" +
    String(CHAT_ID) + "\r\n";

  String captionField = "--" + boundary + "\r\n" +
    "Content-Disposition: form-data; name=\"caption\"\r\n\r\n" +
    "📷 ESP32-CAM Alert! " + String(millis()/1000) + "s\r\n";

  String photoField = "--" + boundary + "\r\n" +
    "Content-Disposition: form-data; name=\"photo\"; filename=\"capture.jpg\"\r\n" +
    "Content-Type: image/jpeg\r\n\r\n";

  String footer = "\r\n--" + boundary + "--\r\n";

  // Hitung total content length
  int contentLength = chatIdField.length() + captionField.length() +
                      photoField.length() + fb->len + footer.length();

  header += "Content-Length: " + String(contentLength) + "\r\n\r\n";

  // Kirim request
  client.print(header);
  client.print(chatIdField);
  client.print(captionField);
  client.write(fb->buf, fb->len);
  client.print(footer);

  // Baca response
  unsigned long timeout = millis();
  while (client.connected() && !client.available()) {
    if (millis() - timeout > 10000) {
      Serial.println("Timeout!");
      return false;
    }
    delay(10);
  }

  String response = client.readString();
  bool success = response.indexOf("\"ok\":true") > 0;
  Serial.printf("Telegram photo: %s\n", success ? "OK" : "FAIL");
  return success;
}

// Kirim text message
void sendTelegramMessage(String message) {
  HTTPClient http;
  String url = "https://api.telegram.org/bot" + String(BOT_TOKEN) +
               "/sendMessage?chat_id=" + String(CHAT_ID) +
               "&text=" + message;

  http.begin(url);
  int code = http.GET();
  Serial.printf("Telegram msg: %d\n", code);
  http.end();
}

// Poll untuk pesan dari Telegram (command handler)
void checkTelegramCommands() {
  HTTPClient http;
  static int lastUpdateId = 0;

  String url = "https://api.telegram.org/bot" + String(BOT_TOKEN) +
               "/getUpdates?offset=" + String(lastUpdateId + 1) +
               "&timeout=1";

  http.begin(url);
  int code = http.GET();

  if (code == 200) {
    String response = http.getString();
    // Parse dan handle commands
    if (response.indexOf("\"text\":\"/photo\"") > 0) {
      camera_fb_t *fb = esp_camera_fb_get();
      if (fb) {
        sendTelegramPhoto(fb);
        esp_camera_fb_return(fb);
      }
    } else if (response.indexOf("\"text\":\"/status\"") > 0) {
      String msg = "🔋 ESP32-CAM Status:\n";
      msg += "Free Heap: " + String(ESP.getFreeHeap()) + " bytes\n";
      msg += "Uptime: " + String(millis() / 1000) + "s\n";
      msg += "WiFi RSSI: " + String(WiFi.RSSI()) + " dBm";
      sendTelegramMessage(msg);
    }

    // Update offset
    int idx = response.lastIndexOf("\"update_id\":");
    if (idx > 0) {
      String idStr = response.substring(idx + 13);
      lastUpdateId = idStr.toInt();
    }
  }

  http.end();
}

8. Motion Detection & Alert

Selain face detection, kita juga bisa mendeteksi perubahan gerakan dengan membandingkan frame secara berurutan. Ini berguna untuk sistem keamanan yang mengirim foto ketika ada pergerakan.

C++ (Motion Detection)
#include "img_converters.h"
#include "fb_gfx.h"

// Motion detection dengan frame differencing
class MotionDetector {
  private:
    uint8_t *prevFrame;
    int frameSize;
    int threshold;
    unsigned long lastAlert;

  public:
    MotionDetector(int w, int h, int thresh = 30) {
      frameSize = w * h;
      prevFrame = (uint8_t *)malloc(frameSize);
      threshold = thresh;
      lastAlert = 0;
      memset(prevFrame, 0, frameSize);
    }

    // Deteksi motion dari frame grayscale
    bool detect(camera_fb_t *fb) {
      // Konversi ke grayscale
      uint8_t *gray = (uint8_t *)malloc(fb->width * fb->height);

      // Simplified: ambil setiap 3 byte (R) dari RGB
      for (int i = 0; i < frameSize && i * 3 < fb->len; i++) {
        gray[i] = fb->buf[i * 3];  // Ambil channel R saja
      }

      // Hitung perbedaan pixel
      int diffCount = 0;
      for (int i = 0; i < frameSize; i++) {
        int diff = abs((int)gray[i] - (int)prevFrame[i]);
        if (diff > threshold) {
          diffCount++;
        }
      }

      // Simpan frame sebelumnya
      memcpy(prevFrame, gray, frameSize);
      free(gray);

      // Hitung persentase perubahan
      float changePercent = (float)diffCount / frameSize * 100;
      bool motionDetected = changePercent > 5.0;  // 5% perubahan

      if (motionDetected) {
        Serial.printf("Motion: %.1f%% perubahan\n", changePercent);
      }

      return motionDetected;
    }

    // Cooldown: kirim alert tidak lebih sering dari 30 detik
    bool shouldAlert() {
      if (millis() - lastAlert > 30000) {
        lastAlert = millis();
        return true;
      }
      return false;
    }
};

// Penggunaan dalam loop
MotionDetector motion(320, 240, 30);

void loop() {
  server.handleClient();
  checkTelegramCommands();

  camera_fb_t *fb = esp_camera_fb_get();
  if (fb) {
    if (motion.detect(fb) && motion.shouldAlert()) {
      Serial.println("🚨 Motion detected!");
      sendTelegramPhoto(fb);
      sendTelegramMessage("🚨 Gerakan terdeteksi!");
    }
    esp_camera_fb_return(fb);
  }

  delay(200);  // Cek setiap 200ms
}

9. Optimasi Performa

💡 Tips Streaming Stabil

Untuk streaming yang stabil, gunakan resolusi VGA (640×480) dengan JPEG quality 15. Ini memberikan balance yang baik antara kualitas dan frame rate (~15 FPS). Jika hanya butuh face detection, QVGA (320×240) dengan quality 20 sudah cukup.

10. Quiz Pemahaman

Pertanyaan 1: Format streaming video apa yang paling cocok untuk ESP32-CAM?

a) H.264
b) MPEG-4
c) MJPEG
d) VP9

Pertanyaan 2: Mengapa ESP32-CAM memerlukan FTDI adapter?

a) Untuk power supply
b) Karena tidak punya USB-to-serial onboard
c) Untuk meningkatkan WiFi
d) Untuk koneksi kamera

Pertanyaan 3: Resolusi minimum untuk face detection real-time di ESP32?

a) UXGA (1600×1200)
b) VGA (640×480)
c) QVGA (320×240) atau lebih kecil
d) Resolusi tidak berpengaruh

Pertanyaan 4: Mengapa MJPEG lebih cocok dari H.264 untuk ESP32?

a) MJPEG lebih efisien dari H.264
b) H.264 memerlukan codec video yang berat
c) MJPEG mendukung audio
d) H.264 tidak mendukung WiFi

Pertanyaan 5: GPIO berapa yang digunakan LED flash pada ESP32-CAM AI Thinker?

a) GPIO 2
b) GPIO 4
c) GPIO 12
d) GPIO 33
← Sebelumnya Rust on ESP32 Selanjutnya → ESP32 Deep Sleep
🔍 Zoom
100%
🎨 Tema