ESP32-CAM 实战教程:手机连 Wi-Fi 查看网页摄像头,并采集声音
本文由 AI 协助整理,代码和验证步骤适用于学习记录。不同 ESP32-CAM 板型、摄像头模组、麦克风模块和库版本可能导致引脚与配置差异,请以实际硬件原理图为准。

这次想做什么
这篇准备做一个更有画面感的小项目:用 ESP32-CAM 采集摄像头画面,让手机连接 ESP32 自己开的 Wi-Fi 热点,然后在手机浏览器里打开网页查看实时画面。同时再接一个 I2S 数字麦克风,做声音采集,至少能在网页上看到音量变化,后续再扩展成短音频片段或 WebSocket 音频流。
先把话说在前面:ESP32-CAM 很适合做网页摄像头入门项目,但它不是手机,也不是专业网络摄像机。视频可以通过 MJPEG 流比较顺利地在浏览器里显示;声音如果要做到真正的实时播放、音画同步、低延迟、抗卡顿,就会牵涉编码、缓冲、浏览器播放策略和网络带宽。学习阶段更稳妥的路线是:第一步先跑通视频流,第二步接入 I2S 麦克风,第三步把音量或短 PCM 采样展示出来。这样既能体现“视频 + 声音采集”,又不至于把 ESP32 按在地上要求它表演手机直播。
这类项目很适合拿来写博客,因为它有一个清楚的使用场景:手机不用安装 App,连接设备 Wi-Fi,打开浏览器就能看画面。它也有足够多的坑:供电不足、烧录模式、摄像头排线、PSRAM、分辨率、帧率、I2S 引脚冲突、手机浏览器缓存。踩坑素材多得很,文章自然就不会瘦得像刚出厂的杜邦线。
硬件准备
推荐硬件如下:
| 模块 | 用途 |
|---|---|
| ESP32-CAM AI Thinker | 摄像头采集与网页服务 |
| OV2640 摄像头 | ESP32-CAM 常见摄像头模组 |
| USB 转 TTL 模块 | 给 ESP32-CAM 烧录程序 |
| INMP441 或 SPH0645 | I2S 数字麦克风 |
| 稳定 5V 电源 | 给 ESP32-CAM 供电 |
| 手机 | 连接 Wi-Fi 并打开网页查看 |
ESP32-CAM 最大的特点是小巧、便宜、资料多;最大的脾气是供电和引脚。它不像普通 DevKit 那样直接插 USB 就能烧录,通常需要 USB 转 TTL 模块。烧录时 GPIO0 接 GND,复位或重新上电后进入下载模式;程序烧录完成后,要断开 GPIO0 与 GND,再复位运行。这个步骤如果忘了,板子会像一个坚持留在考场不肯交卷的学生,一直待在下载模式里。
声音采集建议用 I2S 数字麦克风,例如 INMP441。模拟麦克风模块当然也能读,但 ESP32-CAM 上可用引脚紧张,模拟采样又容易受噪声影响。I2S 麦克风输出数字音频,结构更清楚,写文章也更容易解释。

