云计算百科
云计算领域专业知识百科平台

新手零门槛搭建 ESP32 + OV3660 摄像头 MJPEG 流服务器(ESP-IDF v5.1.6 适配版)

前言

本文专为零基础新手打造,从硬件选型与接线、开发环境准备、代码编写与编译到最终访问摄像头流,全程保姆级教程。基于 ESP-IDF v5.1.6 开发,完美适配 OV3660 摄像头与微软 Edge 浏览器,代码可直接复制使用,一次性解决「编译报错」「SOI 标记缺失」「Edge 无法解析流」等核心问题。

一、核心目标

用 ESP32-CAM 开发板 + OV3660 摄像头,搭建一个 MJPEG 视频流服务器,通过 WiFi 连接后,在浏览器中输入 ESP32 局域网 IP,即可实时查看摄像头画面。

二、硬件架构与选型

1. 核心硬件清单(新手必买,无兼容问题)

硬件名称规格要求作用
ESP32-CAM 开发板 搭载 ESP32-WROOM-32 核心,无 PSRAM 版本即可 核心控制单元,负责 WiFi 通信、摄像头驱动、HTTP 服务器运行
OV3660 摄像头模块 适配 ESP32-CAM 引脚,自带排线 图像采集,输出 JPEG 格式图像
ESP32-CAM 专用烧录座 适配 ESP32-CAM 引脚,支持一键烧录 替代 USB 转 TTL,简化烧录流程,无需手动接 IO0
USB 数据线 Micro USB 接口,适配烧录座 给烧录座供电,传输烧录数据

2. 硬件架构原理

#mermaid-svg-4BmQsvjQuGGxEK48{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-4BmQsvjQuGGxEK48 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4BmQsvjQuGGxEK48 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4BmQsvjQuGGxEK48 .error-icon{fill:#552222;}#mermaid-svg-4BmQsvjQuGGxEK48 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4BmQsvjQuGGxEK48 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4BmQsvjQuGGxEK48 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4BmQsvjQuGGxEK48 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4BmQsvjQuGGxEK48 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4BmQsvjQuGGxEK48 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4BmQsvjQuGGxEK48 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4BmQsvjQuGGxEK48 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4BmQsvjQuGGxEK48 .marker.cross{stroke:#333333;}#mermaid-svg-4BmQsvjQuGGxEK48 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4BmQsvjQuGGxEK48 p{margin:0;}#mermaid-svg-4BmQsvjQuGGxEK48 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-4BmQsvjQuGGxEK48 .cluster-label text{fill:#333;}#mermaid-svg-4BmQsvjQuGGxEK48 .cluster-label span{color:#333;}#mermaid-svg-4BmQsvjQuGGxEK48 .cluster-label span p{background-color:transparent;}#mermaid-svg-4BmQsvjQuGGxEK48 .label text,#mermaid-svg-4BmQsvjQuGGxEK48 span{fill:#333;color:#333;}#mermaid-svg-4BmQsvjQuGGxEK48 .node rect,#mermaid-svg-4BmQsvjQuGGxEK48 .node circle,#mermaid-svg-4BmQsvjQuGGxEK48 .node ellipse,#mermaid-svg-4BmQsvjQuGGxEK48 .node polygon,#mermaid-svg-4BmQsvjQuGGxEK48 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4BmQsvjQuGGxEK48 .rough-node .label text,#mermaid-svg-4BmQsvjQuGGxEK48 .node .label text,#mermaid-svg-4BmQsvjQuGGxEK48 .image-shape .label,#mermaid-svg-4BmQsvjQuGGxEK48 .icon-shape .label{text-anchor:middle;}#mermaid-svg-4BmQsvjQuGGxEK48 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4BmQsvjQuGGxEK48 .rough-node .label,#mermaid-svg-4BmQsvjQuGGxEK48 .node .label,#mermaid-svg-4BmQsvjQuGGxEK48 .image-shape .label,#mermaid-svg-4BmQsvjQuGGxEK48 .icon-shape .label{text-align:center;}#mermaid-svg-4BmQsvjQuGGxEK48 .node.clickable{cursor:pointer;}#mermaid-svg-4BmQsvjQuGGxEK48 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4BmQsvjQuGGxEK48 .arrowheadPath{fill:#333333;}#mermaid-svg-4BmQsvjQuGGxEK48 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4BmQsvjQuGGxEK48 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4BmQsvjQuGGxEK48 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4BmQsvjQuGGxEK48 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4BmQsvjQuGGxEK48 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4BmQsvjQuGGxEK48 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4BmQsvjQuGGxEK48 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4BmQsvjQuGGxEK48 .cluster text{fill:#333;}#mermaid-svg-4BmQsvjQuGGxEK48 .cluster span{color:#333;}#mermaid-svg-4BmQsvjQuGGxEK48 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-4BmQsvjQuGGxEK48 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4BmQsvjQuGGxEK48 rect.text{fill:none;stroke-width:0;}#mermaid-svg-4BmQsvjQuGGxEK48 .icon-shape,#mermaid-svg-4BmQsvjQuGGxEK48 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4BmQsvjQuGGxEK48 .icon-shape p,#mermaid-svg-4BmQsvjQuGGxEK48 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4BmQsvjQuGGxEK48 .icon-shape .label rect,#mermaid-svg-4BmQsvjQuGGxEK48 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4BmQsvjQuGGxEK48 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4BmQsvjQuGGxEK48 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4BmQsvjQuGGxEK48 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

