本文代码由 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 无连接。

排查过程:

  1. 先怀疑 weak 符号没被覆盖 → nm sample_venc | grep rtsp_push 看符号表
  2. 发现强符号存在,但函数没被调用 → 加 printf 在入口处
  3. 打印出 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。

排查过程:

  1. 第一反应是 RECORD 请求格式写错了 → 用 tcpdump -X port 8554 抓包对比
  2. 抓包发现 RECORD 的 Session 头是 Session: 2eb7e656;timeout=55
  3. 查 RFC 2326 才发现,Session 头的值只能包含 session ID,不能带参数
  4. 回头看 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。

排查过程:

  1. 对比 VLC 拉流时的 SETUP 请求,发现 VLC 的 Transport 头是 RTP/AVP/TCP;unicast;interleaved=0-1
  2. 我写的也是这个,一模一样啊?
  3. 去翻 MediaMTX 源码(gortsplib),发现它要求推流模式的 SETUP 必须带 mode=record
  4. 拉流模式(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),拉流端花屏,几秒后断开。

排查过程:

  1. 先怀疑是 FU-A 分片逻辑写错了 → 打印每个分片的 FU indicator 和 FU header 字节
  2. 发现第一个分片的 FU indicator = 0x9C,二进制 1001 1100
  3. 查 RFC 6184,FU indicator 格式是 F(1bit) | NRI(2bit) | Type(5bit)
    • F 位必须为 0
    • Type = 28 表示 FU-A
  4. 我的 0x9C = 1001 1100,F 位 = 1(错!),Type = 28(对)
  5. 再检查 rtp[2](sequence number 高字节),发现从未赋值,一直是 0
  6. 还有 FU-A 循环里 seq 没递增,所有分片用同一个序列号

根因:

三个子问题叠加:

  1. fu_indicator = nri | 28 | 0x80,多写了 | 0x80,导致 F 位 = 1
  2. rtp[2](seq 高字节)没赋值
  3. 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,拉流端黑屏,几秒后超时断开。

排查过程:

  1. 先抓包看 ANNOUNCE 的 SDP,发现 sprop-parameter-sets 字段是空的
  2. 回头看代码,g_sps_len 和 g_pps_len 在 ANNOUNCE 时还是 0
  3. 为什么?因为 SPS/PPS 是在编码启动后才产出的第一个 IDR 帧之前
  4. 我的逻辑是"遇到第一个 IDR 才触发握手",但此时 SPS/PPS 还没缓存
  5. 再抓包看 RTP 流,发现握手后直接发 IDR,没有 SPS/PPS 包

根因:

两个问题:

  1. ANNOUNCE 时 SPS/PPS 还没缓存,SDP 里带不上
  2. 握手完成后,代码没有主动补发 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 可以通过两种方式传递:

  1. ANNOUNCE 的 SDP 里带 sprop-parameter-sets(base64 编码)
  2. 作为独立的 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__ */

Logo

海思 HiSpark 开发者社区是上海海思为开发者搭建的内容和交流平台,依托完备芯片解决方案、专业开发环境、开源/开放的软件资源与社区技术内容,构建完整服务生态开发者的闭环体系。

更多推荐