ESP32-CAM 網路影像串流,初學者完整教學
把 ESP32-CAM 開發板連上你家的 Wi-Fi,然後用手機或電腦的瀏覽器,就能即時看到攝影機的畫面。不需要安裝任何 App,只要在同一個 Wi-Fi 底下,打開網頁就能看。
把 ESP32-CAM 開發板連上你家的 Wi-Fi,然後用手機或電腦的瀏覽器,就能即時看到攝影機的畫面。不需要安裝任何 App,只要在同一個 Wi-Fi 底下,打開網頁就能看。
一、你需要準備的東西
這篇範例只需要一片最常見的ESP32-CAM 開發板,AI-Thinker或相容板,已搭載 OV2640 攝影機模組。先不要用其它型號的ESP32內建鏡頭的開發板,那些開發板的腳位不同,傑森會另外介紹。

上傳程式有兩種方式
ESP32-CAM 本身沒有 USB 接頭,所以上傳程式需要額外的工具。依你手上有什麼,選一種方式:
方式 A:使用上傳底板(推薦初學者)
上傳底板是一塊小小的轉接板,把 ESP32-CAM 直接插上去,再用 Micro USB 線接電腦就好。不需要任何接線,插上去就能用。



方式 B:使用 FTDI USB-TTL 轉接器
如果你沒有上傳底板,可以用 FTDI(或 CP2102、CH340 等 USB 轉 TTL 模組)來上傳。但需要自己接線,而且上傳前要手動把 GPIO0 接到 GND。


二、安裝 Arduino IDE 開發環境
Arduino IDE 設定
安裝開發板:如果之前已使用過ESP32開發板,就不用重複進行了。若是第一次,請參考我們另一篇專文教學哦!

步驟 4:選擇正確的開發板設定
在 Arduino IDE 的 工具 選單中,做以下設定:
| 設定項目 | 選擇 |
|---|---|
| Board(開發板) | AI Thinker ESP32-CAM |
| Partition Scheme | Huge APP (3MB No OTA/1MB SPIFFS) |
| Upload Speed | 115200(比較穩定,初學者建議用這個) |
| Port | 選擇你的 COM Port(接上 USB 後會出現) |
⚠️ Partition Scheme 一定要改成 Huge APP,否則程式太大會上傳失敗。
三、接線與上傳
方式 A:使用上傳底板
- 把 ESP32-CAM 插到上傳底板上(注意方向,有攝影機的那面朝外)
- 用 Micro USB 線接上電腦
- Arduino IDE 選好 Port
- 直接按「上傳」就好
如果上傳時卡在 Connecting...,試著按住底板上的 IO0 按鈕,再按一下 Reset,然後放開 IO0,這會強制進入下載模式。方式 B:使用 FTDI USB-TTL
需要用杜邦線手動接線:
| ESP32-CAM | FTDI 模組 |
|---|---|
| U0T(GPIO1) | RX |
| U0R(GPIO3) | TX |
| GND | GND |
| 5V | VCC(5V) |