图像采集

数据传输

处理核心JPEG 校验、WiFi 通信、HTTP 服务器

网络中转 无线连接

解析 MJPEG 流并显示画面

OV3660 摄像头

ESP32-CAM 引脚

ESP32 核心

家庭 WiFi 路由器

浏览器 Edge/Chrome

显示画面

3. 硬件实物图(新手直观参考)

以下是 ESP32-CAM + OV3660 + 烧录座的实物图,新手可直接对照购买: 在这里插入图片描述

4. 硬件接线(使用烧录座,零难度)

(1)ESP32-CAM 与烧录座接线
  • 将 ESP32-CAM 开发板金手指朝下,插入烧录座的对应插槽;
  • 确保 ESP32-CAM 与烧录座引脚完全对齐,无偏移。
  • (2)烧录座与 USB 数据线接线

    直接将 Micro USB 数据线插入烧录座的 Micro USB 接口,另一端插入电脑 USB 口即可(无需额外接线)。

    (3)ESP32-CAM 与 OV3660 接线

    直接将 OV3660 自带的排线插入 ESP32-CAM 的摄像头接口即可(注意排线方向,金手指朝向开发板正面)。

    三、开发环境准备(ESP-IDF v5.1.6)

    已安装 ESP-IDF v5.1.6 的新手可跳过此步骤,直接进入「四、项目搭建」。

    1. 下载并安装 ESP-IDF Tools

  • 访问 ESP 官方下载页:https://dl.espressif.com/dl/esp-idf/
  • 下载 ESP-IDF v5.1.6 对应的 Windows 安装包(esp-idf-tools-setup-5.1.6.exe);
  • 双击安装,一路默认下一步,记住安装路径(例如 D:\\ESP32\\esp-idf\\v5.1.6)。
  • 2. 配置 VS Code 开发环境

  • 安装 VS Code,在扩展商店搜索并安装 Espressif IDF 插件(作者:Espressif Systems);
  • 打开 VS Code,按 Ctrl+Shift+P,输入 ESP-IDF: Configure ESP-IDF Extension;
  • 选择「Use an existing ESP-IDF directory」,找到并选择刚才安装的 ESP-IDF v5.1.6 路径;
  • 等待插件配置完成,底部状态栏显示「ESP-IDF: 5.1.6」即成功。
  • 四、新手重建项目(核心步骤)

    步骤 1:创建新项目

  • 打开 VS Code,按 Ctrl+Shift+P,输入 ESP-IDF: New Project;
  • 项目名称填写 web-camera-ov3660,保存路径自定义(例如 D:\\ESP32\\web-camera-ov3660);
  • 模板选择 esp32 → blank(空白项目),点击「Create」。
  • 步骤 2:创建核心文件

    在项目的 main 目录下,删除默认的 main.c,新建以下 6 个文件: app_main.c、app_wifi.c、app_wifi.h、app_camera.c、app_camera.h、app_httpd.c、app_httpd.h。

    步骤 3:复制代码(直接粘贴,无需修改)

    所有代码已适配 ESP-IDF v5.1.6,解决「SOI 标记缺失」「Edge 兼容性」「编译报错」问题。

    1. app_main.c(程序入口)

    #include "esp_log.h"
    #include "nvs_flash.h"
    #include "app_wifi.h"
    #include "app_camera.h"
    #include "app_httpd.h"

    static const char *TAG = "MAIN";

    void app_main(void)
    {
    // 1. 初始化 NVS(WiFi 依赖)
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    ESP_ERROR_CHECK(nvs_flash_erase());
    err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);
    ESP_LOGI(TAG, "NVS 初始化成功");

    // 2. 初始化 WiFi(直接调用,无返回值)
    wifi_init_sta();
    ESP_LOGI(TAG, "WiFi 初始化完成");

    // 3. 初始化 OV3660 摄像头
    err = app_camera_main();
    if (err != ESP_OK) {
    ESP_LOGE(TAG, "摄像头初始化失败!错误码:0x%x", err);
    return;
    }
    ESP_LOGI(TAG, "摄像头初始化成功");

    // 4. 启动 HTTP 服务器
    httpd_handle_t server = start_webserver();
    if (server == NULL) {
    ESP_LOGE(TAG, "HTTP 服务器启动失败");
    return;
    }
    ESP_LOGI(TAG, "系统初始化完成!访问:http://ESP32_IP/ 查看摄像头");
    }

    2. app_wifi.c(WiFi 连接实现) 替换为你的 WiFi 名称和密码

    #include <string.h>
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "freertos/event_groups.h"
    #include "esp_log.h"
    #include "esp_wifi.h"
    #include "esp_netif.h"
    #include "esp_event.h"
    #include "app_wifi.h"

    static const char *TAG = "WIFI";
    static EventGroupHandle_t s_wifi_event_group;

    // ===================== 新手修改处 =====================
    // 替换为你的 WiFi 名称和密码
    #define WIFI_SSID "替换为你的 WiFi 名称"
    #define WIFI_PASSWORD "替换为你的 WiFi 密码"
    // =====================================================

    #define WIFI_CONNECTED_BIT BIT0
    #define WIFI_FAIL_BIT BIT1

    static void wifi_event_handler(void* arg, esp_event_base_t event_base,
    int32_t event_id, void* event_data)
    {
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
    esp_wifi_connect();
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
    ESP_LOGW(TAG, "WiFi 断开,重试连接…");
    esp_wifi_connect();
    xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
    ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
    ESP_LOGI(TAG, "WiFi 连接成功,IP地址:" IPSTR, IP2STR(&event->ip_info.ip));
    xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    }
    }

    void wifi_init_sta(void)
    {
    s_wifi_event_group = xEventGroupCreate();

    // 初始化网络接口
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();

    // 初始化 WiFi 驱动
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // 注册事件回调
    esp_event_handler_instance_t instance_any_id;
    esp_event_handler_instance_t instance_got_ip;
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
    ESP_EVENT_ANY_ID,
    &wifi_event_handler,
    NULL,
    &instance_any_id));
    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
    IP_EVENT_STA_GOT_IP,
    &wifi_event_handler,
    NULL,
    &instance_got_ip));

    // 配置 WiFi 账号密码
    wifi_config_t wifi_config = {
    .sta = {
    .ssid = WIFI_SSID,
    .password = WIFI_PASSWORD,
    .threshold.authmode = WIFI_AUTH_WPA2_PSK,
    },
    };
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    // 等待 WiFi 连接完成
    xEventGroupWaitBits(s_wifi_event_group,
    WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
    pdFALSE,
    pdFALSE,
    portMAX_DELAY);
    }

    3. app_wifi.h(WiFi 函数声明)

    #ifndef _APP_WIFI_H_
    #define _APP_WIFI_H_

    #ifdef __cplusplus
    extern "C" {
    #endif

    void wifi_init_sta(void);

    #ifdef __cplusplus
    }
    #endif

    #endif /* _APP_WIFI_H_ */

    4. app_camera.c(OV3660 摄像头驱动,解决 SOI 缺失)

    #include "app_camera.h"
    #include "esp_camera.h"
    #include "driver/gpio.h"
    #include "esp_log.h"

    // ESP32-CAM 与 OV3660 引脚匹配
    #define CAM_PIN_PWDN 32
    #define CAM_PIN_RESET 1
    #define CAM_PIN_XCLK 0
    #define CAM_PIN_SIOD 26
    #define CAM_PIN_SIOC 27
    #define CAM_PIN_D7 35
    #define CAM_PIN_D6 34
    #define CAM_PIN_D5 39
    #define CAM_PIN_D4 36
    #define CAM_PIN_D3 21
    #define CAM_PIN_D2 19
    #define CAM_PIN_D1 18
    #define CAM_PIN_D0 5
    #define CAM_PIN_VSYNC 25
    #define CAM_PIN_HREF 23
    #define CAM_PIN_PCLK 22

    static const char *TAG = "CAMERA";

    esp_err_t app_camera_main(void)
    {
    // OV3660 专用配置(解决 SOI 标记缺失)
    camera_config_t camera_config = {
    .pin_pwdn = CAM_PIN_PWDN,
    .pin_reset = CAM_PIN_RESET,
    .pin_xclk = CAM_PIN_XCLK,
    .pin_sccb_sda = CAM_PIN_SIOD,
    .pin_sccb_scl = CAM_PIN_SIOC,
    .pin_d7 = CAM_PIN_D7,
    .pin_d6 = CAM_PIN_D6,
    .pin_d5 = CAM_PIN_D5,
    .pin_d4 = CAM_PIN_D4,
    .pin_d3 = CAM_PIN_D3,
    .pin_d2 = CAM_PIN_D2,
    .pin_d1 = CAM_PIN_D1,
    .pin_d0 = CAM_PIN_D0,
    .pin_vsync = CAM_PIN_VSYNC,
    .pin_href = CAM_PIN_HREF,
    .pin_pclk = CAM_PIN_PCLK,
    .xclk_freq_hz = 10000000, // 降频到 10MHz,稳定编码
    .ledc_timer = LEDC_TIMER_0,
    .ledc_channel = LEDC_CHANNEL_0,
    .pixel_format = PIXFORMAT_JPEG, // 强制 JPEG 格式
    .frame_size = FRAMESIZE_QVGA, // 320×240,适配无 PSRAM
    .jpeg_quality = 10, // 降低质量,避免数据溢出
    .fb_count = 2, // 双缓冲区,防止数据丢失
    .fb_location = CAMERA_FB_IN_DRAM,
    .grab_mode = CAMERA_GRAB_WHEN_EMPTY
    };

    // 初始化摄像头
    esp_err_t err = esp_camera_init(&camera_config);
    if (err != ESP_OK) {
    ESP_LOGE(TAG, "摄像头初始化失败!错误码:0x%x", err);
    return err;
    }

    // 强制锁定 JPEG 配置,杜绝 RAW 格式
    sensor_t *s = esp_camera_sensor_get();
    if (s) {
    s->set_framesize(s, FRAMESIZE_QVGA);
    s->set_pixformat(s, PIXFORMAT_JPEG);
    ESP_LOGI(TAG, "OV3660 JPEG 配置生效");
    } else {
    ESP_LOGE(TAG, "获取摄像头传感器失败");
    return ESP_FAIL;
    }

    ESP_LOGI(TAG, "摄像头初始化成功!");
    return ESP_OK;
    }

    5. app_camera.h(摄像头函数声明)

    #ifndef APP_CAMERA_H
    #define APP_CAMERA_H

    #include "esp_err.h"

    esp_err_t app_camera_main(void);

    #endif // APP_CAMERA_H

    6. app_httpd.c(HTTP 服务器 + MJPEG 流,适配 Edge)

    #include "esp_err.h"
    #include "esp_http_server.h"
    #include "esp_log.h"
    #include "esp_camera.h"
    #include "string.h"
    #include "freertos/FreeRTOS.h"
    #include "freertos/task.h"
    #include "app_httpd.h"

    static const char *TAG = "HTTPD";

    // 主页 HTML(自带样式,适配浏览器)
    static const char *STREAM_HTML =
    "<!DOCTYPE html>"
    "<html>"
    "<head>"
    "<title>ESP32 OV3660 Camera</title>"
    "<meta charset=\\"utf-8\\">"
    "<style>"
    "body { margin: 0; padding: 20px; background-color: #f0f0f0; }"
    ".camera-container { max-width: 640px; margin: 0 auto; background: white; padding: 10px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }"
    "img { width: 100%; height: auto; border-radius: 4px; }"
    "h1 { text-align: center; color: #333; font-family: Arial, sans-serif; }"
    "</style>"
    "</head>"
    "<body>"
    "<div class=\\"camera-container\\">"
    "<h1>ESP32 OV3660 摄像头画面</h1>"
    "<img src=\\"/stream\\" alt=\\"Camera Stream\\">"
    "</div>"
    "</body>"
    "</html>";

    static esp_err_t index_handler(httpd_req_t *req)
    {
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, STREAM_HTML, strlen(STREAM_HTML));
    return ESP_OK;
    }

    // MJPEG 流核心处理(纯 ESP-IDF v5.1.6 支持 API)
    static esp_err_t stream_handler(httpd_req_t *req)
    {
    camera_fb_t *fb = NULL;
    esp_err_t res = ESP_OK;
    char part_buf[128];
    char conn_hdr[32];
    TickType_t start_tick;

    // Edge 浏览器兼容配置
    httpd_resp_set_type(req, "multipart/x-mixed-replace; boundary=frame");
    httpd_resp_set_hdr(req, "Cache-Control", "no-cache");
    httpd_resp_set_hdr(req, "Connection", "close");

    while (true) {
    // 手动超时获取帧(替代高版本函数,避免编译报错)
    start_tick = xTaskGetTickCount();
    fb = NULL;
    while (xTaskGetTickCount() start_tick < 500 / portTICK_PERIOD_MS) {
    fb = esp_camera_fb_get();
    if (fb) break;
    vTaskDelay(1 / portTICK_PERIOD_MS);
    }

    if (!fb) {
    ESP_LOGE(TAG, "获取帧超时");
    continue;
    }

    // 校验 JPEG 起始标记(0xFFD8),解决 SOI 缺失问题
    if (fb->format == PIXFORMAT_JPEG && fb->buf[0] == 0xFF && fb->buf[1] == 0xD8) {
    size_t hlen = snprintf(part_buf, sizeof(part_buf),
    "–frame\\r\\n"
    "Content-Type: image/jpeg\\r\\n"
    "Content-Length: %zu\\r\\n\\r\\n",
    fb->len);
    res = httpd_resp_send_chunk(req, part_buf, hlen);
    if (res != ESP_OK) break;

    res = httpd_resp_send_chunk(req, (const char *)fb->buf, fb->len);
    if (res != ESP_OK) break;
    } else {
    ESP_LOGE(TAG, "JPEG 数据异常,缺少 SOI 标记");
    }

    // 释放缓冲区,避免内存泄漏
    if (fb) {
    esp_camera_fb_return(fb);
    fb = NULL;
    }

    // 检测客户端断开连接(ESP-IDF v5.1.6 标准写法)
    ssize_t conn_hdr_len = httpd_req_get_hdr_value_len(req, "Connection");
    if (conn_hdr_len > 0 && conn_hdr_len < sizeof(conn_hdr)) {
    httpd_req_get_hdr_value_str(req, "Connection", conn_hdr, conn_hdr_len + 1);
    if (strcmp(conn_hdr, "close") == 0) {
    break;
    }
    }
    }

    // 清理资源
    if (fb) esp_camera_fb_return(fb);
    httpd_resp_send_chunk(req, "\\r\\n–frame–\\r\\n", strlen("\\r\\n–frame–\\r\\n"));
    ESP_LOGI(TAG, "流连接断开");
    return res;
    }

    httpd_handle_t start_webserver(void)
    {
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_uri_handlers = 16;
    config.server_port = 80;

    httpd_handle_t server = NULL;
    if (httpd_start(&server, &config) == ESP_OK) {
    // 注册主页路由
    httpd_uri_t index_uri = {
    .uri = "/",
    .method = HTTP_GET,
    .handler = index_handler,
    .user_ctx = NULL
    };
    httpd_register_uri_handler(server, &index_uri);

    // 注册视频流路由
    httpd_uri_t stream_uri = {
    .uri = "/stream",
    .method = HTTP_GET,
    .handler = stream_handler,
    .user_ctx = NULL
    };
    httpd_register_uri_handler(server, &stream_uri);

    ESP_LOGI(TAG, "HTTP 服务器启动成功,端口:80");
    } else {
    ESP_LOGE(TAG, "HTTP 服务器启动失败");
    }

    return server;
    }

    void stop_webserver(httpd_handle_t server)
    {
    if (server) {
    httpd_stop(server);
    ESP_LOGI(TAG, "HTTP 服务器已停止");
    }
    }

    7. app_httpd.h(HTTP 函数声明)

    #ifndef APP_HTTPD_H
    #define APP_HTTPD_H

    #include "esp_http_server.h"

    httpd_handle_t start_webserver(void);
    void stop_webserver(httpd_handle_t server);

    #endif // APP_HTTPD_H

    步骤 4:修改 CMakeLists.txt(编译配置)

    打开 main 目录下的 CMakeLists.txt,替换为以下内容:

    idf_component_register(SRCS "app_main.c" "app_wifi.c" "app_camera.c" "app_httpd.c"
    INCLUDE_DIRS ".")

    五、编译与烧录(使用烧录座,新手零门槛)

    步骤 1:配置串口与目标芯片

  • 按 Ctrl+Shift+P,输入 ESP-IDF: Select Port,选择烧录座对应的串口(例如 COM3);
  • 输入 ESP-IDF: Select Target,选择 esp32。
  • 步骤 2:清理并编译

    在 VS Code 终端中执行以下命令(复制粘贴即可):

    # 彻底清理旧缓存(解决编译残留问题)
    idf.py fullclean

    # 编译项目
    idf.py build

    编译成功会显示「Project build complete」,无任何报错。

    步骤 3:烧录程序(烧录座专用)

  • 将 ESP32-CAM 插入烧录座,确保引脚对齐;
  • 用 USB 数据线连接烧录座与电脑,在终端执行:
  • idf.py flash

  • 烧录完成后,按一下 ESP32-CAM 的复位键(RST),无需手动切换 IO0。
  • 步骤 4:查看串口日志(获取 ESP32 IP)

    在终端执行:

    idf.py monitor

    等待日志输出,找到以下关键信息:

    I (5471) WIFI: WiFi 连接成功,IP地址:192.168.31.179
    I (5941) MAIN: 系统初始化完成!访问:http://ESP32_IP/ 查看摄像头

    记录下你的 ESP32 IP(例如 192.168.31.179)。

    六、查看摄像头画面(最终验证)

  • 确保电脑/手机与 ESP32 连接同一个 WiFi;
  • 打开微软 Edge/谷歌 Chrome 浏览器,输入刚才记录的 IP(例如 http://192.168.31.179);
  • 按 Ctrl+F5 强制刷新(避免浏览器缓存),即可看到实时摄像头画面!
  • 画面如下 在这里插入图片描述
  • 七、新手常见问题排查

    问题现象原因解决方法
    编译报错「unknown type name」 头文件缺失/顺序错误 严格按本文代码的头文件顺序引入
    日志显示「NO-SOI」 OV3660 编码异常 检查 app_camera.c 中 xclk_freq_hz 是否为 10MHz,fb_count 是否为 2
    Edge 只显示标题,不显示画面 浏览器缓存/流格式不兼容 按 Ctrl+F5 强制刷新,关闭 Edge 安全增强模式
    WiFi 连接失败 账号密码错误/信号差 检查 app_wifi.c 中的 WIFI_SSID 和 WIFI_PASSWORD,靠近路由器
    烧录失败 烧录座接触不良/串口错误 重新插拔 ESP32-CAM,重新选择正确串口

    结语

    本文从硬件到软件,全程为新手打造,代码经过实测验证,完美适配 ESP-IDF v5.1.6、OV3660 摄像头与 Edge 浏览器。新手只需按步骤复制代码、接线、编译,即可快速搭建属于自己的 ESP32 摄像头流服务器,后续可在此基础上扩展人脸识别、视频录制等功能。

    源码

    已上传Gitee https://gitee.com/yunjingshan/esp32-ov3660-mjpeg-server

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 新手零门槛搭建 ESP32 + OV3660 摄像头 MJPEG 流服务器(ESP-IDF v5.1.6 适配版)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!