hi3516cv610 4g RTSP推流 完整代码学习记录
本文代码由 AI 辅助生成,作者负责调试、验证和原理学习。5 个 Bug 均为实机排查,踩坑过程真实记录。
海思 HI3516 SDK 自带的 RTSP 示例只能做局域网拉流(板子当 server),想做 4G 公网推流(板子当 client 主动推到云端)没有现成方案。
网上找了一圈,要么依赖 ffmpeg/librtmp,要么代码不全。
本文记录RTSP client 的完整实现,代码直接改在 SDK 的 sample_venc.c 末尾,不改 Makefile,附上踩过的 5 个坑和调试经验。
最终效果:板子 → ML307 4G 模组 → 腾讯云 MediaMTX → 任意 PC ffplay 拉流,延迟 2~5 秒。
原方案(WiFi 拉流):
[PC ffplay] ──拉流──→ [HI3516 RTSP server] ──WiFi──→ 局域网
新方案(4G 推流):
[HI3516 RTSP client] ──主动推流──→ [ML307 4G] ──公网──→ [腾讯云 MediaMTX] ←── [PC ffplay]
视频演示:
123
1、weak 符号挂载(修改点:sample_comm_venc.c文件)
// sample_comm_venc.c(common 库,所有 sample 共享)
__attribute__((weak)) void sample_venc_rtsp_push(const unsigned char *nal, int len, long long pts){ (void)nal; (void)len; (void)pts; // 空实现,保证链接不报错}
- 为什么用 weak?不改 Makefile,不影响其它 sample
- 调用入口在哪?编码线程每产出一个 NAL 就调一次
if(0 == index){
for (i = 0; i < stream.pack_cnt; i++) {
/* H.265 流处理(本模块未使用) */
if(stream.pack[i].data_type.h265_type == OT_VENC_H265_NALU_SEI){
continue;
}else {
pStremData = (unsigned char*)stream.pack[i].addr + stream.pack[i].offset;
nSize =stream.pack[i].len - stream.pack[i].offset;
if(g_rtsplive) {
rtsp_sever_tx_video(g_rtsplive, session_h265, pStremData, nSize, stream.pack[i].pts);
}
}
/* H.264 流处理 —— 推流入口
*
* 编码线程每从编码器取出一个 H.264 NAL(可能是 SPS/PPS/IDR/P/SEI),
* 就调用一次 sample_venc_rtsp_push()。
*
* 实际调用的是 sample_venc.c 末尾定义的强符号(真正的推流实现),
* 而非本文件的 weak 空实现。
*
* 参数:
* pStremData - NAL 数据指针(可能带起始码 00 00 00 01)
* nSize - NAL 长度
* stream.pack[i].pts - 海思 SDK 给的时间戳(单位通常是 ms)
*/
if(stream.pack[i].data_type.h264_type == OT_VENC_H264_NALU_SEI){
continue; /* SEI 跳过,不送推流 */
}else {
pStremData = (unsigned char*)stream.pack[i].addr + stream.pack[i].offset;
nSize =stream.pack[i].len - stream.pack[i].offset;
if(g_rtsplive) {
rtsp_sever_tx_video(g_rtsplive, session_h264, pStremData, nSize, stream.pack[i].pts);
sample_venc_rtsp_push(pStremData, nSize, stream.pack[i].pts); /* ← 推流入口 */
}
}
}
}
2、RTSP主动推流代码流程
修改文件:/mpp/sample/venc/sample_venc.c
在文件末尾追加 374 行推流代码,实现 RTSP client 主动推流。
核心流程:
1. 编码线程每产出一个 NAL → 调用 `sample_venc_rtsp_push()
2. 跳过起始码 → 识别 NAL 类型(SPS/PPS/IDR/P)
3. 缓存 SPS/PPS → 遇到首个 IDR 触发 RTSP 握手
4. 握手(OPTIONS → ANNOUNCE → SETUP → RECORD)→ 开始推 RTP 包 **完整代码**见文末附件(带详细注释,可直接复制给 AI 解析)。
3、五个bug记录
Bug 1:推流函数从未触发(NAL type 恒为 0)
现象:
前台跑
sample_venc,[rtsp-push]无任何输出,netstat无连接。
排查过程:
- 先怀疑 weak 符号没被覆盖 →
nm sample_venc | grep rtsp_push看符号表- 发现强符号存在,但函数没被调用 → 加
printf在入口处- 打印出
nal[0] = 0x00,意识到数据带起始码
根因:
HI3516 SDK 输出的 H.264 裸流带 4 字节起始码
00 00 00 01,代码直接把nal[0]当 NAL header,type = 0x00 & 0x1f = 0,永远不进 SPS/PPS/IDR 分支。
修复:
code.c
// 跳过起始码再处理
if (len >= 4 && nal[0] == 0 && nal[1] == 0 && nal[2] == 0 && nal[3] == 1) {
nal += 4; len -= 4;
} else if (len >= 3 && nal[0] == 0 && nal[1] == 0 && nal[2] == 1) {
nal += 3; len -= 3;
}
服务器tcpdump抓包可以看到0x00。
Bug 2:RECORD 返回 400 Bad Request
现象:
握手走到第 5 步 RECORD 时,服务器直接返回
400 Bad Request然后 FIN 断连。前面 OPTIONS、ANNOUNCE、SETUP 都正常返回 200 OK。
排查过程:
- 第一反应是 RECORD 请求格式写错了 → 用
tcpdump -X port 8554抓包对比- 抓包发现 RECORD 的 Session 头是
Session: 2eb7e656;timeout=55- 查 RFC 2326 才发现,Session 头的值只能包含 session ID,不能带参数
- 回头看 SETUP 响应,服务器给的是
Session: 2eb7e656;timeout=55,我代码把整段都塞进去了
根因:
rtsp_push_init()里提取 Session 值时,没有截断分号后面的timeout=55参数。gortsplib(MediaMTX 底层库)校验严格,直接拒绝。
修复:
code.c
// 提取 Session 值时,遇到分号就截断
p = strstr(resp, "Session:");
if (p) {
p += 8;
while (*p == ' ' || *p == '\t') p++;
char *nl = strchr(p, '\r');
if (nl == NULL) nl = strchr(p, '\n');
if (nl == NULL) nl = p + strlen(p);
// 关键:只取到分号为止
char *semi = strchr(p, ';');
if (semi && semi < nl) nl = semi;
memcpy(session, p, nl - p);
session[nl - p] = 0;
}
教训:
RTSP 协议的头字段格式很严格,参数和值之间用分号隔开,提取时不能偷懒。抓包看原始响应比看代码猜更靠谱。
Bug 3:SETUP 成功但 RECORD 被拒
现象:
SETUP 返回 200 OK,但紧接着 RECORD 返回 503 Service Unavailable。MediaMTX 日志报
session not ready for recording。
排查过程:
- 对比 VLC 拉流时的 SETUP 请求,发现 VLC 的 Transport 头是
RTP/AVP/TCP;unicast;interleaved=0-1- 我写的也是这个,一模一样啊?
- 去翻 MediaMTX 源码(gortsplib),发现它要求推流模式的 SETUP 必须带
mode=record- 拉流模式(playback)不需要这个参数,但推流必须显式声明
根因:
RTSP 协议里,SETUP 的 Transport 头决定会话模式:
mode=play(默认):客户端拉流mode=record:客户端推流不带
mode=record,MediaMTX 默认当拉流会话处理,RECORD 请求自然被拒。
修复:
code.c
// SETUP 请求的 Transport 头加上 mode=record
snprintf(cmd, sizeof(cmd),
"SETUP %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Transport: RTP/AVP/TCP;unicast;interleaved=0-1;mode=record\r\n" // ← 加了 mode=record
"Session: %s\r\n"
"\r\n", track, (int)cseq++, session);
教训:
RTSP 的 Transport 头参数很多,
interleaved指定通道号,mode指定会话类型。推流和拉流的 SETUP 不一样,不能照搬 VLC 的参数。
Bug 4:FU-A 分片 "non-starting" 错误
现象:
MediaMTX 日志疯狂报
invalid FU-A packet (non-starting),拉流端花屏,几秒后断开。
排查过程:
- 先怀疑是 FU-A 分片逻辑写错了 → 打印每个分片的
FU indicator和FU header字节- 发现第一个分片的
FU indicator = 0x9C,二进制1001 1100- 查 RFC 6184,FU indicator 格式是
F(1bit) | NRI(2bit) | Type(5bit)
- F 位必须为 0
- Type = 28 表示 FU-A
- 我的
0x9C = 1001 1100,F 位 = 1(错!),Type = 28(对)- 再检查
rtp[2](sequence number 高字节),发现从未赋值,一直是 0- 还有 FU-A 循环里
seq没递增,所有分片用同一个序列号
根因:
三个子问题叠加:
fu_indicator = nri | 28 | 0x80,多写了| 0x80,导致 F 位 = 1rtp[2](seq 高字节)没赋值- FU-A 循环内
seq++漏了,所有分片序列号相同
修复:
// 1. FU indicator 去掉 0x80
unsigned char fu_indicator = (unsigned char)(nri | 28); // F=0, NRI, Type=28
// 2. 每个分片都要更新 seq
rtp[2] = (unsigned char)((g_rtsp_seq >> 8) & 0xff); // seq 高字节
rtp[3] = (unsigned char)(g_rtsp_seq & 0xff); // seq 低字节
// 3. 循环内 seq 递增
if (rtsp_push_send_rtp(rtp, plen) != 0) return -1;
g_rtsp_seq++; // ← 这行漏了
教训:
RTP 头的每个字节都有严格定义,F/NRI/Type 位不能乱来。FU-A 分片的序列号必须递增,否则接收端判丢包。这种位操作 bug 打印二进制看最直观。
Bug 5:SPS/PPS 未发导致 MediaMTX 解析失败
现象:
握手成功,RTP 包也发出去了,但 MediaMTX 日志报
no SPS/PPS in SDP,拉流端黑屏,几秒后超时断开。
排查过程:
- 先抓包看 ANNOUNCE 的 SDP,发现
sprop-parameter-sets字段是空的- 回头看代码,
g_sps_len和g_pps_len在 ANNOUNCE 时还是 0- 为什么?因为 SPS/PPS 是在编码启动后才产出的第一个 IDR 帧之前
- 我的逻辑是"遇到第一个 IDR 才触发握手",但此时 SPS/PPS 还没缓存
- 再抓包看 RTP 流,发现握手后直接发 IDR,没有 SPS/PPS 包
根因:
两个问题:
- ANNOUNCE 时 SPS/PPS 还没缓存,SDP 里带不上
- 握手完成后,代码没有主动补发 SPS/PPS,直接发 IDR,MediaMTX 无法初始化解码器
修复:
// 1. rtsp_push_init() 握手完成后,立即发送缓存的 SPS/PPS
g_rtsp_ready = 1;
printf("[rtsp-push] connected & publishing to %s\n", url);
// 关键:握手后先补发 SPS/PPS
if (g_sps_len > 0) rtsp_push_send_nal(g_sps, g_sps_len, 0);
if (g_pps_len > 0) rtsp_push_send_nal(g_pps, g_pps_len, 0);
return 0;
为什么 SPS/PPS 这么重要?
H.264 解码器必须先拿到 SPS(分辨率、帧率、profile)和 PPS(熵编码参数)才能开始解码。IDR 帧虽然是关键帧,但它的解码依赖 SPS/PPS。
RTSP 协议里,SPS/PPS 可以通过两种方式传递:
- ANNOUNCE 的 SDP 里带
sprop-parameter-sets(base64 编码)- 作为独立的 RTP 包发送(在 IDR 之前)
我的代码两种都做了:SDP 里带一份,握手后再补发一份,双保险。
教训:
H.264 流的顺序必须是
SPS → PPS → IDR → P → P → ...,不能乱。推流前一定要确认参数集已经传递给接收端。
4、直接加在sample_venc.c的完整代码,(和之前sample_comm_venc.c共计2个地方改动)
/* =========================================================================
* ===== [rtsp-push] 4G 主动推流模块 =====
*
* 功能:把 HI3516 编码出的 H.264 裸流,通过 RTSP 协议主动推送到云端
* MediaMTX 服务器,实现 4G 公网远程拉流。
*
* 架构:板子 --USB--> ML307 4G 模组 --4G--> 腾讯云 CVM (MediaMTX :8554)
*
* 协议栈:
* - RTSP (RFC 2326):文本协议,负责握手建流(OPTIONS/ANNOUNCE/SETUP/RECORD)
* - RTP (RFC 3550):二进制协议,负责传输音视频数据
* - H.264 (RFC 6184):视频编码格式,数据单元叫 NAL
*
* 传输方式:RTP over TCP (interleaved),即 RTP 包嵌在 RTSP 的 TCP 连接里,
* 用 0x24 ('$') 开头的 4 字节头标识。
*
* 衔接方式:本模块在 sample_venc.c 末尾定义强符号 sample_venc_rtsp_push(),
* 覆盖 sample_comm_venc.c 中的 weak 同名空函数。SDK 编码线程每
* 产出一个 NAL 就调用一次该入口。
* ========================================================================= */
#if !defined(__LITEOS__)
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
/* ========================= 配置宏 ========================= */
/* 服务器地址:你的腾讯云 CVM 公网 IP */
#define RTSP_PUSH_SERVER "your_server_ip"
/* RTSP 服务端口:MediaMTX 默认监听 8554,这是 RTSP 协议的惯例端口
* (类似 HTTP 的 80、HTTPS 的 443) */
#define RTSP_PUSH_PORT 8554
/* 推流路径:URL = rtsp://175.27.225.223:8554/live/cam
* MediaMTX 根据路径区分多路流,拉流端也要用同一路径。
* 路径由客户端 ANNOUNCE 时指定,服务端无需预先配置。 */
#define RTSP_PUSH_PATH "/live/cam"
/* ========================= H.264 NAL 类型 =========================
* H.264 编码出来的不是完整图片,而是一串 NAL 单元。
* 每个 NAL 以起始码 00 00 00 01(或 00 00 01)开头,
* 紧跟一个字节 header,其低 5 位表示 NAL 类型。
*
* 典型流顺序:SPS → PPS → IDR → P → P → IDR → ...
*
* NAL type | 宏名 | 英文全称 | 作用
* ---------+------------+-----------------------------------+-------------------
* 1 | VCL_N | Video Coding Layer - Non-IDR | 普通 P 帧
* 5 | IDR | Instantaneous Decoding Refresh | 关键帧(I 帧)
* 6 | SEI | Supplemental Enhancement Info | 附加信息,可丢弃
* 7 | SPS | Sequence Parameter Set | 视频序列参数
* 8 | PPS | Picture Parameter Set | 图像参数
* ============================================================ */
#define NAL_TYPE_SPS 7 /* 序列参数集:分辨率、帧率、profile/level 等全局参数 */
#define NAL_TYPE_PPS 8 /* 图像参数集:熵编码模式、去块滤波等参数 */
#define NAL_TYPE_IDR 5 /* 关键帧:可独立解码,不依赖前面的帧 */
#define NAL_TYPE_VCL_N 1 /* 普通视频帧(P 帧):依赖前面的 IDR/P 帧 */
#define NAL_TYPE_SEI 6 /* 补充增强信息(时间码、相机参数等),可丢弃 */
/* ========================= RTP 相关配置 ========================= */
/* RTP 时间戳增量:RTP 协议规定视频时钟频率固定为 90kHz(90000 tick/秒),
* 这是 RFC 3551 标准——90kHz 能整除大多数视频帧率(24/25/30/60fps)。
* 30fps 下每帧间隔 = 90000 / 30 = 3000 tick。 */
#define RTP_TS_TICK 3000
/* 单个 RTP 包 payload 上限(字节)。以太网 MTU 是 1500,减去 IP 头 20 +
* TCP 头 20 = 40,剩 1460。取 1400 留余量避免 IP 层分片。
* 超过此大小的 NAL 必须拆成多个 RTP 包(FU-A 分片)。 */
#define RTP_MAX_PL 1400
/* SSRC (Synchronization Source identifier):32 位 RTP 流标识符。
* 接收端靠它区分不同流。固定值 0x11223344 方便抓包时一眼认出自己的包。 */
#define RTSP_PUSH_RTP_SSRC 0x11223344u
/* ========================= 全局状态变量 ========================= */
/* TCP socket 文件描述符,-1 表示未连接 */
static int g_rtsp_fd = -1;
/* 握手完成标志:0=未握手,1=可以推流 */
static int g_rtsp_ready = 0;
/* 失败标志:握手失败后置 1,后续不再尝试(避免反复阻塞编码线程) */
static int g_rtsp_failed= 0;
/* SPS/PPS 缓存:编码启动后先收到 SPS/PPS,缓存下来用于 ANNOUNCE 的 SDP。
* 128 字节足够(通常 SPS 30~50 字节,PPS 几字节)。 */
static unsigned char g_sps[128]; static int g_sps_len = 0;
static unsigned char g_pps[128]; static int g_pps_len = 0;
/* RTP 时间戳(32 位),单位 1/90000 秒。接收端靠它还原播放节奏。 */
static unsigned int g_rtsp_ts = 0;
/* 上一帧 PTS (Presentation Time Stamp):SDK 给的时间戳,用于判断
* "当前 NAL 属于新一帧还是同一帧"。同一帧的多个 NAL 共享同一 RTP 时间戳。 */
static long long g_rtsp_last_vcl_pts = -1;
/* RTP 序列号(16 位,0~65535 循环):每个 RTP 包递增 1,
* 接收端靠它检测丢包和乱序。 */
static unsigned int g_rtsp_seq = 0;
/* ========================= 工具函数 ========================= */
/* ---------------- Base64 编码(RFC 4648) ----------------
*
* 作用:把二进制数据(如 SPS/PPS)转成可打印 ASCII 字符串,
* 以便放进 SDP 的 sprop-parameter-sets 字段。
*
* 为什么需要:SDP 是纯文本协议,SPS/PPS 是二进制,直接放会乱码。
*
* 原理:每 3 字节二进制 → 拆成 4 个 6bit 索引 → 查表转字符。
* 不足 3 字节时补 '=':剩 1 字节补 2 个 '=',剩 2 字节补 1 个 '='。
*
* 编码表(64 个字符):A-Z(0~25), a-z(26~51), 0-9(52~61), +(62), /(63)
*
* 示例:0x67 0x42 0x00 → 二进制 01100111 01000010 00000000
* 拆 6bit: 011001(25) 110100(52) 001000(8) 000000(0)
* 输出: Z 0 I A
*/
static void rtsp_push_b64encode(const unsigned char *in, int inlen,
char *out, int outcap)
{
static const char tb[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
int i, j = 0;
for (i = 0; i < inlen; ) {
unsigned int a = 0;
int left = inlen - i;
/* pad: 不足 3 字节时需要补几个 '=' */
int pad = (left < 3) ? (3 - left) : 0;
/* 把 1~3 字节拼成 24 位整数 */
a |= in[i] << 16;
if (left > 1) a |= in[i+1] << 8;
if (left > 2) a |= in[i+2];
/* 拆成 4 个 6bit 索引,查表输出 */
if (j < outcap) out[j++] = tb[(a >> 18) & 0x3f];
if (j < outcap) out[j++] = tb[(a >> 12) & 0x3f];
if (j < outcap) out[j++] = (pad > 1) ? '=' : tb[(a >> 6) & 0x3f];
if (j < outcap) out[j++] = (pad > 0) ? '=' : tb[a & 0x3f];
i += 3;
}
if (j < outcap) out[j] = 0; /* 字符串结尾 */
}
/* ---------------- 读 RTSP 响应头 ----------------
*
* RTSP 协议类似 HTTP,响应是纯文本,以空行(\r\n\r\n 或 \n\n)结尾。
* 例如:
* RTSP/1.0 200 OK\r\n
* CSeq: 1\r\n
* Session: 2eb7e656\r\n
* \r\n
*
* 本函数用 select() 做 3 秒超时读取,避免阻塞。
* 收到空行或超时即返回。
*/
static int rtsp_push_read_resp(int fd, char *hdr, int cap)
{
int n = 0;
fd_set rf;
struct timeval tv;
while (n < cap - 1) {
FD_ZERO(&rf); FD_SET(fd, &rf);
tv.tv_sec = 3; tv.tv_usec = 0;
/* select 等待数据可读,超时 3 秒 */
int r = select(fd + 1, &rf, NULL, NULL, &tv);
if (r <= 0) break; /* 超时或无数据 */
int c = (int)recv(fd, hdr + n, (size_t)((cap - 1) - n), 0);
if (c <= 0) break; /* 连接断开 */
n += c; hdr[n] = 0;
/* 收到空行,响应结束 */
if (strstr(hdr, "\r\n\r\n") || strstr(hdr, "\n\n")) break;
}
hdr[n] = 0;
return n;
}
/* ---------------- 发送一个 RTSP 文本请求并读响应 ----------------
*
* RTSP 请求是纯文本,例如:
* OPTIONS rtsp://175.27.225.223:8554/live/cam RTSP/1.0\r\n
* CSeq: 1\r\n
* \r\n
*
* send() 可能只发部分(尤其 4G 网络不稳定),循环发完。
* 发完立即读响应,是同步阻塞模型(嵌入式够用)。
*/
static int rtsp_push_cmd_once(int fd, const char *cmd,
char *resp_hdr, int resp_cap)
{
int total = (int)strlen(cmd);
int off = 0;
while (off < total) {
int w = (int)send(fd, cmd + off, (size_t)(total - off), 0);
if (w <= 0) return -1;
off += w;
}
rtsp_push_read_resp(fd, resp_hdr, resp_cap);
return 0;
}
static int rtsp_push_send_nal(const unsigned char *nal, int len, int marker_end);
/* ---------------- RTSP 握手初始化 ----------------
*
* RTSP 握手流程(推流端主动发起):
* 1. TCP connect —— 建立传输层连接
* 2. OPTIONS —— 询问服务器支持哪些方法
* 3. ANNOUNCE + SDP —— 告诉服务器"我要推什么流"(编码格式、SPS/PPS 等)
* 4. SETUP —— 协商传输参数(通道号、传输方式)
* 5. RECORD —— 开始推流
*
* 握手成功后,RTP 数据通过 interleaved 格式嵌在同一个 TCP 连接里发送。
* interleaved 格式:$ + channel(1字节) + length(2字节) + RTP包
*/
static int rtsp_push_init(void)
{
struct sockaddr_in sa;
struct hostent *he;
char resp[2048];
char sdp[2048];
char body[1024];
char cseq = 1; /* RTSP 序列号,每个请求递增,用于匹配请求/响应 */
char session[128]; /* 服务器分配的会话 ID,SETUP 后获取,RECORD 时带上 */
char url[256]; /* 完整推流 URL */
char track[256]; /* track URL = URL + "/track1" */
char *p;
/* ---- 步骤 1:建立 TCP 连接 ---- */
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_port = htons((unsigned short)RTSP_PUSH_PORT);
/* inet_pton 把字符串 IP 转成二进制,失败则尝试 DNS 解析 */
if (inet_pton(AF_INET, RTSP_PUSH_SERVER, &sa.sin_addr) != 1) {
he = gethostbyname(RTSP_PUSH_SERVER);
if (he == NULL) {
printf("[rtsp-push] resolve host failed: %s\n", RTSP_PUSH_SERVER);
return -1;
}
memcpy(&sa.sin_addr, he->h_addr_list[0], (size_t)he->h_length);
}
g_rtsp_fd = socket(AF_INET, SOCK_STREAM, 0); /* 创建 TCP socket */
if (g_rtsp_fd < 0) {
printf("[rtsp-push] socket() failed\n");
return -1;
}
if (connect(g_rtsp_fd, (struct sockaddr *)&sa, sizeof(sa)) != 0) {
printf("[rtsp-push] connect %s:%d failed\n",
RTSP_PUSH_SERVER, RTSP_PUSH_PORT);
close(g_rtsp_fd); g_rtsp_fd = -1;
return -1;
}
/* 构造 URL 和 track URL */
snprintf(url, sizeof(url), "rtsp://%s:%d%s",
RTSP_PUSH_SERVER, RTSP_PUSH_PORT, RTSP_PUSH_PATH);
snprintf(track, sizeof(track), "%s/track1", url);
session[0] = 0;
/* ---- 步骤 2:OPTIONS 请求 ----
* 询问服务器支持哪些方法(DESCRIBE/ANNOUNCE/SETUP/RECORD 等)。
* 服务器返回 200 OK + Allow 头列出支持的方法。
* 这里我们其实不需要解析响应,只是走个流程。 */
{
char cmd[512];
snprintf(cmd, sizeof(cmd),
"OPTIONS %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"User-Agent: HI3516-RTSP-Pusher\r\n"
"\r\n", url, (int)cseq++);
if (rtsp_push_cmd_once(g_rtsp_fd, cmd, resp, sizeof(resp)) != 0) goto fail;
}
/* ---- 步骤 3:ANNOUNCE 请求 + SDP 消息体 ----
*
* SDP (Session Description Protocol, RFC 4566) 是纯文本格式,
* 描述多媒体会话参数。关键字段:
* v=0 —— SDP 版本号
* o=- —— 会话发起者(- 表示无特定用户)
* s= —— 会话名称
* t=0 0 —— 时间描述(0 0 表示永久)
* m=video —— 媒体类型:video,端口 0(interleaved 模式不需要)
* a=rtpmap —— RTP 映射:96 号动态负载类型 → H264,时钟 90kHz
* a=fmtp —— 格式参数:packetization-mode=1 表示非零模式
* (支持 FU-A 分片),sprop-parameter-sets 携带
* base64 编码的 SPS 和 PPS
* a=control —— track 控制 URL
*/
{
char cmd[3000];
char b64s[512], b64p[512];
if (g_sps_len > 0 && g_pps_len > 0) {
/* 把 SPS/PPS 二进制数据 base64 编码成文本 */
rtsp_push_b64encode(g_sps, g_sps_len, b64s, sizeof(b64s));
rtsp_push_b64encode(g_pps, g_pps_len, b64p, sizeof(b64p));
snprintf(body, sizeof(body),
"v=0\r\n"
"o=- 0 0 IN IP4 127.0.0.1\r\n"
"s=HI3516 Push\r\n"
"t=0 0\r\n"
"m=video 0 RTP/AVP 96\r\n"
"a=rtpmap:96 H264/90000\r\n"
"a=fmtp:96 packetization-mode=1; sprop-parameter-sets=%s,%s\r\n"
"a=control:track1\r\n",
b64s, b64p);
} else {
/* 没拿到 SPS/PPS 时退化为不带参数集的 SDP */
snprintf(body, sizeof(body),
"v=0\r\n"
"o=- 0 0 IN IP4 127.0.0.1\r\n"
"s=HI3516 Push\r\n"
"t=0 0\r\n"
"m=video 0 RTP/AVP 96\r\n"
"a=rtpmap:96 H264/90000\r\n"
"a=control:track1\r\n");
}
snprintf(cmd, sizeof(cmd),
"ANNOUNCE %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Content-Type: application/sdp\r\n"
"Content-Length: %d\r\n"
"User-Agent: HI3516-RTSP-Pusher\r\n"
"\r\n%s",
url, (int)cseq++, (int)strlen(body), body);
if (rtsp_push_cmd_once(g_rtsp_fd, cmd, resp, sizeof(resp)) != 0) goto fail;
}
/* ---- 步骤 4:SETUP 请求 ----
*
* 协商传输参数:
* Transport: RTP/AVP/TCP;unicast;interleaved=0-1;mode=record
* - RTP/AVP/TCP —— RTP over TCP(不是 UDP)
* - unicast —— 单播
* - interleaved=0-1 —— RTP 数据走 channel 0,RTCP 走 channel 1
* - mode=record —— 推流模式(gortsplib 要求,否则 RECORD 会被拒)
*
* 服务器返回 Session 头,格式如:Session: 2eb7e656;timeout=55
* 我们需要提取 session ID(到分号为止),后续 RECORD 要带上。
*/
{
char cmd[512];
snprintf(cmd, sizeof(cmd),
"SETUP %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Transport: RTP/AVP/TCP;unicast;interleaved=0-1;mode=record\r\n"
"Session: %s\r\n"
"User-Agent: HI3516-RTSP-Pusher\r\n"
"\r\n", track, (int)cseq++, session);
if (rtsp_push_cmd_once(g_rtsp_fd, cmd, resp, sizeof(resp)) != 0) goto fail;
}
/* 从 SETUP 响应中提取 Session ID */
p = strstr(resp, "Session:");
if (p) {
p += 8;
while (*p == ' ' || *p == '\t') p++; /* 跳过空白 */
char *nl = strchr(p, '\r');
if (nl == NULL) nl = strchr(p, '\n');
if (nl == NULL) nl = p + strlen(p);
/* 关键修复:Session 头可能带 ;timeout=xx 参数,只取到分号为止。
* 此前把整段 "2eb7e656;timeout=55" 塞进 RECORD 的 Session 头,
* 导致 gortsplib 返回 400 Bad Request。 */
char *semi = strchr(p, ';');
if (semi && semi < nl) nl = semi;
if (nl - p < (int)sizeof(session)) {
memcpy(session, p, (size_t)(nl - p));
session[nl - p] = 0;
}
}
/* ---- 步骤 5:RECORD 请求 ----
*
* 告诉服务器"开始推流了"。必须带 Session 头。
* Range: npt=0.000- 表示从当前位置开始(npt = Normal Play Time)。
* 服务器返回 200 OK 后,就可以发 RTP 数据了。
*/
{
char cmd[512];
snprintf(cmd, sizeof(cmd),
"RECORD %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Session: %s\r\n"
"Range: npt=0.000-\r\n"
"User-Agent: HI3516-RTSP-Pusher\r\n"
"\r\n", url, (int)cseq++, session);
if (rtsp_push_cmd_once(g_rtsp_fd, cmd, resp, sizeof(resp)) != 0) goto fail;
}
/* 握手成功,初始化状态 */
g_rtsp_seq = 0;
g_rtsp_ready = 1;
printf("[rtsp-push] connected & publishing to %s:\n", url);
/* 握手完成后立即发送 SPS/PPS,让 MediaMTX 初始化解码器。
* 此前直接推 IDR 导致 MediaMTX 无法解析(Bug 5)。 */
if (g_sps_len > 0) rtsp_push_send_nal(g_sps, g_sps_len, 0);
if (g_pps_len > 0) rtsp_push_send_nal(g_pps, g_pps_len, 0);
return 0;
fail:
close(g_rtsp_fd);
g_rtsp_fd = -1;
printf("[rtsp-push] RTSP handshake failed\n");
return -1;
}
/* ---------------- 发送一个 RTP 包(interleaved 格式) ----------------
*
* RTP over TCP 的传输格式(RFC 2326 第 10.12 节):
* $ + channel(1字节) + length(2字节, big-endian) + RTP包数据
*
* channel:0 表示 RTP 数据,1 表示 RTCP 控制。这里用 0。
* length:RTP 包长度,2 字节大端序(高字节在前)。
*
* 示例:发送一个 100 字节的 RTP 包
* 24 00 00 64 [100字节RTP数据]
*/
static int rtsp_push_send_rtp(const unsigned char *rtp, int rtp_len)
{
static unsigned char out[4 + 12 + RTP_MAX_PL + 2];
if (g_rtsp_fd < 0) return -1;
if (rtp_len > (int)sizeof(out) - 4) rtp_len = (int)sizeof(out) - 4;
out[0] = 0x24; /* '$' 标识 interleaved 帧开始 */
out[1] = 0; /* channel 0:RTP 数据 */
out[2] = (unsigned char)((rtp_len >> 8) & 0xff); /* length 高字节 */
out[3] = (unsigned char)(rtp_len & 0xff); /* length 低字节 */
memcpy(out + 4, rtp, (size_t)rtp_len);
return (send(g_rtsp_fd, (const char *)out, 4 + rtp_len, 0) == 4 + rtp_len) ? 0 : -1;
}
/* ---------------- 把单个 NAL 打成 RTP 包发送 ----------------
*
* 两种情况:
* 1. NAL 长度 <= RTP_MAX_PL (1400):单包发送,payload 就是整个 NAL
* 2. NAL 长度 > 1400:FU-A 分片发送(RFC 6184 第 5.3 节)
*
* RTP 头格式(12 字节):
* byte 0: V(2bit)=2 | P(1) | X(1) | CC(4bit)
* byte 1: M(1bit) | PT(7bit)=96
* byte 2-3: sequence number (16bit)
* byte 4-7: timestamp (32bit)
* byte 8-11: SSRC (32bit)
*
* FU-A 分片格式(用于大 NAL):
* RTP头(12字节) + FU indicator(1字节) + FU header(1字节) + 分片数据
*
* FU indicator: F(1bit)=0 | NRI(2bit) | Type(5bit)=28(FU-A)
* FU header: S(1bit,起始) | E(1bit,结束) | R(1bit)=0 | Type(5bit,原NAL类型)
*/
static int rtsp_push_send_nal(const unsigned char *nal, int len, int marker_end)
{
unsigned char rtp[12 + RTP_MAX_PL + 2];
unsigned char nalh = nal[0]; /* NAL header 第一个字节 */
unsigned int ts = g_rtsp_ts;
/* ---- 构造 RTP 头(12 字节) ---- */
rtp[0] = 0x80; /* V=2, P=0, X=0, CC=0 */
rtp[1] = 96; /* PT=96(动态负载类型,H.264) */
rtp[2] = (unsigned char)((g_rtsp_seq >> 8) & 0xff); /* seq 高字节 */
rtp[3] = (unsigned char)(g_rtsp_seq & 0xff); /* seq 低字节 */
rtp[4] = (unsigned char)((ts >> 24) & 0xff); /* timestamp 字节0 */
rtp[5] = (unsigned char)((ts >> 16) & 0xff); /* timestamp 字节1 */
rtp[6] = (unsigned char)((ts >> 8) & 0xff); /* timestamp 字节2 */
rtp[7] = (unsigned char)(ts & 0xff); /* timestamp 字节3 */
rtp[8] = (unsigned char)(RTSP_PUSH_RTP_SSRC >> 24); /* SSRC 字节0 */
rtp[9] = (unsigned char)(RTSP_PUSH_RTP_SSRC >> 16); /* SSRC 字节1 */
rtp[10] = (unsigned char)(RTSP_PUSH_RTP_SSRC >> 8); /* SSRC 字节2 */
rtp[11] = (unsigned char)(RTSP_PUSH_RTP_SSRC); /* SSRC 字节3 */
/* ---- 情况 1:NAL 较小,单包发送 ---- */
if (len <= RTP_MAX_PL) {
int plen = 12 + len;
if (marker_end) rtp[1] |= 0x80; /* 设置 M 位(Marker bit) */
memcpy(rtp + 12, nal, (size_t)len);
if (rtsp_push_send_rtp(rtp, plen) != 0) return -1;
g_rtsp_seq++;
return 0;
}
/* ---- 情况 2:NAL 较大,FU-A 分片 ----
*
* 一个 NAL 拆成多个 RTP 包,每个包带 2 字节额外头(FU indicator + FU header)。
* 第一个分片:S=1, E=0
* 中间分片: S=0, E=0
* 最后一个: S=0, E=1
*
* 注意:原 NAL header 不直接发送,而是拆开:
* - NRI 位放进 FU indicator
* - Type 位放进 FU header
*/
{
int off = 1; /* 跳过原 NAL header(第 0 字节) */
int nri = nalh & 0x60; /* NRI: 2 bit,表示 NAL 重要性 */
int t5 = nalh & 0x1f; /* Type: 5 bit,原 NAL 类型 */
while (off < len) {
int chunk = len - off;
if (chunk > RTP_MAX_PL - 2) chunk = RTP_MAX_PL - 2; /* 留 2 字节给 FU 头 */
int first = (off == 1);
int last = (off + chunk >= len);
int plen = 12 + 2 + chunk;
/* FU indicator: F=0 | NRI | Type=28(FU-A)
* 此前代码多写了 | 0x80,导致 F=1,MediaMTX 报错(Bug 4) */
unsigned char fu_indicator = (unsigned char)(nri | 28);
/* FU header: S(起始) | E(结束) | R=0 | Type */
unsigned char fu_header = (unsigned char)((first ? 0x80 : 0) | t5);
if (last) fu_header |= 0x40; /* 设置 E 位(结束) */
if (last && marker_end) rtp[1] |= 0x80; /* 最后一个分片设 M 位 */
rtp[12] = fu_indicator;
rtp[13] = fu_header;
memcpy(rtp + 14, nal + off, (size_t)chunk);
/* 每个分片 seq 必须递增,否则接收端判丢包(Bug 4 修复) */
rtp[2] = (unsigned char)((g_rtsp_seq >> 8) & 0xff);
rtp[3] = (unsigned char)(g_rtsp_seq & 0xff);
if (rtsp_push_send_rtp(rtp, plen) != 0) return -1;
g_rtsp_seq++;
off += chunk;
if (last) break;
}
}
return 0;
}
/* ---------------- 主入口:编码线程每产出一个 NAL 调用一次 ----------------
*
* 衔接方式:本函数是强符号,覆盖 sample_comm_venc.c 中的 weak 同名空函数。
* SDK 编码线程每取到一个 H.264 NAL 就调用一次。
*
* 处理流程:
* 1. 跳过 NAL 起始码(00 00 00 01 或 00 00 01)
* 2. 识别 NAL 类型(SPS/PPS/IDR/P/SEI)
* 3. 缓存 SPS/PPS(用于 ANNOUNCE 的 SDP)
* 4. 遇到第一个 IDR 且 SPS/PPS 齐全时,触发 RTSP 握手
* 5. 把 NAL 打成 RTP 包发送
*/
void sample_venc_rtsp_push(const unsigned char *nal, int len, long long pts)
{
int type;
if (nal == NULL || len <= 0) return;
if (g_rtsp_failed) return;
/* 步骤 1:跳过 NAL 起始码
*
* HI3516 SDK 输出的 H.264 裸流带起始码(00 00 00 01 或 00 00 01),
* 但 RTP 打包时只需要 NAL 数据本身。
*
* 此前代码没跳过起始码,导致 nal[0]=0x00,type = 0x00 & 0x1f = 0,
* 永远不进 SPS/PPS/IDR 分支,推流函数从未真正触发(Bug 1)。 */
if (len >= 4 && nal[0] == 0 && nal[1] == 0 && nal[2] == 0 && nal[3] == 1) {
nal += 4; len -= 4;
} else if (len >= 3 && nal[0] == 0 && nal[1] == 0 && nal[2] == 1) {
nal += 3; len -= 3;
}
if (len <= 0) return;
/* 步骤 2:识别 NAL 类型(取 NAL header 低 5 位) */
type = nal[0] & 0x1f;
/* 步骤 3:缓存 SPS/PPS(用于 ANNOUNCE 的 SDP)
* 注意:缓存后不 return,SPS/PPS 也要作为 RTP 包发出去 */
if (type == NAL_TYPE_SPS && len <= sizeof(g_sps)) {
memcpy(g_sps, nal, len); g_sps_len = len;
} else
if (type == NAL_TYPE_PPS && len <= sizeof(g_pps)) {
memcpy(g_pps, nal, len); g_pps_len = len;
} else
if (type == NAL_TYPE_SEI) return; /* SEI 是附加信息,丢弃 */
/* 步骤 4:VCL 帧(IDR/P 帧)推进 RTP 时间戳
*
* 同一帧可能拆成多个 NAL(如 IDR + SEI),它们 PTS 相同,
* 共享同一个 RTP 时间戳。只有 PTS 变化时才递增时间戳。 */
if (type == NAL_TYPE_IDR || type == NAL_TYPE_VCL_N) {
if (g_rtsp_last_vcl_pts != pts) {
g_rtsp_ts += RTP_TS_TICK; /* 递增 3000 tick(1/30 秒) */
g_rtsp_last_vcl_pts = pts;
}
}
/* 步骤 5:首次遇到关键帧且参数集齐全时,触发 RTSP 握手
*
* 为什么等 IDR?因为解码器必须从关键帧开始才能解码。
* 为什么等 SPS/PPS?因为 ANNOUNCE 的 SDP 需要它们。 */
if (!g_rtsp_ready) {
if (type != NAL_TYPE_IDR) return; /* 不是关键帧,继续等 */
if (g_sps_len <= 0 || g_pps_len <= 0) return; /* 参数未齐,继续等 */
if (rtsp_push_init() != 0) {
g_rtsp_failed = 1;
printf("[rtsp-push] init failed, disabled\n");
return;
}
}
/* 步骤 6:把 NAL 打成 RTP 包发送
*
* marker_end:RTP 的 M 位,表示一帧结束。
* IDR 帧通常独立成帧,设 M=1;非 IDR 帧暂不设。 */
{
int is_idr = (type == NAL_TYPE_IDR);
int marker_end = 0;
if (is_idr) marker_end = 1;
rtsp_push_send_nal(nal, len, marker_end);
}
}
#endif /* !__LITEOS__ */
更多推荐

所有评论(0)