深度解析 RK3518 平台 FFmpeg 多协议归一拉流:MEDAI-W2 拉流解码器 RTMP/RTSP/M3U8/FLV 统一接入与归一化设计实现
产品背景 :MEDAI-W2 是一款基于瑞芯微 RK3518 平台的拉流解码器,可通过网络拉取 RTSP/RTMP 等流媒体信号,解码后输出到 HDMI,并支持 UVC 与 NET1 两路视频通道。设备内置 Web 管理系统(固件版本 2.1),在"解码设置"首页的 网络拉流 区(说明书 3.4 节)支持按通道配置拉流地址(如
rtsp://192.168.1.87/live/stream1)、开关与【同步配置】。前文已解析 UVC 输入 × MPP 硬解码、V4L2/UVC 框架、OpenGL 绿幕抠图 + DRM 送显 与 RGA + DRM HDMI 输出 四条链路。本文回到最上游的"接入层" :如何用 FFmpeg 把 RTMP / RTSP / HLS(M3U8) / FLV 四种协议统一收敛为一条归一化的码流,再对接 MPP 硬解与下游显示。这是 MEDAI-W2"一个地址框、任意协议都能拉"体验的技术根基。
一、多协议归一的设计动机
1.1 四种协议到底差在哪
MEDAI-W2 要面对的信源千差万别,但它们在"接入层"呈现出四种典型形态:
| 协议 | URL 形态 | 传输方式 | 典型容器 | 端到端延迟 | 典型场景 |
|---|---|---|---|---|---|
| RTMP | rtmp://host/app/stream |
TCP 长连 | FLV | 低(1~3s) | 推流/拉流直播 |
| RTSP | rtsp://host:554/... |
TCP/UDP(可交织) | RTP | 低(0.5~2s) | IPC/摄像机/NVR(说明书 FAQ Q2 提及 554 端口) |
| HLS(M3U8) | http(s)://host/xx.m3u8 |
HTTP 分片(TS) | M3U8+TS | 高(6~30s) | CDN 分发、跨防火墙 |
| FLV | flv://host/... 或 http://host/xx.flv(HTTP-FLV) |
TCP | FLV | 低(1~3s) | 低延迟直播(FLV over HTTP) |
它们在传输协议、容器封装、分片策略、时间基准、码流帧格式 上完全不同------但 MEDAI-W2 的下游(MPP 硬解 → RGA/DRM/OpenGL → HDMI)只关心一件事:一份归一化的、带正确时间戳的压缩视频 ES(Elementary Stream)。
1.2 为什么要在 RK3518 上做"归一"
- 用户体验:说明书 3.4 节只给用户一个"拉流地址"输入框,背后必须自动识别是 RTMP 还是 RTSP 还是 M3U8,用户无需关心协议细节;
- 代码复用:四种协议的接入若各写一套,维护成本高、bug 多;归一后下游只有一条处理链路(与《MPP 硬解》系列一致);
- 资源约束:RK3518 是嵌入式平台,CPU 仅四核 A53,绝不能把"协议差异"的复杂度转嫁到逐帧处理上------接入层要轻、要稳。
1.3 在软件栈中的位置
┌──────────────────────────────────────────────────┐
│ 应用层:MEDAI-W2 视频服务(拉流 + 解码 + 显示) │
├──────────────────────────────────────────────────┤
│ FFmpeg(libavformat) ── 协议归一 / 解封装 / 时间戳 │
│ ↓ 归一化 AVPacket(视频 ES) │
│ MPP(VPU 硬解) ── 见《UVC×MPP》系列 │
│ ↓ NV12(dmabuf) │
│ RGA / OpenGL ── 见《RGA+DRM》《OpenGL 抠图》系列 │
│ ↓ │
│ DRM/KMS ── HDMI1 输出 │
├──────────────────────────────────────────────────┤
│ 内核:rockchip-rga / mpp_service / rockchip-drm │
├──────────────────────────────────────────────────┤
│ 硬件:RK3518 VPU / RGA / VOP / HDMI │
└──────────────────────────────────────────────────┘
FFmpeg 是"归一前门",MPP 是"硬解后门",中间以一份归一化的 AVPacket 衔接------这正是本系列各篇文章能拼成完整链路的关键接缝。
二、FFmpeg 归一架构设计
2.1 统一 Source 抽象
把"一个拉流通道"抽象为一个 stream_source_t,对上层只暴露三个动作:
┌─────────────────────────────────────────────┐
│ stream_source_t(每通道一个) │
│ │
│ source_open(url) → 识别协议、开流、建 BSF │
│ source_read(pkt) → 取一帧归一化 AVPacket │
│ source_close() → 释放、准备重连 │
│ │
│ 内部:AVFormatContext + AVBSFContext + 重连态 │
└─────────────────────────────────────────────┘
↑ 上层(MPP 桥/多通道管理)只看到这三个接口
MEDAI-W2 的多通道(NET1 等)就是多个 stream_source_t 实例,每个实例走完全相同的代码路径------这正是说明书 3.4 节"通道名称 + 拉流地址 + 开关"能平行扩展的原因。
2.2 协议自动识别与按需 AVOption
FFmpeg 的 avformat_open_input 会根据 URL scheme 与探测自动选择 demuxer,大多数情况下无需手动指定格式。但不同协议需要不同的健壮性选项,否则在嵌入式弱网下极易卡死:
| 协议 | 关键 AVOption | 目的 |
|---|---|---|
| RTSP | rtsp_transport=tcp |
走 TCP 交织,避免 UDP 丢包/防火墙阻断 |
| RTSP | stimeout=3000000 |
socket 超时 3s(微秒),防永久阻塞 |
| HLS | http_persistent=1 |
复用 HTTP 连接,减少握手开销 |
| HLS | timeout=5000000 |
分片拉取超时 5s |
| RTMP | rtmp_buffer=1000 |
1s 接收缓冲,抗抖动 |
| RTMP | rtmp_live=live |
按直播模式处理 |
| FLV | (通常无需特殊项) | HTTP-FLV 直接走 http 协议即可 |
经验:RTSP 在公网/跨网段几乎必须
rtsp_transport=tcp,否则 UDP 包常被丢弃导致"拉到流但无画面"(说明书 FAQ Q2 的同类现象)。
2.3 时间戳归一化
四种协议的时间基准(time_base)和 PTS 方案各异,下游 MPP 与显示时钟必须一个统一时钟。归一策略:
- 基准转换 :用
av_rescale_q(pts, stream->time_base, AV_TIME_BASE_Q)把所有 PTS 统一到微秒; - 缺值兜底 :FLV/部分 RTSP 可能
AV_NOPTS_VALUE,用 DTS 或单调递增计数器兜底; - 不连续修正:HLS/TS 的 33-bit PTS 回绕、换流时的跳变,需用偏移量修正,避免画面倍速/慢放(与《UVC×MPP》3.3 节"PTS 重基"同理);
- 单调性保证:送解码前做一次轻量排序/夹紧,防止乱序包导致 VPU 出帧异常。
2.4 码流帧格式归一(容易被忽略的一环)
协议被解封装后,视频 ES 的封装格式还可能不一致:
- RTSP / FLV / RTMP 的 H.264 通常是 Annex-B (起始码
00 00 00 01); - 但经 MP4/部分封装的 H.264 是 AVCC(长度前缀),HEVC 可能是 HVCC。
MPP VPU 硬解要求输入为规范的 ES(Annex-B)。归一手段是 FFmpeg 比特流过滤器(BSF):
c
/* H.264: AVCC → Annex-B;HEVC: HVCC → Annex-B */
if (par->codec_id == AV_CODEC_ID_H264)
init_bsf(&s->bsf, "h264_mp4toannexb", par);
else if (par->codec_id == AV_CODEC_ID_HEVC)
init_bsf(&s->bsf, "hevc_mp4toannexb", par);
这一步让"协议归一"真正完整------不仅传输层归一,码流层也归一,下游 MPP 永远拿到一致的 ES。
2.5 断线重连与退避
网络流天然不稳定,MEDAI-W2 必须"无声自愈"。重连触发条件:
av_read_frame返回AVERROR_EOF(对流结束)/AVERROR(EIO)(IO 错误)/AVERROR(EAGAIN)(暂时不可用);- 停滞检测:连续 N 秒未取到视频帧(弱网半死状态),主动断开重连。
策略:指数退避(BASE 1s → 翻倍 → 封顶 30s),避免风暴式重连打爆对端;重连成功后重置退避。这与说明书"拉流开关已开却无画面"的排障逻辑互补------设备侧先自救,自救失败才在 Web 端暴露"未连接"。
2.6 缓冲与显示时钟解耦
网络抖动(突发/空窗)与 HDMI 显示时钟(匀速)天然冲突。归一层引入一个轻量抖动缓冲:
- 缓冲深度通常 4~8 帧(与《MPP 硬解》3.4 节队列深度一致),兼顾延迟与抗抖;
- 下游显示按输出时钟匀速取帧,重连/卡顿时用上一帧或黑场过渡,避免花屏/冻帧。
三、参考实现
3.1 统一打开(协议识别 + 按需选项 + BSF 建链)
c
/* MEDAI-W2:FFmpeg 统一拉流源 ------ 识别协议、设选项、建码流归一 BSF */
typedef struct {
AVFormatContext *fmt;
AVBSFContext *bsf; /* 码流帧格式归一(AVCC→Annex-B) */
int video_idx;
char url[512];
} stream_source_t;
int source_open(stream_source_t *s, const char *url)
{
AVDictionary *opts = NULL;
if (strncmp(url, "rtsp://", 7) == 0) {
av_dict_set(&opts, "rtsp_transport", "tcp", 0);
av_dict_set(&opts, "stimeout", "3000000", 0); /* 3s */
} else if (strstr(url, ".m3u8")) {
av_dict_set(&opts, "http_persistent", "1", 0);
av_dict_set(&opts, "timeout", "5000000", 0); /* 5s */
} else if (strncmp(url, "rtmp://", 7) == 0) {
av_dict_set(&opts, "rtmp_buffer", "1000", 0);
av_dict_set(&opts, "rtmp_live", "live", 0);
}
/* flv / http-flv 无需特殊项 */
if (avformat_open_input(&s->fmt, url, NULL, &opts) < 0) return -1;
if (avformat_find_stream_info(s->fmt, NULL) < 0) return -1;
s->video_idx = av_find_best_stream(s->fmt, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
if (s->video_idx < 0) return -1;
AVCodecParameters *par = s->fmt->streams[s->video_idx]->codecpar;
if (par->codec_id == AV_CODEC_ID_H264)
init_bsf(&s->bsf, "h264_mp4toannexb", par);
else if (par->codec_id == AV_CODEC_ID_HEVC)
init_bsf(&s->bsf, "hevc_mp4toannexb", par);
return 0;
}
3.2 读取循环 + 时间戳/码流归一
c
/* 取一帧:码流归一(Annex-B) + 时间戳归一(微秒) */
int source_read(stream_source_t *s, AVPacket *pkt)
{
int ret = av_read_frame(s->fmt, pkt);
if (ret < 0) return ret; /* EOF/错误 → 触发重连 */
if (pkt->stream_index == s->video_idx) {
if (s->bsf) { /* 码流帧格式归一 */
av_bsf_send_packet(s->bsf, pkt);
av_bsf_receive_packet(s->bsf, pkt);
}
if (pkt->pts == AV_NOPTS_VALUE) /* 缺值兜底 */
pkt->pts = (pkt->dts == AV_NOPTS_VALUE) ? next_monotonic_us() : pkt->dts;
/* 时间戳归一:各协议 time_base → 微秒 */
pkt->pts = av_rescale_q(pkt->pts,
s->fmt->streams[s->video_idx]->time_base,
AV_TIME_BASE_Q);
}
return 0;
}
3.3 重连状态机(指数退避)
c
/* 简化重连循环:错误/停滞 → 关闭 → 退避 → 重开 */
int source_loop(stream_source_t *s)
{
int64_t last_frame_ms = now_ms();
int backoff_us = 1000000; /* BASE 1s */
const int MAX_US = 30000000; /* 封顶 30s */
while (running) {
AVPacket pkt;
int ret = source_read(s, &pkt);
if (ret < 0 || now_ms() - last_frame_ms > STALL_MS) {
/* 错误 或 停滞 → 重连 */
source_close(s);
if (source_open(s, s->url) < 0) {
backoff_us = min(backoff_us * 2, MAX_US); /* 退避翻倍 */
av_usleep(backoff_us);
continue;
}
backoff_us = 1000000; /* 成功 → 复位 */
last_frame_ms = now_ms();
continue;
}
last_frame_ms = now_ms();
push_to_decoder(&pkt); /* 见 3.4 桥接 MPP */
}
return 0;
}
3.4 归一码流桥接到 MPP(与《UVC×MPP》系列衔接)
c
/* AVPacket → MppPacket(对接《UVC 输入 × MPP 硬解码》3.5 节解码循环) */
void push_to_decoder(AVPacket *pkt)
{
MppPacket mpkt;
mpp_packet_init(&mpkt, pkt->data, pkt->size);
mpp_packet_set_pts(mpkt, pkt->pts); /* 已归一为微秒 */
mpi->decode_put_packet(ctx, mpkt); /* 送 VPU 硬解 → NV12 */
mpp_packet_deinit(&mpkt);
/* 零拷贝优化(量产可启用):FFmpeg 的 pkt->buf 可通过
av_buffer_get_opaque / dmabuf 导出为 fd,直接以 dmabuf 喂 MPP,
省去一次 memcpy,与全系列"零拷贝"主线一致。 */
}
四、与 Web 管理系统 / 说明书的映射
- 网络拉流区(说明书 3.4 节) :用户填写"通道名称 + 拉流地址 + 开关",点【同步配置】即触发
source_open(url);地址无论是 RTSP/RTMP/M3U8/FLV,都走同一stream_source_t路径------"一个框、任意协议"正是归一设计的外在表现; - 输出状态区(3.3 节) :HDMI1 / UVC / NET1 三通道状态中,NET1 网络输出是否"已连接"由本归一层是否成功
source_open与持续取帧驱动; - FAQ Q2(拉流开关开却无画面) :先查地址/对端在线/554 端口,其次看本层是否进入了重连退避------
stimeout与重连日志是首要排查点; - 系统监控(CPU 低位):FFmpeg 只做轻量解封装与归一,重活(解码/显示)在 VPU/RGA/GPU,故 CPU 长期低位(说明书示例 7%)。
五、调试与验证
5.1 单协议快速验证(开发期)
bash
# RTSP
ffmpeg -rtsp_transport tcp -i rtsp://192.168.1.87/live/stream1 -f null -
# RTMP
ffmpeg -i rtmp://host/app/stream -f null -
# HLS(M3U8)
ffmpeg -i http://host/live/index.m3u8 -f null -
# HTTP-FLV / FLV
ffmpeg -i http://host/live.flv -f null -
5.2 应用层排查
bash
# FFmpeg 日志级别拉满,看 demux/重连细节
export FFREPORT=file=ffreport.log:level=48
# 或代码内 av_log_set_level(AV_LOG_DEBUG)
# 确认 MPP 解码是否被喂上(见《MPP 硬解》4.2)
ls -l /dev/mpp_service
cat /proc/interrupts | grep -i vpu
5.3 典型问题定位
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 拉流无画面 | 协议选项缺失(如 RTSP 未 tcp) | 加 rtsp_transport=tcp + stimeout,看 FAQ Q2 |
| 频繁重连 | 弱网/对端限流 | 调大 rtmp_buffer/退避上限,看重连日志 |
| 画面倍速/慢放 | PTS 未归一或回绕 | 检查 2.3 节时间戳归一与偏移修正 |
| 解码失败/绿屏 | ES 仍是 AVCC,未转 Annex-B | 确认 2.4 节 BSF 已生效 |
| 卡顿 | 抖动缓冲过浅 | 调大缓冲深度(2.6 节),权衡延迟 |
| 某协议拉不到 | URL scheme/端口不对 | 用 5.1 的 ffmpeg 命令逐协议验证 |
六、总结
MEDAI-W2 基于 RK3518 的"FFmpeg 多协议归一拉流"链路,可以归纳为 "一框接入、两层归一、三招自愈、对接一门":
- 一框接入:Web 端只给一个拉流地址框,RTMP/RTSP/M3U8/FLV 全部可填,协议细节对用户透明;
- 两层归一 :第一层传输/容器归一 (FFmpeg demuxer 自动识别 + 按需 AVOption),第二层码流/时钟归一(BSF 把 AVCC/HVCC 转 Annex-B,PTS 统一到微秒);
- 三招自愈:错误重连 + 停滞检测 + 指数退避,弱网下无声恢复;
- 对接一门:归一后的 AVPacket 通过统一桥接喂给 MPP VPU 硬解,与《UVC×MPP》《RGA+DRM》《OpenGL 抠图》系列无缝拼成完整链路。
把协议差异全部挡在 FFmpeg 这一道"前门"里,下游 Decode→Display 全程只认一份归一化 ES------这正是 MEDAI-W2 能在四核 A53 的 RK3518 上同时支撑多通道拉流、Web 服务与硬解显示的架构前提。该设计同样适用于 RK35 系列(RK3518/RK3528/RK3568/RK3576)上的网络视频网关、拉流解码与商业显示产品。
参考资料
- 《MEDAI-W2 拉流解码器使用说明书》(固件版本 2.1,文档版本 V1.0)
- 《MEDAI-W2 UVC 输入 × MPP 硬解码》技术解析(同系列文章)
- 《MEDAI-W2 V4L2/UVC 框架与自动格式协商》技术解析(同系列文章)
- 《MEDAI-W2 OpenGL 绿幕抠图 + DRM 送显》技术解析(同系列文章)
- 《MEDAI-W2 RGA + DRM HDMI 输出》技术解析(同系列文章)
- FFmpeg Documentation:
libavformat(protocols / demuxers)、libavcodec(BSF: h264_mp4toannexb / hevc_mp4toannexb)- 瑞芯微 RK3518 平台技术资料