1. 项目概述

ESP32-RTSPServer 是一款专为 ESP32 系列 SoC(包括 ESP32-S2、ESP32-S3、ESP32-C3/C6)设计的轻量级嵌入式 RTSP 流媒体服务器库。它并非通用型流媒体服务框架,而是深度耦合 ESP-IDF/Arduino-ESP32 生态的实时音视频传输中间件,其核心目标是在资源受限的 MCU 级硬件上,以最小内存开销和确定性时序,实现符合 RFC 2326(RTSP)、RFC 3550(RTP)和 RFC 3551(RTP Payload for Audio/Video)标准的端到端流媒体服务能力。

该库不依赖外部 GStreamer、FFmpeg 或 VLC 等重量级组件,所有协议栈(RTSP 信令、RTP 封包、RTCP 基础反馈)均以 C++ 实现,直接运行于 FreeRTOS 之上。其设计哲学是“硬件驱动先行、协议精简可控”:视频流强制绑定 ESP32 Camera HAL(支持 OV2640、OV3660、OV5640、OV7670、OV2460 等主流模组),音频流强制绑定 I2S 接口(支持 PDM 麦克风或 I2S DAC 输出),字幕流则提供裸数据通道。这种强绑定牺牲了通用性,却换来极高的实时性保障与极低的系统资源占用——在 ESP32-S3 上,启用 VGA@25fps + 48kHz 音频 + 字幕的完整三流模式,静态 RAM 占用仅约 180KB,Flash 占用约 210KB(含 Camera 和 I2S 驱动)。

与传统 Linux 下的 live555 gstreamer-rtsp-server 不同,ESP32-RTSPServer 不提供 SDP 文件动态生成、会话状态持久化、多路复用调度等高级功能,而是将复杂度下沉至硬件抽象层(HAL),由开发者通过 FreeRTOS 任务显式控制帧采集、编码(若需)、封包与发送节奏。这种“半手动”模式要求开发者对 RTSP 生命周期(OPTIONS/DESCRIBE/SETUP/PLAY/TEARDOWN)和 RTP 时间戳机制有基本理解,但同时也赋予了对每一帧延迟、带宽分配、QoS 策略的完全掌控权。

2. 核心架构与数据流

2.1 整体分层模型

ESP32-RTSPServer 采用四层垂直架构,各层职责清晰,无跨层调用:

层级 组件 职责 关键约束
应用层 用户 Sketch( setup() / loop() 初始化硬件(Camera/I2S)、创建 FreeRTOS 任务、调用 sendRTSPFrame / sendRTSPAudio 必须使用 xTaskCreate 显式创建独立任务处理音视频,禁止在 loop() 中阻塞调用
服务层 RTSPServer 类实例 管理 RTSP 信令状态机、维护客户端连接表、调度 RTP 发送时机、提供 readyToSend*() 同步接口 所有 send*() 调用必须在 readyToSend*() 返回 true 后执行,否则丢帧
传输层 内置 UDP Socket(LwIP)、RTP/RTCP 封包器 构建 RTP 包头(含时间戳、序列号、SSRC)、计算校验和、按 TransportType 分发至对应端口 视频/音频/字幕严格分离端口(默认 5004/5006/5008),不支持单端口多流复用
硬件抽象层 ESP32 Camera HAL、I2S HAL、FreeRTOS Timer 提供 esp_camera_fb_get() 帧缓冲区、 i2s_read() 音频采样、 esp_timer_start_periodic() 字幕定时器 Camera 必须配置为 PIXFORMAT_JPEG (硬件 JPEG 编码)或 PIXFORMAT_RGB888 (软件编码,慎用)

2.2 典型数据流(以 VGA@25fps 视频流为例)

  1. 帧采集 sendVideo 任务循环调用 esp_camera_fb_get() 获取一帧 JPEG 数据( fb->buf , fb->len )。Camera HAL 在 DMA 完成后触发中断,将数据存入双缓冲区。
  2. 就绪判断 rtspServer.readyToSendFrame() 检查内部 RTP 发送队列是否空闲(避免覆盖未发送完的上一帧)。该函数非阻塞,返回 true 表示可安全写入。
  3. RTP 封包 rtspServer.sendRTSPFrame() 将 JPEG 数据按 RFC 2435(RTP Payload Format for JPEG-compressed Video)封装:
    • 设置 M=1 (Marker Bit)标识关键帧(JPEG SOI)
    • 计算 Timestamp = 上一帧时间戳 + 90000 / fps (JPEG 使用 90kHz 时钟频率)
    • 填充 Sequence Number (自增)
    • 写入 SSRC (随机生成的同步源标识符)
    • 追加 JPEG 特定头( Type-specific , F , DRI , Width , Height 字段)
  4. UDP 发送 :封装后的 RTP 包经 LwIP UDP Socket 发送至 rtpIp:rtpVideoPort (默认 239.255.0.1:5004 )。
  5. RTCP 反馈 :服务层每 5 秒自动发送 RTCP Sender Report(SR),携带 rtpFps 统计值,供客户端做拥塞控制。

关键工程考量 readyToSendFrame() 的存在本质是解决“生产者-消费者”速率不匹配问题。Camera 采集速率(如 50fps)常高于网络发送能力(如 25fps),若无此门控,将导致帧缓冲区溢出或内存碎片。开发者需根据实际网络带宽调整 vTaskDelay() 周期,例如 vTaskDelay(pdMS_TO_TICKS(40)) 强制限速至 25fps。

3. 核心功能详解与工程实践

3.1 认证机制(Basic Authentication)

RTSP Basic Auth 采用 RFC 2617 标准,明文 Base64 编码传输,适用于局域网内轻量级访问控制。 setCredentials() 接口仅存储用户名/密码字符串,认证逻辑由服务层在 DESCRIBE SETUP 请求中自动完成:

// 在 setup() 中设置
const char* rtspUser = "admin";
const char* rtspPassword = "123456";
rtspServer.setCredentials(rtspUser, rtspPassword);

抓包验证 :Wireshark 中可见 Authorization: Basic YWRtaW46MTIzNDU2 admin:123456 的 Base64)。
安全提示 :切勿在公网环境使用,密码明文风险高;如需增强,需自行扩展 Digest Auth 或 TLS(需修改库源码集成 mbedTLS)。