重要:上傳程式前,要把 ESP32-CAM 的 GPIO0 用杜邦線接到 GND。 這是告訴 ESP32「我要進入下載模式」的訊號。
上傳步驟:
- 接好線,GPIO0 接 GND
- 在 Arduino IDE 點上傳
- 看到下方出現
Connecting...時,按一下Reset - 等待上傳完成
- 上傳完成後,把 GPIO0 從 GND 斷開
- 按一下 Reset,程式就會開始執行
⚠️ 如果上傳完忘記斷開 GPIO0,ESP32 會一直停在下載模式,程式不會執行。
四、程式碼
在 Arduino IDE 中建立新檔案,貼上以下程式碼。只需要修改前兩行的 Wi-Fi 名稱和密碼,其他不用動。
這支範例傑森已極度精簡,只有一項功能,就是可以在瀏覽器看到鏡頭拍到的串流影像。
#include "esp_camera.h" // ESP32 攝影機函式庫
#include <WiFi.h> // Wi-Fi 連線函式庫
#include "esp_http_server.h" // HTTP 伺服器函式庫
// ===== 請改成你自己的 Wi-Fi 名稱和密碼 =====
const char* ssid = "你的WiFi名稱";
const char* password = "你的WiFi密碼";
// ===== 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
// ===== MJPEG 串流格式定義(HTTP 多段傳輸用,不用改)=====
#define PART_BOUNDARY "123456789000000000000987654321"
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\r\n--" PART_BOUNDARY "\r\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\r\nContent-Length: %u\r\n\r\n";
// 兩個伺服器:一個放網頁(port 80),一個放串流(port 81)
httpd_handle_t stream_httpd = NULL;
httpd_handle_t page_httpd = NULL;
// ========== 串流處理:不斷擷取畫面並傳給瀏覽器 ==========
static esp_err_t stream_handler(httpd_req_t *req) {
camera_fb_t *fb = NULL; // fb = frame buffer,存放一幀畫面
char part_buf[64];
// 設定回傳格式為 MJPEG 串流
esp_err_t res = httpd_resp_set_type(req, _STREAM_CONTENT_TYPE);
if (res != ESP_OK) return res;
// 無限迴圈:持續拍照 → 傳送 → 拍照 → 傳送...
while (true) {
fb = esp_camera_fb_get(); // 從攝影機取得一幀 JPEG 圖片
if (!fb) { res = ESP_FAIL; break; } // 拍照失敗就跳出
// 把這幀圖片用 HTTP chunk 方式傳給瀏覽器
size_t hlen = snprintf(part_buf, 64, _STREAM_PART, fb->len);
res = httpd_resp_send_chunk(req, _STREAM_BOUNDARY, strlen(_STREAM_BOUNDARY));
if (res == ESP_OK) res = httpd_resp_send_chunk(req, part_buf, hlen);
if (res == ESP_OK) res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len);
esp_camera_fb_return(fb); // 用完歸還 buffer
if (res != ESP_OK) break; // 傳送失敗(瀏覽器關閉)就停止
}
return res;
}
// ========== 首頁:顯示標題和嵌入串流畫面 ==========
static esp_err_t index_handler(httpd_req_t *req) {
// 這段是完整的 HTML 網頁,會顯示在瀏覽器上
const char html[] = R"rawliteral(
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ESP32-CAM 網路串流 - 傑森創工</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#111;color:#eee;font-family:Arial,sans-serif;text-align:center}
h1{font-size:20px;padding:8px 0 2px;color:#e94560}
.sub{font-size:12px;color:#888;padding-bottom:6px}
img{width:100%;max-width:640px;display:block;margin:0 auto;border-radius:6px}
</style>
</head>
<body>
<h1>ESP32-CAM 網路串流</h1>
<p class="sub">傑森創工</p>
<img id="stream" src="">
<script>
// 自動偵測 ESP32-CAM 的 IP,接上 port 81 的串流網址
var h=location.hostname;
document.getElementById('stream').src='http://'+h+':81/stream';
</script>
</body>
</html>
)rawliteral";
httpd_resp_set_type(req, "text/html");
return httpd_resp_send(req, html, strlen(html));
}
void setup() {
Serial.begin(115200);
// ===== 攝影機硬體設定(腳位對應,固定的)=====
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 = 10000000; // 時鐘頻率 10MHz(降低可減少條紋)
config.pixel_format = PIXFORMAT_JPEG; // 輸出格式為 JPEG
config.frame_size = FRAMESIZE_QVGA; // 解析度 320x240(小但流暢)
config.jpeg_quality = 12; // JPEG 品質 0~63,數字越小畫質越好
config.fb_count = psramFound() ? 2 : 1; // 有 PSRAM 用雙緩衝,串流更順
// 啟動攝影機,失敗就停在這裡
if (esp_camera_init(&config) != ESP_OK) {
Serial.println("攝影機啟動失敗!請檢查排線");
return;
}
Serial.println("攝影機啟動成功");
// ===== 連接 Wi-Fi =====
WiFi.begin(ssid, password);
WiFi.setSleep(false); // 關閉省電模式,串流更穩定
Serial.print("正在連接 Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi 已連線!");
// ===== 啟動 Port 80 網頁伺服器(顯示有標題的控制頁面)=====
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
cfg.server_port = 80;
httpd_uri_t index_uri = { .uri = "/", .method = HTTP_GET, .handler = index_handler, .user_ctx = NULL };
if (httpd_start(&page_httpd, &cfg) == ESP_OK) {
httpd_register_uri_handler(page_httpd, &index_uri);
}
// ===== 啟動 Port 81 串流伺服器(提供 MJPEG 影像串流)=====
cfg.server_port = 81;
cfg.ctrl_port += 1;
httpd_uri_t stream_uri = { .uri = "/stream", .method = HTTP_GET, .handler = stream_handler, .user_ctx = NULL };
if (httpd_start(&stream_httpd, &cfg) == ESP_OK) {
httpd_register_uri_handler(stream_httpd, &stream_uri);
}
// 印出網址,複製貼到瀏覽器就能看到畫面
Serial.println("=========================================");
Serial.printf(" 開啟網頁:http://%s\n", WiFi.localIP().toString().c_str());
Serial.printf(" 純串流: http://%s:81/stream\n", WiFi.localIP().toString().c_str());
Serial.println("=========================================");
}
// loop 裡不需要做任何事,伺服器在背景自動運作
void loop() {
delay(10000);
}
五、上傳程式並測試
- 把程式碼中的 Wi-Fi 名稱和密碼改成你自己的
- 按照第三節的方式接線並上傳
- 上傳成功後,開啟 Arduino IDE 的 Serial Monitor
- 右下角鮑率選 115200
- 按一下 ESP32-CAM 的 Reset 按鈕
- 你會看到類似這樣的訊息:
攝影機啟動成功
正在連接 Wi-Fi....
Wi-Fi 已連線!
=========================================
開啟網頁:http://192.168.1.105
純串流: http://192.168.1.105:81/stream
=========================================
- 用手機或電腦的瀏覽器,輸入「開啟網頁」那行的網址
- 你就能看到有標題的即時影像串流頁面了!
⚠️ 手機和電腦必須跟 ESP32-CAM 連在同一個 Wi-Fi 下才能看到畫面。

六、常見問題排除
| 問題 | 解決方法 |
|---|---|
| 上傳時卡在 Connecting... | 使用上傳底板:按住 IO0 → 按 Reset → 放開 IO0。使用 FTDI:確認 GPIO0 有接 GND,重試放 Reset 的時機 |
| Serial Monitor 顯示亂碼 | 鮑率選錯了,改成 115200 |
| 攝影機啟動失敗 | 攝影機排線鬆了,打開卡扣重新壓好再扣回去 |
| 一直顯示 Connecting Wi-Fi | Wi-Fi 名稱或密碼打錯了,注意大小寫 |
| 畫面有水平閃動條紋 | 通常是供電不足,換用 5V/2A 行動電源供電 |
| 反覆重啟 | 供電不足觸發 brownout 保護,換更穩定的電源 |
| 網頁打不開 | 確認手機/電腦和 ESP32-CAM 在同一個 Wi-Fi 下 |
| 上傳後程式沒有執行 | 使用 FTDI 的人:上傳完要把 GPIO0 從 GND 斷開,再按 Reset |
七、想改解析度?
程式碼裡的 config.frame_size 可以改成其他值:
| 常數 | 解析度 | 說明 |
|---|---|---|
| FRAMESIZE_QVGA | 320×240 | 目前使用的,串流最流暢 |
| FRAMESIZE_CIF | 400×296 | 稍大一點 |
| FRAMESIZE_VGA | 640×480 | 畫質和速度平衡 |
| FRAMESIZE_SVGA | 800×600 | 畫質不錯但串流會變慢 |
| FRAMESIZE_XGA | 1024×768 | 適合拍照,串流會卡 |
| FRAMESIZE_UXGA | 1600×1200 | 最大解析度,只適合拍照 |
解析度越高,畫面更清楚但串流速度越慢,而且越吃電。初學者建議從 QVGA 或 VGA 開始。
八、原理簡單說明
這個範例做了三件事:
- 啟動攝影機 — 設定好腳位和參數,讓 OV2640 攝影機模組開始工作
- 連上 Wi-Fi — ESP32 連上你家的路由器,取得一個區域網路 IP
- 開兩個伺服器 — Port 80 提供有標題的網頁,Port 81 提供 MJPEG 影像串流。網頁裡用一個
<img>標籤指向 Port 81 的串流網址,瀏覽器就會自動持續接收並顯示新畫面
所謂 MJPEG(Motion JPEG),就是不斷地拍一張 JPEG → 傳給瀏覽器 → 再拍一張 → 再傳,快速連續播放就變成「影片」了。跟真正的影片編碼(如 H.264)不同,MJPEG 不做幀間壓縮,所以比較吃頻寬,但好處是程式簡單、不需要額外的解碼器。
這個範例只是開始,但只要會用串流之後就不難囉!傑森還會上架其它相關教學,請大家期待吧^^




