TXW82x Spook RTSP 流媒体服务器架构文档
1. 概述
Spook 是一个运行在 TXW82x 嵌入式芯片上的轻量级 RTSP 流媒体服务器,通过 Wi-Fi 将摄像头采集的视频以 RTSP 协议推送给手机/电脑等客户端。最初由 Nathan Lutchansky 于 2004 年开发的开源项目,后被移植到 TXW82x 平台(CK810 CPU + lwIP 协议栈)。
1.1 核心能力
| 能力 |
说明 |
| 视频编码 |
MJPEG (硬件 JPEG 编码器)、H.264 (硬件 H.264 编码器) |
| 音频编码 |
AAC (硬件音频编码器) |
| 传输协议 |
RTP/AVP over UDP、RTP/AVP over TCP (RTSP 交织) |
| 默认端口 |
554 |
| 多路流 |
同时注册多个 RTSP 路径,独立推流 |
| 录像回放 |
支持通过 RTSP 回放 SD 卡中的 MP4/AVI 文件 |
| 并发客户端 |
多客户端同时连接同一路流 |
1.2 硬件平台
| 组件 |
说明 |
| CPU |
CK810 (C-SKY 架构) |
| 视频编码 |
硬件 JPEG 编码器 + 硬件 H.264 编码器 |
| 音频编码 |
硬件 AAC 编码器 |
| 网络 |
Wi-Fi (AP 模式,默认 IP 192.168.169.1) |
| 内存 |
SRAM + PSRAM 混合堆 |
2. 软件架构
2.1 模块层次图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| ┌──────────────────────────────────────────────────────────────────────┐ │ 应用层 (app_fpv.c) │ │ spook_init() → 初始化基础设施 │ │ rtsp_mjpeg_live_init() → 注册 /webcam 路径 │ │ rtsp_h264_live_init() → 注册 /h264 路径 │ └───────────────────────────┬──────────────────────────────────────────┘ │ ┌───────────────────────────▼──────────────────────────────────────────┐ │ Spook RTSP Server 内核 │ │ │ │ ┌──────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │ │ │ spook.c │ │ rtsp_common.c │ │ tcp.c │ │ │ │ 模块入口 │ │ RTSP协议层 │ │ TCP连接管理 │ │ │ │ 随机数工具 │ │ 路径注册/会话管理 │ │ 事件驱动I/O │ │ │ └──────────────┘ └────────┬─────────┘ └──────────────────────┘ │ │ │ │ │ ┌───────────────────────────┼────────────────────────────────────┐ │ │ │ RTSP 路径注册 按需调用 │ │ │ │ │ │ │ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │ │ │ │ rtsp_mjpeg_live.c │ │ rtsp_h264_live.c │ │ │ │ │ │ /webcam (MJPEG) │ │ /h264 (H.264) │ │ │ │ │ └─────────┬───────────┘ └─────────┬───────────┘ │ │ │ │ │ │ │ │ │ │ ┌─────────▼───────────┐ ┌─────────▼───────────┐ │ │ │ │ │ custom_rtsp_jpg.c │ │ webfile.c │ │ │ │ │ │ /custom (动态源) │ │ /file (文件回放) │ │ │ │ │ └─────────────────────┘ └─────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │ │ rtp.c │ │ rtp-jpeg.c │ │ rtp-h264.c │ │ │ │ RTP协议层 │ │ JPEG分包 │ │ H.264 FU-A分片 │ │ │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │ │ session.c │ │ stream.c │ │ frame.c │ │ │ │ 会话管理 │ │ 流管道 │ │ 帧管理/MSI接口 │ │ │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │ │ │ encoder- │ │ encoder- │ │ encoder- │ │ │ │ jpeg.c/h │ │ h264.c │ │ audio.c/h │ │ │ │ JPEG适配 │ │ H264适配 │ │ 音频适配 │ │ │ └──────────────┘ └──────────────┘ └──────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘ │ ┌───────────────────────────▼─────────────────────────────────────────┐ │ SDK 底层依赖 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ ┌────────┐ │ │ │ lwIP │ │ csi_ │ │ MSI │ │ video_app │ │audio │ │ │ │ TCP/IP │ │ kernel │ │ 多媒体 │ │ 视频应用 │ │媒体 │ │ │ └──────────┘ └──────────┘ └──────────┘ └────────────┘ └────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ 硬件层: 摄像头(CSI/DVP) → ISP → JPEG/H264编码器 → Wi-Fi │ │ │ └──────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────┘
|
2.2 文件清单
| 文件 |
行数 |
职责 |
| spook.c |
~145 |
模块入口:spook_init(),随机数工具,rtp_name 定义 |
| spook.h |
~10 |
对外接口声明 |
| spook_config.h |
~70 |
全局配置宏、编码器名称、rtp_name 结构体 |
| rtsp.c |
- |
RTSP 协议解析器 |
| rtsp_common.c |
~500+ |
路径注册、轨道设置、SDP 生成、会话管理 |
| rtsp_common.h |
~70 |
rtsp_source、rtsp_session、rtsp_track 定义 |
| rtsp_mjpeg_live.c |
~180 |
/webcam — MJPEG 实时预览路径实现 |
| rtsp_h264_live.c |
~260 |
/h264 — H.264 实时流 + /loop/RECA/ 回放 |
| custom_rtsp_jpg.c |
~160 |
/custom — 自定义视频源路径 |
| webfile.c |
~230 |
/file — SD 卡文件回放(已注释) |
| rtp.h |
- |
RTP 核心结构体定义 |
| rtp.c |
~700 |
RTP 包收发、UDP/TCP 传输、RTCP |
| rtp-jpeg.c |
~560 |
JPEG RTP 封包(RFC 2435) |
| rtp-h264.c |
~320 |
H.264 RTP 封包(RFC 3984, FU-A) |
| rtp-audio.c |
- |
AAC RTP 封包 |
| stream.c |
~120 |
Stream 发布-订阅管道 |
| stream.h |
~40 |
Stream 结构体定义 |
| frame.h |
~160 |
Frame 结构体、FrameExchanger、回调函数类型 |
| frame.c |
~410 |
帧管理、MSI 接口、主发送线程 |
| session.c |
~60 |
RTP 媒体会话管理 |
| session.h |
~10 |
会话接口声明 |
| rtp_media.h |
~50 |
RTP 媒体接口定义 |
| encoder-jpeg.c |
~150 |
JPEG 编码器适配 |
| encoder-jpeg.h |
~25 |
JPEG 编码器结构体 |
| encoder-h264.c |
~140 |
H.264 编码器适配 |
| encoder-audio.c |
~145 |
AAC 音频编码器适配 |
| encoder-audio.h |
~15 |
音频编码器结构体 |
| event.h |
- |
事件驱动框架(基于 eloop) |
| tcp.c |
- |
TCP 连接管理 |
| pmsg.h/c |
- |
HTTP/RTSP 协议消息解析 |
| log.h |
- |
日志宏(已注释禁用) |
| access_log.c |
- |
访问日志 |
| ephoto.c |
- |
测试 JPEG 图片数据 |
3. 核心数据结构
3.1 rtp_name — 路径描述符
定义于 spook_config.h,描述一条 RTSP 流所需的编码器和路径信息。
1 2 3 4 5
| typedef struct { const char *video_encode_name; const char *audio_encode_name; const char *path; } rtp_name;
|
使用示例:
1 2 3 4 5
| const rtp_name live_dvp = { .video_encode_name = JPG_ENCODER_NAME, .audio_encode_name = AUDIO_AAC_ENCODER_NAME, .path = "/webcam", };
|
3.2 rtsp_source — RTSP 源
定义于 rtsp_common.h,代表一个 RTSP 路径的完整状态。
1 2 3 4 5 6 7 8 9 10 11 12
| struct rtsp_source { struct rtsp_session *sess_list; struct rtsp_track track[MAX_TRACKS]; struct os_task handle; void *signal; struct rtp_node live_node; int head_len; rtsp_creat creat; rtsp_release release; rtsp_play_fn play; void *priv; };
|
3.3 rtsp_track — 轨道
1 2 3 4 5 6 7
| struct rtsp_track { int index; struct rtsp_source *source; struct stream_destination *stream; int ready; struct rtp_media *rtp; };
|
定义于 rtp_media.h,纯函数指针结构体,定义了对一种媒体类型的全部操作。
1 2 3 4 5 6 7 8 9 10 11 12
| struct rtp_media { rtp_media_get_sdp_func get_sdp; rtp_media_get_payload_func get_payload; rtp_media_frame_func frame; rtp_media_send_func send; rtp_media_send_more_func send_more; rtp_media_send_func rtcp_send; void *private; int per_ms_incr; uint32_t sample_rate; uint8_t type; };
|
3.5 frame_exchanger — 帧交换器
定义于 frame.h,单 slot 的帧转发器,是编码器输出和 stream 管道之间的桥梁。
1 2 3 4 5 6 7
| struct frame_exchanger { int ready; int scan_ready; frame_deliver_func f; struct frame *jf; void *d; };
|
3.6 rtp_node — 音视频出口节点
1 2 3 4 5
| struct rtp_node { void *video_ex; void *audio_ex; void *priv; };
|
3.7 rtsp_priv — 路径私有数据
1 2 3 4 5 6 7 8
| struct rtsp_priv { struct rtp_node *live_node; stream *webfile_s; struct msi *video_msi; struct msi *audio_msi; struct msi *v_msi; struct msi *a_msi; };
|
3.8 rtp_endpoint — RTP 端点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| struct rtp_endpoint { struct session *session; int payload; int max_data_size; unsigned int ssrc; unsigned int start_timestamp; unsigned int last_timestamp; int seqnum; int packet_count; int octet_count; int sendEnable; int trans_type; union { struct { } udp; struct { } inter; } trans; };
|
4. 完整 MJPEG 流调用链
4.1 初始化阶段
阶段流程图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| main() → fpv_app_init() → user_protocol() │ └─ spook_init() [spook.c] │ └─ spook_thread() │ ├─ init_random() │ └─ random_key = 0x12345678 │ ├─ global_init() │ └─ config_port(554) [spook.c] │ ├─ jpeg_encode_init("jpg_encoder") [encoder-jpeg.c] │ ├─ start_block() │ │ └─ malloc(jpeg_encoder) ← 分配编码器结构体 │ ├─ set_output("jpg_encoder", en) │ │ └─ new_stream("jpg_encoder", FORMAT_JPEG, en) │ │ └─ 创建 stream 节点,挂入全局链表 │ └─ end_block(en) │ └─ new_exchanger(16, get_back_frame, en) │ └─ 创建 frame_exchanger,回调=get_back_frame │ ├─ h264_encode_init("h264_encoder") [encoder-h264.c] │ └─ 同上,创建 "h264_encoder" stream │ ├─ rtsp_audio_encode_init("audio_aac_encoder") [encoder-audio.c] │ └─ 创建 "audio_aac_encoder" stream │ ├─ rtsp_audio_encode_init("audio_aac_encoder2") │ └─ 创建 "audio_aac_encoder2" stream │ ├─ rtsp_mjpeg_live_init(&live_dvp) [rtsp_mjpeg_live.c] │ ├─ rtsp_start_block() │ │ └─ malloc(rtsp_source) │ ├─ rtsp_set_path("/webcam", source, self_rtsp_open) │ ├─ set_video_track("jpg_encoder", source) │ │ ├─ connect_to_stream("jpg_encoder", rtsp_common_send, &track) │ │ │ → 查找已存在的 stream("jpg_encoder") │ │ │ → 创建 stream_destination,注册回调 rtsp_common_send │ │ ├─ source->live_node.video_ex = get_video_ex(en) │ │ │ → en->ex (jpeg_encoder 的 frame_exchanger) │ │ └─ track->rtp = new_rtp_media_jpeg_stream(stream) │ │ → 创建 rtp_media { │ │ .frame = jpeg_process_frame, │ │ .send_more = jpeg_send_more, │ │ .get_sdp = jpeg_get_sdp │ │ } │ │ │ ├─ set_audio_track("audio_aac_encoder", source) │ │ └─ 同上,音频轨道 │ ├─ register_live_fn(source, self_creat, self_destory, NULL) │ └─ rtsp_end_block(source) │ ├─ rtsp_h264_live_init(&live_h264) ← 注册 /h264 ├─ custom_rtsp_jpeg_init(&live_custom) ← 注册 /custom └─ rtsp_h264_live_init(&live_h264_2) ← 注册 /loop/RECA/
|
初始化后内存结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| stream 全局链表 ├─ "jpg_encoder" (FORMAT_JPEG) │ ├─ private → jpeg_encoder │ │ ├─ output → stream("jpg_encoder") 自指 │ │ └─ ex → frame_exchanger │ │ ├─ f = get_back_frame │ │ ├─ d = jpeg_encoder │ │ └─ jf = frame * │ └─ dest_list │ └─ stream_destination [video track of /webcam] │ ├─ process_frame = rtsp_common_send │ └─ d = &rtsp_source.track[0] │ ├─ "h264_encoder" (FORMAT_H264) ├─ "audio_aac_encoder" (FORMAT_AUDIO) └─ "audio_aac_encoder2" (FORMAT_AUDIO)
RTSP 路径注册表 ├─ "/webcam" → rtsp_source │ ├─ track[0]: video → rtp_media(jpeg_process_frame) │ ├─ track[1]: audio → rtp_media(audio_send) │ └─ live_node.video_ex → frame_exchanger(get_back_frame) │ ├─ "/h264" → rtsp_source ├─ "/custom" → rtsp_source └─ "/loop/RECA/" → rtsp_source
|
4.2 客户端连接阶段
RTSP 协议交互
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| 客户端 (VLC/ffplay) 服务器 (端口 554) │ │ │ ── TCP connect ──► │ tcp.c: accept → conn │ │ │ ── OPTIONS ──► │ rtsp.c: 回复支持方法 │ ◄── 200 OK ── │ │ │ │ ── DESCRIBE /webcam ──► │ │ │ → 查找路径 "/webcam" │ │ → self_rtsp_open() │ │ → rtsp_open() 创建 session │ ◄── SDP ── │ │ m=video 0 RTP/AVP 26 │ track0: MJPEG │ m=audio 0 RTP/AVP 97 │ track1: AAC │ │ │ ── SETUP track0 ──► │ ★ 首次 SETUP 触发重量级初始化 │ │ rtsp_setup(s, t=0) │ │ └─ source->creat(source, path) │ │ → self_creat() [rtsp_mjpeg_live.c] │ │ ├─ os_zalloc(rtsp_priv) │ │ ├─ r->video_msi = msi_find(AUTO_JPG,1) │ │ ├─ r->v_msi = rtsp_msi_init(...) │ │ ├─ msi_add_output(video_msi, NULL, R_RTP_JPEG) │ │ └─ OS_TASK_INIT("live_rtsp_mjpeg", ...) │ │ → new_rtp_endpoint(26) │ ◄── 200 OK (channel 0-1) ── │ │ │ │ ── SETUP track1 ──► │ (线程已存在,不再创建) │ ◄── 200 OK (channel 2-3) ── │ │ │ │ ── PLAY ──► │ │ │ rtsp_play(s, start) │ │ ├─ ls->playing = 1 │ │ ├─ track->ready = 1 │ │ └─ set_waiting(stream, 1) │ │ → jpeg_encoder->running = 1 │ │ → ex->scan_ready = 1 │ ◄── 200 OK ── │ │ │ │ ◄══ RTP 数据流 ══► │ 发送线程开始推流 │ $ (interleaved data) │ TCP 交织通道 0-1 持续发送
|
4.3 数据发送阶段
核心发送线程 spook_send_thread_stream()
定义于 frame.c:316,是整个流媒体服务的心脏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| void spook_send_thread_stream(struct rtsp_priv *r) { while (1) { if (audio_rtsp) { audio_fb = msi_get_fb(audio_rtsp->msi, 0); if (audio_fb) { audio_ex->f(audio, audio_ex->d); } }
fb = msi_get_fb(jpg_rtsp->msi, 0);
if (fb) { jpeg = ex->jf; jpeg->get_f = fb; jpeg->d = fb->data; jpeg->length = fb->len; jpeg->timestamp = fb->time;
ex->f(jpeg, ex->d); }
if (!fb && !audio_fb) { os_sleep_ms(1); if (超时>1000ms) ex->f(NULL, ex->d); continue; } } }
|
帧处理完整调用链
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| ① msi_get_fb(v_msi, 0) ← 从 MSI 队列取帧 (缓冲区 28 帧) │ ▼ ② 填充 frame 结构体 │ ▼ ③ ex->f(jpeg, ex->d) │ ex->f = get_back_frame [encoder-jpeg.c] │ ex->d = jpeg_encoder * │ ▼ ④ get_back_frame(f, en) [encoder-jpeg.c] │ ▼ ⑤ deliver_frame_to_stream(f, en->output) [stream.c] │ en->output = stream("jpg_encoder") │ ▼ [遍历 stream 的 dest_list] │ ⑥ dest->process_frame(f, &track[t]) │ = rtsp_common_send(f, track) [rtsp_common.c] │ ├─ [6a] JPEG 头解析 ──────────────────────────────────┐ │ track->rtp->frame(f, track->rtp) │ │ = jpeg_process_frame(f, rtp) [rtp-jpeg.c] │ │ │ │ │ ├─ 遍历 JPEG 标记: │ │ │ 0xFFD8 SOI (图像起始) │ │ │ 0xFFDB DQT → parse_DQT() 解析量化表 │ │ │ 0xFFC0 SOF → parse_SOF() 解析宽/高/采样 │ │ │ 0xFFC4 DHT → parse_DHT() 解析哈夫曼表 │ │ │ 0xFFDD DRI → parse_DRI() 解析重置间隔 │ │ │ 0xFFDA SOS → 找到扫描数据起始位置 │ │ │ │ │ ├─ out->scan_data = f->d + offset │ │ ├─ out->scan_data_len = data_length │ │ ├─ out->init_done = 1 │ │ └─ return 1 (解析完成) │ └─────────────────────────────────────────────────────┘ │ ▼ └─ [6b] RTP 分包发送 ─────────────────────────────────┐ track->rtp->send_more(loop_search_ep, │ ls, track, track->rtp->private) │ = jpeg_send_more(...) [rtp-jpeg.c] │ │ │ └─ 遍历所有连接的客户端 session: │ │ │ ▼ │ jpeg_send_frame_to_endpoint(ep, out, fb) │ │ │ ├─ 构建 RTP 固定头 (12字节) │ │ V=2, P=0, PT=26, sequence++, timestamp │ │ │ ├─ 构建 JPEG 特定头 (8或12字节) │ │ type, width/8, height/8, quant tables │ │ │ ├─ 附加量化表 (128字节) │ │ │ ├─ [可选] 扩展头 (EXTHDR) │ │ Huffman 表 + DRI 信息 │ │ │ └─ 循环分包: │ while i < scan_data_len: │ ├─ max_size = rtp_get_payload_size() │ ├─ 构建数据分片 (32字节对齐) │ └─ rtp_sendmsg(ep, v, ...) │ ├─ UDP: sendto() + 重试×30 │ └─ TCP: send() 交织通道 │ └─────────────────────────────────────────────────────┘
|
5. Stream 发布-订阅模型
Stream 管道是 Spook 实现编码器与消费者解耦的核心设计。
5.1 模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 生产者 (编码器) 消费者 (RTSP/HTTP) │ │ │ new_stream("jpg_encoder", ...) │ │ └─ 创建命名的 stream 节点 │ │ │ │ │ connect_to_stream("jpg_encoder", cb, d) │ │ └─ 创建 stream_destination │ │ ├─ process_frame = 回调函数 │ │ └─ d = 回调参数 │ │ │ deliver_frame_to_stream(f, stream) │ │ └─ 遍历 stream->dest_list │ │ └─ dest->process_frame(f, d) ──► │ 收到帧 │ │ │ │ connect_to_stream("jpg_encoder", cb2, d2) │ │ └─ 第二个消费者加入 │ │ │ deliver_frame_to_stream(f, stream) │ │ └─ 遍历 dest_list │ │ ├─ dest->process_frame(f, d) ──► │ 消费者1收到 │ └─ dest->process_frame(f, d) ──► │ 消费者2也收到
|
5.2 关键 API
1 2 3 4 5 6 7 8 9
| struct stream *new_stream(const char *name, int format, void *d); void deliver_frame_to_stream(struct frame *f, void *d);
struct stream_destination *connect_to_stream(const char *name, frame_deliver_func process_frame, void *d); void set_waiting(struct stream_destination *dest, int waiting); void disconnect_stream(struct stream_destination *dest, ...);
|
5.3 在 MJPEG 流中的应用
1 2 3 4 5 6 7 8 9 10 11
| jpeg_encode_init("jpg_encoder") ← 生产者 └─ new_stream("jpg_encoder") ← 创建管道
set_video_track("jpg_encoder",...) ← 消费者 (RTSP) └─ connect_to_stream("jpg_encoder", rtsp_common_send, &track) ← 注册回调
get_back_frame(f, en) ← 帧到达 └─ deliver_frame_to_stream(f, en->output) └─ rtsp_common_send(f, &track) └─ jpeg_process_frame + jpeg_send_more
|
6. 关键配置参数
6.1 spook_config.h
| 宏 |
默认值 |
说明 |
SPOOK_PORT |
554 |
RTSP 服务端口号 |
MAX_DATA_PACKET_SIZE |
1440 |
RTP 最大数据包大小 (MTU 限制) |
EXCHANGER_SLOT_SIZE |
16 |
FrameExchanger 的 slot 数量 |
JPEG_FRAMEINC |
1 |
JPEG 帧率分子 |
H264_FRAMEINC |
1 |
H.264 帧率分子 |
SCAN_DATA_OFFSET |
0x253 |
JPEG 扫描数据偏移量 |
SPOOK_CACHE_BUF_LEN |
1600 |
TCP 发送缓存大小 |
6.2 frame.c 内部配置
| 宏 |
默认值 |
说明 |
MAX_RTSP_JPG_RECV |
28 |
JPEG MSI 接收缓冲区深度 |
MAX_RTSP_AUDIO_RECV |
10 |
音频 MSI 接收缓冲区深度 |
6.3 编码器名称宏
| 宏 |
值 |
用途 |
JPG_ENCODER_NAME |
"jpg_encoder" |
MJPEG 编码器 stream 名 |
H264_ENCODER_NAME |
"h264_encoder" |
H.264 编码器 stream 名 |
AUDIO_AAC_ENCODER_NAME |
"audio_aac_encoder" |
音频 stream 名 (用于 /webcam) |
AUDIO_AAC_ENCODER_NAME2 |
"audio_aac_encoder2" |
音频 stream 名2 (用于 /h264) |
7. RTSP 路径一览
| 路径 |
变量名 |
视频编码 |
音频编码 |
功能 |
/webcam |
live_dvp |
MJPEG |
AAC |
实时预览 (FPV 图传) |
/h264 |
live_h264 |
H.264 |
AAC |
H.264 高清实时流 |
/custom |
live_custom |
MJPEG |
AAC |
自定义视频源 |
/loop/RECA/ |
live_h264_2 |
H.264 |
AAC |
MP4 录像文件回放 |
/file |
webfile |
JPEG |
- |
SD 卡文件播放 (已注释) |
8. 完整 RTSP 协议交互示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| → OPTIONS rtsp://192.168.169.1:554/webcam RTSP/1.0 CSeq: 1 User-Agent: VLC/3.0.20
← RTSP/1.0 200 OK CSeq: 1 Public: DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, OPTIONS
→ DESCRIBE rtsp://192.168.169.1:554/webcam RTSP/1.0 CSeq: 2
← RTSP/1.0 200 OK CSeq: 2 Content-Type: application/sdp Content-Length: 123
v=0 o=- 1 1 IN IP4 127.0.0.1 s=Test a=type:broadcast t=0 0 c=IN IP4 0.0.0.0 m=video 0 RTP/AVP 26 a=control:track0 m=audio 0 RTP/AVP 97 a=rtpmap:97 mpeg4-generic/8000 a=control:track1
→ SETUP rtsp://192.168.169.1:554/webcam/track0 RTSP/1.0 CSeq: 3 Transport: RTP/AVP/TCP;interleaved=0-1
← RTSP/1.0 200 OK CSeq: 3 Session: A1B2C3D4 Transport: RTP/AVP/TCP;interleaved=0-1
→ SETUP rtsp://192.168.169.1:554/webcam/track1 RTSP/1.0 CSeq: 4 Session: A1B2C3D4 Transport: RTP/AVP/TCP;interleaved=2-3
← RTSP/1.0 200 OK CSeq: 4 Session: A1B2C3D4 Transport: RTP/AVP/TCP;interleaved=2-3
→ PLAY rtsp://192.168.169.1:554/webcam RTSP/1.0 CSeq: 5 Session: A1B2C3D4
← RTSP/1.0 200 OK CSeq: 5 Session: A1B2C3D4 RTP-Info: url=rtsp://192.168.169.1:554/webcam/track0; seq=45678;rtptime=12345
(RTP 数据通过 TCP 交织通道 0-1 持续发送) $ (interleaved binary data)...
→ TEARDOWN rtsp://192.168.169.1:554/webcam RTSP/1.0 CSeq: 6 Session: A1B2C3D4
← RTSP/1.0 200 OK CSeq: 6
|
9. 事件驱动与并发模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ┌──────────────────────────────────────────────────────────┐ │ CPU0 (CK810) │ │ │ │ [eloop 事件循环线程] [发送线程 per path] │ │ ┌──────────────────────┐ ┌──────────────────┐ │ │ │ TCP listen │ │ spook_send_ │ │ │ │ → accept conn │ │ thread_stream() │ │ │ │ → read RTSP req │ │ │ │ │ │ → parse & respond │ │ while(1) { │ │ │ │ → setup/play/ │ │ msi_get_fb() │ │ │ │ teardown │ │ ex->f() │ │ │ │ │ │ rtp_sendmsg() │ │ │ │ [FD事件] │ │ } │ │ │ │ [定时器]→RTCP │ └──────────────────┘ │ │ └──────────────────────┘ │ │ │ │ [硬件编码器线程] │ │ ┌──────────────────────┐ │ │ │ JPEG/H264 编码器 │ → framebuff │ │ │ → 写入 MSI 管道 │ → msi_get_fb() ← 发送线程 │ │ └──────────────────────┘ │ └──────────────────────────────────────────────────────────┘
|
10. 延迟分析与优化
10.1 延迟分布
1 2 3 4 5
| 数据路径延迟 (MJPEG 720P, ~25fps, 每帧40ms):
摄像头采集 ──┬── ISP处理 ──┬── JPEG编码 ──┬── MSI队列 ──┬── 发送线程 ──┬── RTP发送 ──┬── Wi-Fi │ (1-2ms) │ (5-15ms) │ (0-28帧) │ (0-1ms) │ (1-3ms) │ (1-5ms) │ │ │ ≈1.1s! │ │ │
|
10.2 延迟来源与优化
| 延迟源 |
代码位置 |
典型值 |
优化方案 |
| MSI 队列缓冲 |
frame.c:MAX_RTSP_JPG_RECV=28 |
0~1.1s |
改为 3~5,大幅降低延迟 |
| RTP 重试 |
rtp-jpeg.c:505 retries=30 |
0~300ms |
改为 0,丢包不重传 |
| 无主动丢帧 |
spook_send_thread_stream |
持续累积 |
发送前丢弃队列中旧帧 |
| JPEG 头重复解析 |
rtp-jpeg.c:jpeg_process_frame |
每帧1次 |
同分辨率只解析一次(已有init_done) |
| 音视频同线程 |
frame.c:spook_send_thread_stream |
相互阻塞 |
音频分离线程 |
| 空闲休眠 1ms |
frame.c os_sleep_ms(1) |
1ms |
改用事件唤醒 |
10.3 推荐优化措施
最简优化(改 3 处即可显著降低延迟):
frame.c — 减小 MSI 缓冲深度:
1
| #define MAX_RTSP_JPG_RECV 3
|
frame.c — 发送前丢弃旧帧,只保留最新帧:
1 2 3 4 5
| while (msi_peek_fb(jpg_rtsp->msi)) { msi_get_fb(jpg_rtsp->msi, 0); msi_delete_fb(NULL, fb); }
|
rtp-jpeg.c — 关闭 UDP 重试:
1 2
| rtp_sendmsg(ep, v, vcnt+1, out->timestamp, plen + i == out->scan_data_len, 0);
|
11. 常见问题与排查
Q: 客户端连不上 RTSP 服务器?
1 2 3 4 5 6 7
| 检查清单: □ 设备 Wi-Fi 是否启动 (AT+WIFIMODE? → 应为 AP) □ 手机/电脑是否连接到设备的 Wi-Fi (SSID: 82X_XXXXXX) □ IP 是否正确 (默认 192.168.169.1,或 AT+NETIP? 查看) □ 端口是否正确 (默认 554,Windows 需管理员权限) □ 防火墙是否阻止了端口 □ 串口日志是否有 accept/connection 打印
|
Q: 视频流卡顿或延迟高?
1 2 3 4 5 6
| 检查清单: □ 帧率统计: 串口打印 "cnt_num:xx" 是否 ≈ 25 □ 发送时间: 串口打印 "time:xx" 是否 < 50ms □ MSI 队列深度: MAX_RTSP_JPG_RECV 是否过大 □ Wi-Fi 信号强度: rssi 值 □ 编码器分辨率是否过高 (建议 720P 或以下)
|
Q: 画面花屏或无法解码?
1 2 3 4 5
| 可能原因: □ JPEG 数据损坏: 检查 DQT/SOF/DHT 解析是否正常 □ 量化表偏移错误: 检查 rtp-jpeg.c parse_DQT 中 quant 偏移计算 □ 分包边界未对齐: 检查 32 字节对齐逻辑 □ 网络丢包: UDP 模式下丢包导致花屏,建议使用 TCP 模式
|
12. 开发指南
12.1 添加新的 RTSP 路径
参考 /webcam 的实现,新增路径需要:
创建新文件 rtsp_xxx.c,实现:
self_creat() — 客户端首次 SETUP 时调用,创建 MSI 管道和线程
self_destory() — TEARDOWN 时调用,清理资源
self_rtsp_open() — DESCRIBE 时调用,设置 SDP 回调
在 spook_thread() 中注册:
1 2 3 4 5 6
| const rtp_name live_xxx = { .video_encode_name = ENCODER_NAME, .audio_encode_name = AUDIO_NAME, .path = "/xxx", }; rtsp_xxx_live_init(&live_xxx);
|
确保对应的编码器 stream 已通过 xxx_encode_init() 创建。
12.2 播放器兼容性
| 播放器 |
RTSP over TCP |
RTSP over UDP |
测试状态 |
| VLC |
✅ |
✅ |
推荐 |
| PotPlayer |
✅ |
✅ |
可用 |
| ffplay |
✅ |
✅ |
命令行测试 |
| 手机 VLC |
✅ |
✅ |
iOS/Android 均可 |
| Windows Media Player |
❌ |
❌ |
不支持 RTSP |
附录 A: 关键函数调用关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| spook_init() └─ spook_thread() ├─ init_random() ├─ global_init() → config_port(554) ├─ jpeg_encode_init() [encoder-jpeg.c] │ └─ new_stream() + new_exchanger() ├─ h264_encode_init() [encoder-h264.c] ├─ rtsp_audio_encode_init() [encoder-audio.c] ├─ rtsp_mjpeg_live_init() [rtsp_mjpeg_live.c] │ ├─ rtsp_start_block() │ ├─ rtsp_set_path() │ ├─ set_video_track() │ │ ├─ connect_to_stream() [stream.c] │ │ └─ new_rtp_media_jpeg_stream() [rtp-jpeg.c] │ ├─ set_audio_track() │ ├─ register_live_fn() │ └─ rtsp_end_block() ├─ rtsp_h264_live_init() [rtsp_h264_live.c] ├─ custom_rtsp_jpeg_init() [custom_rtsp_jpg.c] └─ rtsp_h264_live_init() [/loop/RECA/]
-- 客户端连接后 --
self_creat() [rtsp_mjpeg_live.c] ├─ msi_find(AUTO_JPG, 1) ← 找到 JPEG 编码器 MSI ├─ rtsp_msi_init() ← 创建接收 MSI ├─ msi_add_output() ← 连接编码器→接收器 └─ OS_TASK_INIT(self_thread) ← 创建发送线程
spook_send_thread_stream() [frame.c] └─ ex->f() └─ get_back_frame() [encoder-jpeg.c] └─ deliver_frame_to_stream() [stream.c] └─ rtsp_common_send() [rtsp_common.c] ├─ jpeg_process_frame() [rtp-jpeg.c] │ └─ parse_DQT/SOF/DHT/DRI └─ jpeg_send_more() [rtp-jpeg.c] └─ jpeg_send_frame_to_endpoint() └─ rtp_sendmsg() [rtp.c] ├─ rtp_send_udp() └─ rtp_send_tcp()
|
附录 B: 版本信息
| 项 |
内容 |
| 项目名称 |
Spook (轻量级 RTSP 服务器) |
| 原作者 |
Nathan Lutchansky lutchann@litech.org |
| 许可证 |
GPL v2 |
| 移植平台 |
TXW82x (C-SKY CK810 + lwIP) |
| 文档版本 |
1.0 |
| 文档日期 |
2026-06-30 |