3.2 多客户端支持模式

ESP32-RTSPServer 默认采用“单客户端独占”策略,即同一 Transport(如 UDP 单播)仅允许一个客户端连接,后续连接请求被拒绝。此设计源于 MCU 资源限制——每个客户端需独立维护 RTP 序列号、时间戳偏移、RTCP 状态,多客户端将线性增加 RAM 开销。

OVERRIDE_RTSP_SINGLE_CLIENT_MODE 宏启用后,服务层改为“广播式”发送:所有客户端共享同一组 RTP 包(相同 SSRC、序列号、时间戳),实现伪多播。此模式下 maxRTSPClients 变量生效:

// 在 RTSPConfig.h 中定义
#define OVERRIDE_RTSP_SINGLE_CLIENT_MODE
// 在 setup() 中设置
rtspServer.maxRTSPClients = 5; // 最大支持 5 个 TCP/UDP 客户端

适用场景 :局域网内多屏监控(如 4 个手机同时观看同一摄像头)。
限制 :无法为不同客户端定制分辨率/码率;RTCP 反馈仅反映首个客户端状态。

3.3 传输类型(TransportType)配置

TransportType 枚举定义了流媒体组合方式,直接影响 begin() 初始化参数及端口分配:

枚举值 含义 默认端口 典型用途
VIDEO_ONLY 仅视频流 rtpVideoPort=5004 低成本视频监控(无音频)
AUDIO_ONLY 仅音频流 rtpAudioPort=5006 独立音频采集节点
VIDEO_AND_AUDIO 视频+音频 5004+5006 主流安防设备
VIDEO_AND_SUBTITLES 视频+字幕 5004+5008 教育类直播(叠加文字说明)
VIDEO_AUDIO_SUBTITLES 全三流 5004+5006+5008 高级交互式应用

端口配置示例 (强制指定):

rtspServer.transport = RTSPServer::VIDEO_AUDIO_SUBTITLES;
rtspServer.rtpVideoPort = 10000;
rtspServer.rtpAudioPort = 10002;
rtspServer.rtpSubtitlesPort = 10004;
rtspServer.begin(); // 使用自定义端口

工程建议 :避免使用知名端口(如 554、80),防止与系统服务冲突;多设备部署时,为每台 ESP32 分配唯一 rtpIp (如 239.255.1.1 , 239.255.1.2 )。

3.4 字幕(Subtitles)实现机制

字幕流并非标准 WebVTT 或 SRT,而是原始文本数据的 RTP 封装(Payload Type 126,私有类型)。 sendRTSPSubtitles() 将传入的 char* 数据作为有效载荷,添加 RTP 头后发送:

void onSubtitles(void* arg) {
    static uint32_t counter = 0;
    char data[64];
    size_t len = snprintf(data, sizeof(data), "Frame:%lu, FPS:%lu", 
                          counter++, rtspServer.rtpFps);
    rtspServer.sendRTSPSubtitles(data, len);
}

// 在 setup() 中启动定时器(1秒周期)
rtspServer.startSubtitlesTimer(onSubtitles);

接收端解析 :VLC 需手动指定字幕解码器为 Raw text ,或通过 --sub-filter=marq 显示。更可靠的方式是使用 ffplay

ffplay -rtsp_transport tcp "rtsp://192.168.1.100:554/stream" -vf "drawtext=textfile=subs.txt:fontcolor=white:box=1"

