IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Series ESP-WebRTC: Dự Án Two-Way Intercom — Gọi Video Hai Chiều ESP32 và Browser

Doorbell ở bài trước là một chiều (ESP32 → browser). Bài này xây intercom thực sự 2 chiều: cả hai phía nghe và thấy nhau, browser cũng gửi video về ESP32 nếu muốn (ví dụ màn hình LCD ở cửa hiển thị mặt nhà chủ).

Sự Khác Biệt Với Doorbell

Tính NăngVideo DoorbellTwo-Way Intercom
Video ESP32 → Browser
Audio ESP32 → Browser
Audio Browser → ESP32
Video Browser → ESP32✅ (tùy chọn)
Ai khởi tạo callESP32 (khi nút nhấn)Cả hai
Màn hình ở ESP32Không cầnHiển thị mặt đối diện

Architecture

ESP32-P4                              Browser
├── Camera → H.264 ──────────────────→ Video player
├── Mic → Opus ──────────────────────→ Speaker
├── Speaker ←─── Opus ───────────────  Mic
└── LCD ←── H.264 (optional) ─────────  Camera (optional)

WebRTC hỗ trợ sendrecv cho cả audio và video — cả hai chiều trong cùng một PeerConnection.

Cấu Hình WebRTC 2 Chiều

Điểm khác biệt chính trong code: thêm audio receive pipeline và LCD display.

esp_webrtc_cfg_t cfg = {
    .signaling = { /* ... */ },

    // Video: gửi từ ESP32 camera
    .video_send_codec = ESP_WEBRTC_VIDEO_CODEC_H264,

    // Video: nhận từ browser (hiển thị trên LCD)
    .video_recv_codec = ESP_WEBRTC_VIDEO_CODEC_H264,
    .video_recv_enable = true,

    // Audio 2 chiều
    .audio_send_codec = ESP_WEBRTC_AUDIO_CODEC_OPUS,
    .audio_recv_codec = ESP_WEBRTC_AUDIO_CODEC_OPUS,
    .audio_recv_enable = true,
};

AV-Render: Hiển Thị Video Từ Browser

Khi browser gửi video về, av_render decode và hiển thị trên LCD của ESP32-P4 EV Board:

av_render_cfg_t render_cfg = {
    .video = {
        .type        = AV_RENDER_VIDEO_LCD,
        .lcd_handle  = lcd_handle,   // Handle từ BSP
        .width       = 480,
        .height      = 270,          // Fit LCD của EV Board
        .pixel_fmt   = LCD_PIXEL_FORMAT_RGB565,
    },
    .audio = {
        .type           = AV_RENDER_AUDIO_I2S,
        .port           = I2S_NUM_1,
        .sample_rate    = 16000,
        .bits_per_sample = 16,
        .channel        = 1,
    },
};
av_render_init(&render_cfg, &g_render);

📷 [Hình minh hoạ: Ảnh demo: LCD ESP32-P4 hiển thị mặt người đang gọi từ browser]

Browser: Gửi Video + Audio

Browser cần permission camera và mic, rồi gửi cả hai chiều:

async function startIntercom() {
  // Xin quyền camera + mic
  const localStream = await navigator.mediaDevices.getUserMedia({
    video: { width: 640, height: 480, frameRate: 15 },
    audio: { echoCancellation: true, noiseSuppression: true }
  });

  // Hiển thị local video (preview)
  document.getElementById('localVideo').srcObject = localStream;

  const pc = new RTCPeerConnection({
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' },
      { urls: 'turn:YOUR_TURN_SERVER', username: 'user', credential: 'pass' }
    ]
  });

  // Thêm local stream vào PeerConnection
  localStream.getTracks().forEach(track => pc.addTrack(track, localStream));

  // Nhận stream từ ESP32
  pc.ontrack = event => {
    document.getElementById('remoteVideo').srcObject = event.streams[0];
  };

  // WHEP offer/answer
  pc.addTransceiver('video', { direction: 'sendrecv' });
  pc.addTransceiver('audio', { direction: 'sendrecv' });

  const offer = await pc.createOffer();
  await pc.setLocalDescription(offer);

  const resp = await fetch('http://SERVER:8889/intercom/whep', {
    method: 'POST',
    headers: { 'Content-Type': 'application/sdp' },
    body: offer.sdp
  });
  const answerSdp = await resp.text();
  await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp });

  return pc;
}

Echo Cancellation

Khi speaker và mic ở gần nhau, ESP32 sẽ thu tiếng từ speaker rồi gửi lại — gây echo khó chịu.

Browser tự xử lý echo cancellation (Web Audio API có built-in AEC). Phía ESP32 cần bật AEC trong config:

esp_capture_audio_cfg_t audio_cfg = {
    // ...
    .aec_enable  = true,   // Acoustic Echo Cancellation
    .ns_enable   = true,   // Noise Suppression
    .agc_enable  = true,   // Auto Gain Control
};

ESP32-P4 có hardware AEC — không tốn CPU.

Xử Lý Latency

Two-way intercom yêu cầu latency thấp hơn doorbell. Target <300ms end-to-end:

Thành PhầnLatency Typical
Capture audio10ms
Opus encode20ms (frame size 20ms)
Network (LAN)5ms
Network (WAN)30–80ms
Opus decode5ms
Audio output10ms
Tổng (LAN)~50ms
Tổng (WAN)~150–200ms

Giảm frame size Opus xuống 10ms để cut latency thêm 10ms.

Điều Khiển Từ ESP32: Mute, Volume

Thêm nút vật lý trên ESP32 để mute mic hoặc điều chỉnh volume speaker:

#define BTN_MUTE    GPIO_NUM_1
#define BTN_VOL_UP  GPIO_NUM_2
#define BTN_VOL_DN  GPIO_NUM_3

static bool g_muted = false;

void button_task(void *arg) {
    while (1) {
        if (gpio_get_level(BTN_MUTE) == 0) {
            g_muted = !g_muted;
            esp_webrtc_set_audio_mute(g_webrtc, g_muted);
            ESP_LOGI("intercom", "Mic %s", g_muted ? "MUTED" : "ACTIVE");
            vTaskDelay(pdMS_TO_TICKS(200));  // Debounce
        }
        vTaskDelay(pdMS_TO_TICKS(50));
    }
}

Nhiều Đơn Vị Intercom

esp-webrtc hỗ trợ nhiều PeerConnection đồng thời — bạn có thể có nhiều ESP32 intercom trong nhà:

ESP32 cổng chính  ─┐
ESP32 garage      ─┤→ MediaMTX → Browser (quản lý tất cả)
ESP32 tầng 2      ─┘

Mỗi ESP32 kết nối một WHIP endpoint khác nhau: /front-gate/whip, /garage/whip, /2nd-floor/whip.

Bài tiếp theo: Dự Án USB Camera Bridge — Biến ESP32 Thành WebCam Online