MEDAI-W2_FFmpeg多协议归一拉流_CSDN文章

深度解析 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 与显示时钟必须一个统一时钟。归一策略:

  1. 基准转换 :用 av_rescale_q(pts, stream->time_base, AV_TIME_BASE_Q) 把所有 PTS 统一到微秒;
  2. 缺值兜底 :FLV/部分 RTSP 可能 AV_NOPTS_VALUE,用 DTS 或单调递增计数器兜底;
  3. 不连续修正:HLS/TS 的 33-bit PTS 回绕、换流时的跳变,需用偏移量修正,避免画面倍速/慢放(与《UVC×MPP》3.3 节"PTS 重基"同理);
  4. 单调性保证:送解码前做一次轻量排序/夹紧,防止乱序包导致 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)上的网络视频网关、拉流解码与商业显示产品。


参考资料

  1. 《MEDAI-W2 拉流解码器使用说明书》(固件版本 2.1,文档版本 V1.0)
  2. 《MEDAI-W2 UVC 输入 × MPP 硬解码》技术解析(同系列文章)
  3. 《MEDAI-W2 V4L2/UVC 框架与自动格式协商》技术解析(同系列文章)
  4. 《MEDAI-W2 OpenGL 绿幕抠图 + DRM 送显》技术解析(同系列文章)
  5. 《MEDAI-W2 RGA + DRM HDMI 输出》技术解析(同系列文章)
  6. FFmpeg Documentation: libavformat(protocols / demuxers)、libavcodec(BSF: h264_mp4toannexb / hevc_mp4toannexb)
  7. 瑞芯微 RK3518 平台技术资料
相关推荐
智能直播2 小时前
MEDAI-W2_Tinyalsa音频_HDMI输出_CSDN文章
音视频·实时音视频·视频编解码
Industio_触觉智能8 天前
瑞芯微RK平台视频硬解排障指南-下|板端实测记录
音视频·视频编码·视频编解码·零拷贝·gstreamer·视频解码·rk3576
智能直播12 天前
RK3576 + NDI 框架:HX2 编码输出与 HX2/FULL介绍
音视频·实时音视频·视频编解码
Industio_触觉智能14 天前
瑞芯微RK平台视频硬解排障指南-上|编解码、GStreamer与零拷贝全解析
视频编码·视频编解码·零拷贝·gstreamer·视频解码·rk3576
常州晟凯电子科技15 天前
8路CVBS/AHD模拟视频编码器评测
音视频·视频编解码
VidDown16 天前
上线三个月后的第一次事故:一个 Django 服务的日志、监控与故障复盘
数据库·python·django·wpf·音视频·视频编解码
VidDown16 天前
24fps 转 25fps 不只是改个数字:帧率转换、PAL/NTSC 与音频补偿
python·网络协议·音视频·视频编解码·视频
常州晟凯电子科技17 天前
8路CVBS/AHD合成一路视频无线传输系统评测
音视频·视频编解码
智能直播17 天前
网络RTMP拉流不卡顿、声音不同步,一文讲透缓冲区、时间戳
开发语言·ffmpeg·php·音视频·视频编解码