4. API 接口深度解析

4.1 构造与生命周期管理

函数 参数说明 工程要点
RTSPServer() 无参构造 仅初始化内部变量,不占用资源
bool init(...) transport : 流类型; rtspPort : RTSP 信令端口(默认 554); sampleRate : 音频采样率(Hz); port1/2/3 : 视频/音频/字幕 RTP 端口; rtpIp : 多播地址; rtpTTL : IP 生存时间 sampleRate 仅用于音频,必须与 I2S 配置一致(如 i2s_config.sample_rate = 48000 ); rtpTTL=1 限制在本子网, 64 可跨路由
void deinit() 释放所有 Socket、Timer、内存池;调用后不可再 begin()
bool reinit() 无需 deinit() ,直接重建状态机;适用于动态切换流类型

4.2 媒体发送接口(关键同步点)

函数 参数说明 时序约束 典型错误
bool readyToSendFrame() 必须在 sendRTSPFrame() 前调用,返回 true 才可发送 忽略检查导致帧丢失或乱序
void sendRTSPFrame(const uint8_t*, size_t, int, int, int) data : JPEG/Raw 数据指针; len : 长度; quality : JPEG 质量(0-63); width/height : 分辨率 data 必须来自 esp_camera_fb_get() ,且 fb->len <= 1400 (避免 IP 分片) 传入 RGB 数据但未配置 PIXFORMAT_RGB888 ,导致解码失败
bool readyToSendAudio() readyToSendFrame() 音频采样率与 sampleRate 不匹配,RTCP 报告异常
void sendRTSPAudio(int16_t*, size_t) data : PCM 数据(小端,16bit); len : 字节数(必须为偶数) len 应为 sampleRate * 2 * 0.02 (20ms 包),如 48kHz 对应 1920 字节 传入 24bit 数据导致音频失真
bool readyToSendSubtitles() 同前 字幕数据过长(>1000 字节)可能被截断

4.3 配置变量与状态监控