引脚连接建议
如果使用 ESP32-CAM AI Thinker,并且本文方案不使用 SD 卡,可以参考下面的 I2S 麦克风连接:
| I2S 麦克风 | ESP32-CAM |
|---|---|
| VCC | 3V3 |
| GND | GND |
| SCK / BCLK | GPIO14 |
| WS / LRCL | GPIO15 |
| SD / DOUT | GPIO13 |
这里必须提醒一句:GPIO13、GPIO14、GPIO15 在很多 ESP32-CAM 板子上也和 SD 卡相关。本文默认不使用 SD 卡,把这些脚让给麦克风。不同板型不要照抄,一定先查原理图。摄像头项目一旦引脚冲突,表现往往不是很礼貌,它可能初始化失败,也可能偶尔成功,然后在你准备截图时突然罢工。
供电方面,建议用稳定 5V。很多 ESP32-CAM 的失败不是代码问题,而是摄像头启动瞬间电流上来以后电压掉下去。串口里如果反复出现 Brownout、重启、乱码,先别急着怀疑人生,先换电源、换线、降低分辨率。嵌入式世界里,“线不行”是一个朴素但杀伤力很强的答案。
访问方式设计
本文采用 SoftAP 模式,也就是 ESP32-CAM 自己开一个 Wi-Fi 热点。
手机操作流程:
- 给 ESP32-CAM 上电。
- 手机搜索 Wi-Fi,连接
ESP32-CAM-AV。 - 打开浏览器,访问
http://192.168.4.1。 - 页面显示摄像头视频流和声音音量状态。
这种方式的好处是不用家里路由器,也不用先配网。拿出去演示时,只要手机能连热点就行。缺点是手机连上 ESP32 热点后,可能暂时不能访问互联网;有些手机还会提示“此 Wi-Fi 无法上网”,这不是故障,只是因为 ESP32-CAM 不是宽带路由器,不负责带你刷短视频。
后续如果想接入家庭网络,可以改成 STA 模式,让 ESP32-CAM 连接路由器,然后手机在同一局域网访问它的 IP。学习阶段我更推荐先用 SoftAP,因为变量少,路由器、防火墙、IP 变化这些杂事先不进场。
视频流的基本思路
网页视频流常见做法是 MJPEG。简单说,就是服务器不断发送一张又一张 JPEG 图片,浏览器看起来就像视频。它不如 H.264 那样省带宽,也不适合高质量长时间监控,但胜在实现简单,浏览器支持也方便。
ESP32-CAM 常见示例里会有两个接口:
GET / 返回网页
GET /stream 返回 MJPEG 视频流
网页里只需要放一个图片标签:
<img src="/stream" />
浏览器会持续请求 /stream,服务器一帧一帧往外推 JPEG。看起来很神奇,本质上有点像一个人飞快翻动连环画。只要帧率别太高、分辨率别太大,ESP32-CAM 是能扛住入门演示的。
分辨率建议从 QVGA 或 VGA 开始,不要一上来就追求 UXGA。先跑通,再调清晰度。很多项目失败,就是因为一开始就把画质拉满,结果内存、带宽、供电一起抗议。
声音采集的基本思路
I2S 麦克风采集到的是一串音频采样。最简单的做法不是立刻在网页里播放声音,而是先计算声音强度,也就是 RMS 或平均绝对值,然后在网页上显示一个音量条。这样可以验证麦克风是否工作,也能避免一开始就陷入浏览器音频播放、缓存和延迟问题。
声音采集可以拆成三步:
- 初始化 I2S。
- 周期读取一小段采样。
- 计算音量值,通过
/audio-level或 WebSocket 给网页。
如果后续想听到声音,可以把 PCM 数据打包给浏览器,用 WebAudio 播放。但这就是进阶内容了。要处理采样率、缓冲队列、播放时钟和网络抖动。别看“听到声音”这四个字很短,它背后可不是四行代码。
代码骨架
下面给一个学习版结构,重点是看清模块分工。摄像头引脚配置可以参考 Arduino ESP32 自带的 CameraWebServer 示例,板型选择 AI Thinker ESP32-CAM。
#include "esp_camera.h"
#include <WiFi.h>
#include <WebServer.h>
#include <driver/i2s.h>
const char* AP_SSID = "ESP32-CAM-AV";
const char* AP_PASS = "12345678";
WebServer server(80);
volatile int audioLevel = 0;
void setupCamera() {
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 5;
config.pin_d1 = 18;
config.pin_d2 = 19;
config.pin_d3 = 21;
config.pin_d4 = 36;
config.pin_d5 = 39;
config.pin_d6 = 34;
config.pin_d7 = 35;
config.pin_xclk = 0;
config.pin_pclk = 22;
config.pin_vsync = 25;
config.pin_href = 23;
config.pin_sccb_sda = 26;
config.pin_sccb_scl = 27;
config.pin_pwdn = 32;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
if (psramFound()) {
config.frame_size = FRAMESIZE_VGA;
config.jpeg_quality = 12;
config.fb_count = 2;
} else {
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 15;
config.fb_count = 1;
}
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("camera init failed: 0x%x\n", err);
}
}
I2S 麦克风初始化可以这样组织:
void setupI2SMic() {
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = 16000,
.bits_per_sample = I2S_BITS_PER_SAMPLE_32BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_STAND_I2S,
.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,
.dma_buf_count = 4,
.dma_buf_len = 256,
.use_apll = false,
.tx_desc_auto_clear = false,
.fixed_mclk = 0
};
i2s_pin_config_t pin_config = {
.bck_io_num = 14,
.ws_io_num = 15,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = 13
};
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pin_config);
}
读取声音强度:
void updateAudioLevel() {
int32_t samples[256];
size_t bytesRead = 0;
i2s_read(I2S_NUM_0, samples, sizeof(samples), &bytesRead, 0);
int count = bytesRead / sizeof(int32_t);
if (count <= 0) return;
long long sum = 0;
for (int i = 0; i < count; i++) {
int32_t v = samples[i] >> 14;
sum += abs(v);
}
audioLevel = sum / count;
}
网页接口可以先做得朴素一点:
void handleRoot() {
server.send(200, "text/html",
"<!doctype html><html><head><meta name='viewport' content='width=device-width,initial-scale=1'>"
"<style>body{font-family:sans-serif;background:#111;color:#eee;text-align:center}"
"img{width:100%;max-width:640px;border:1px solid #444}"
".bar{width:90%;height:24px;background:#333;margin:16px auto}.fill{height:100%;background:#22c55e;width:0}</style>"
"</head><body><h2>ESP32-CAM AV</h2><img src='/stream'>"
"<div class='bar'><div id='v' class='fill'></div></div>"
"<script>setInterval(async()=>{let r=await fetch('/audio-level');let n=await r.text();"
"document.getElementById('v').style.width=Math.min(100,Number(n)/100)+'%';},500)</script>"
"</body></html>");
}
void handleAudioLevel() {
server.send(200, "text/plain", String(audioLevel));
}
这里没有把 /stream 的完整实现贴满,是因为 Arduino ESP32 的 CameraWebServer 示例已经提供了成熟的 MJPEG 处理逻辑。学习时建议先跑通官方示例,再把网页和音量接口加进去。不要一上来就手写完整流处理,否则你会在 multipart 边界、HTTP 头和帧缓冲里来回迷路。
主程序结构大致如下:
void setup() {
Serial.begin(115200);
setupCamera();
setupI2SMic();
WiFi.softAP(AP_SSID, AP_PASS);
Serial.print("AP IP: ");
Serial.println(WiFi.softAPIP());
server.on("/", handleRoot);
server.on("/audio-level", handleAudioLevel);
// server.on("/stream", handleJpgStream);
server.begin();
}
void loop() {
server.handleClient();
updateAudioLevel();
}
手机端验证
烧录完成后,按这个顺序验证:
- 断开 GPIO0 与 GND,重新上电。
- 打开串口监视器,确认摄像头初始化成功。
- 手机连接 Wi-Fi:
ESP32-CAM-AV。 - 浏览器访问
http://192.168.4.1。 - 页面能看到摄像头画面。
- 对着麦克风说话或拍手,网页音量条应有明显变化。
如果视频能看但音量没变化,先检查 I2S 引脚和麦克风供电。INMP441 有些模块会标 L/R 或 SEL,它决定左右声道,代码里如果只读左声道,模块却输出在右声道,读数就会像在认真听空气。可以尝试切换 I2S_CHANNEL_FMT_ONLY_LEFT 和 I2S_CHANNEL_FMT_ONLY_RIGHT。
如果手机连上 Wi-Fi 但打不开网页,先看手机是否自动切回了有互联网的 Wi-Fi。有些手机会觉得 ESP32 热点不能上网,于是热心地帮你换回家庭 Wi-Fi。它的出发点很好,但对调试来说有点添乱。
常见问题
摄像头初始化失败:检查排线方向、板型配置、供电和 PSRAM。AI Thinker 的引脚配置不要随便套到其他板子上。
画面很卡:降低分辨率,先用 QVGA 或 VGA;同时减少网页轮询频率。ESP32-CAM 做演示很合适,但别拿它和专业摄像头比肌肉。
反复重启:优先检查 5V 供电能力和 USB 线。摄像头启动瞬间的电流会让弱电源露馅。
麦克风无数据:检查 I2S 三根信号线、声道配置、模块供电,以及是否和 SD 卡引脚冲突。
手机提示无互联网:正常。本文方案是手机直连 ESP32 热点,不是让 ESP32 当路由器。
想保存视频或音频:可以先保存单张 JPEG 或短 PCM 片段,别急着做长时间录像。ESP32-CAM 的存储、带宽和稳定性都需要单独设计。
可以继续优化的方向
第一,改成家庭 Wi-Fi 模式。设备连接路由器后,手机访问串口里打印的 IP。这样手机还能同时上网,但需要处理配网问题。
第二,加入 WebSocket。音量值和短音频片段通过 WebSocket 推送,比网页定时 fetch 更顺滑。只是代码复杂度会上来,适合在视频流跑稳之后再做。
第三,增加鉴权。摄像头画面属于敏感信息,哪怕只是学习项目,也不要长期裸奔在开放网络里。至少给热点设置密码,局域网访问也可以加简单登录。
第四,加入补光灯控制。ESP32-CAM 常带闪光 LED,但 GPIO4 又常和板载 LED、SD 卡等资源有关,使用前要确认冲突。补光灯一开,画面是亮了,供电压力也跟着上来了。
本篇小结
这篇的核心目标不是打造一个完美的网络摄像机,而是把 ESP32-CAM 的几个关键能力串起来:摄像头采集、网页视频流、SoftAP 热点、手机访问、I2S 声音采集。视频部分用 MJPEG 流实现,声音部分先用音量条验证采集链路,后续再扩展到 WebSocket 或 WebAudio。
做 ESP32-CAM 项目时,最重要的不是背 API,而是保持排错顺序:先供电,再烧录,再摄像头初始化,再 Wi-Fi,再网页,再麦克风。一步一步来,设备就比较愿意配合。要是所有模块一起上,失败时就像一桌人同时说话,热闹是热闹,谁也听不清。
打赏
如果这篇学习记录对你有帮助,欢迎扫码支持后续整理。

打赏支持:王庆岩
浙公网安备 33010602011771号