变量 类型 读写权限 用途说明
rtpFps uint32_t 只读 实时统计的 RTP 发送帧率,用于 QoS 调整
transport TransportType 读写 运行时切换流类型(需 reinit()
sampleRate uint32_t 读写 动态调整音频采样率(需重启 I2S)
rtspPort int 读写 修改 RTSP 信令端口(如防火墙限制)
rtpIp IPAddress 读写 切换单播( INADDR_ANY )或多播地址
rtpTTL uint8_t 读写 控制多播范围(1=本子网,64=全网)
maxRTSPClients uint8_t 读写 OVERRIDE_RTSP_SINGLE_CLIENT_MODE 下生效

5. 硬件集成与性能调优

5.1 Camera 配置最佳实践

OV2460 在 ESP32-S3 上实测性能(见 README 表格)表明:分辨率与帧率呈反比关系。根本原因在于 JPEG 编码带宽瓶颈。优化路径如下:

  1. DMA 缓冲区调优 :在 camera_config_t 中增大 fb_count (默认 2):
    config.fb_count = 4; // 减少 `esp_camera_fb_get()` 阻塞概率
    
  2. JPEG 质量动态调节 :根据 rtpFps 自适应 quality
    int quality = (rtspServer.rtpFps > 30) ? 30 : (rtspServer.rtpFps > 15) ? 40 : 50;
    rtspServer.sendRTSPFrame(fb->buf, fb->len, quality, fb->width, fb->height);
    
  3. 分辨率裁剪 :使用 set_vsync / set_hsync 裁剪无效区域,降低编码负载。

5.2 I2S 音频同步方案

I2S 输入(麦克风)与 RTP 发送存在天然异步性。推荐采用“双缓冲+时间戳对齐”方案:

// 全局环形缓冲区(20ms * 2)
static int16_t audioBuffer[1920 * 2];
static size_t bufferWriteIndex = 0;

// I2S 回调(高优先级)
void i2s_callback(i2s_dev_t* i2s, void* arg) {
    size_t bytes_read;
    i2s_read(I2S_NUM_0, audioBuffer + bufferWriteIndex, 1920, &bytes_read, portMAX_DELAY);
    bufferWriteIndex = (bufferWriteIndex + 1920) % (1920 * 2);
}

// sendAudio 任务中
if (rtspServer.readyToSendAudio()) {
    // 从环形缓冲区读取最新 20ms 数据
    memcpy(sampleBuffer, audioBuffer + ((bufferWriteIndex - 1920 + 3840) % 3840), 1920);
    rtspServer.sendRTSPAudio(sampleBuffer, 1920);
}

5.3 内存与 Flash 优化

  • 禁用日志 #define RTSP_LOGGING_ENABLED 注释掉,节省 7.7KB Flash。
  • 精简 Transport :若无需字幕,使用 VIDEO_AND_AUDIO 而非 VIDEO_AUDIO_SUBTITLES ,减少代码体积。
  • TCP/HTTP Tunnel 谨慎启用 :README 明确标注“Slower”,因其需额外 Socket 和 HTTP 解析,仅在 UDP 被防火墙阻断时备用。

6. 调试与故障排查

6.1 常见问题诊断表

现象 可能原因 排查命令/方法
VLC 提示 “No data received” Camera 未初始化成功 Serial.println(esp_camera_init(&config)) 检查返回值
视频卡顿、马赛克 readyToSendFrame() 返回 false 频繁 添加 Serial.printf("Queue full: %d\n", rtspServer.isFrameQueueFull())
音频断续 I2S 采样率与 sampleRate 不一致 i2s_get_clk(I2S_NUM_0, &rate, &bits, &chan) 验证
多客户端仅首台有画面 未定义 OVERRIDE_RTSP_SINGLE_CLIENT_MODE 检查 RTSPConfig.h 宏定义
RTSP 连接超时 防火墙拦截 554 端口 telnet 192.168.1.100 554 测试连通性

6.2 VLC 连接参数(关键设置)

在 VLC 中打开网络串流,URL 格式为:

rtsp://<ESP32_IP>:<rtspPort>/stream

必设选项 (工具 → 选项 → 全部 → 输入/编解码器 → 访问模块 → RTSP):

  • Caching :设为 300 (毫秒),降低初始延迟
  • RTSP/TCP :勾选,强制 TCP 传输(绕过 UDP 丢包)
  • User Agent :设为 LIVE555 Streaming Media v2023.01.01 (兼容性更好)

7. 源码关键逻辑剖析

7.1 readyToSendFrame() 实现原理

该函数本质是检查内部帧队列( frameQueue )是否已满。队列采用环形缓冲区设计,大小为 FRAME_QUEUE_SIZE (默认 3):

// 简化版逻辑
bool RTSPServer::readyToSendFrame() {
    return (uxQueueMessagesWaiting(frameQueue) < FRAME_QUEUE_SIZE);
}

sendRTSPFrame() 被调用时,数据被 xQueueSendToBack() 入队,由后台 rtpSendTask 消费。若队列满, readyToSendFrame() 返回 false ,提示应用层降速。

7.2 RTP 时间戳生成逻辑

时间戳非系统毫秒,而是基于 90kHz 时钟的增量:

// video_rtp.c 中
uint32_t timestamp = lastTimestamp + (90000.0 / fps); // fps 来自 rtpFps 统计
lastTimestamp = timestamp;

此设计确保视频播放器能精确计算 PTS(Presentation Time Stamp),避免因网络抖动导致的音画不同步。

8. 实际项目集成示例

以下为 ESP32-S3 + OV2460 + I2S 麦克风的完整 setup() 片段:

#include "RTSPConfig.h"
#include <ESP32-RTSPServer.h>
#include "driver/i2s.h"

RTSPServer rtspServer;
const char* rtspUser = "user";
const char* rtspPassword = "pass";

// I2S 配置
i2s_config_t i2s_config = {
    .mode = I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_PDM,
    .sample_rate = 48000,
    .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
    .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 = 1024,
};

void setup() {
    Serial.begin(115200);
    
    // 初始化 Camera
    camera_config_t config;
    config.ledc_channel = LEDC_CHANNEL_0;
    config.ledc_timer = LEDC_TIMER_0;
    config.pin_d0 = 40; // OV2460 D0
    // ... 其他引脚配置
    config.pixel_format = PIXFORMAT_JPEG;
    config.frame_size = FRAMESIZE_VGA;
    config.jpeg_quality = 12;
    config.fb_count = 4;
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) { Serial.printf("Camera init failed: %s\n", esp_err_to_name(err)); }

    // 初始化 I2S
    i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
    i2s_set_pin(I2S_NUM_0, &pin_config);

    // 配置 RTSP Server
    rtspServer.transport = RTSPServer::VIDEO_AND_AUDIO;
    rtspServer.sampleRate = 48000;
    rtspServer.rtspPort = 554;
    rtspServer.setCredentials(rtspUser, rtspPassword);
    rtspServer.maxRTSPClients = 3;

    // 创建任务
    xTaskCreate(sendVideo, "Video", 4096, NULL, 9, NULL);
    xTaskCreate(sendAudio, "Audio", 4096, NULL, 8, NULL);

    // 启动服务器
    if (rtspServer.begin()) {
        Serial.println("RTSP Server started on port 554");
    } else {
        Serial.println("RTSP Server start failed");
    }
}

此配置在 ESP32-S3 上稳定运行 VGA@25fps + 48kHz 音频,实测端到端延迟(Camera 采集到 VLC 显示)约为 280ms,满足一般监控需求。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