音视频学习(八十五):FU-A

为什么需要 FU-A?

在实时音视频传输(WebRTC, RTSP, SIP)中,H.264 码流通常以 NALU (Network Abstraction Layer Unit) 为基本单位。然而,网络传输层存在 MTU(Maximum Transmission Unit,最大传输单元) 的限制。

  • MTU 限制:通常以太网的 MTU 为 1500 字节。
  • 协议开销:一个标准的 RTP 包包含:IP 头(20B)+ UDP 头(8B)+ RTP 固定头(12B)= 40 字节。
  • 有效载荷 :留给视频数据的空间约为 1500 − 40 = 1460 1500 - 40 = 1460 1500−40=1460 字节。

对于 H.264 的 I 帧(关键帧),其 NALU 大小往往达到几十甚至上百 KB,远超 MTU。如果直接让底层的 IP 层进行分片,一旦丢失其中一个 IP 分片,整个视频帧都无法组装,导致严重的解码花屏。

因此,RFC 6184 规定了在应用层(RTP 层)进行分片的机制,其中最常用的就是 FU-A

FU-A 报文结构

当一个 NALU 超过 MTU 时,我们会剥离 NALU 原有的 Header,将其 Payload 切分为多个分片,并在每个分片前添加两个字节的控制头:FU IndicatorFU Header

FU Indicator (1 字节)

其格式与 NALU Header 类似,起到指示"这是一个分片包"的作用:

  • F (1 bit): 禁止位,必须为 0。
  • NRI (2 bits): 重要性指示。取自原始 NALU Header,指示该片对解码的重要性(I 帧通常为 11)。
  • Type (5 bits) : 固定为 28 (二进制 11100),表示该包是 FU-A 类型。

FU Header (1 字节)

其作用是描述该分片在原始 NALU 中的位置:

  • S (Start bit) : 置 1 表示 NALU 的第一个分片。
  • E (End bit) : 置 1 表示 NALU 的最后一个分片。
  • R (Reserved): 保留位,必须为 0。
  • Type (5 bits) : 原始 NALU 的类型(如:若原始是 IDR 帧,则此处为 5)。

封包逻辑全流程

将一个大 NALU 转换为多个 FU-A 包的逻辑步骤如下:

1. 拆解原始 NALU

读取原始 NALU 的第一个字节(Header),提取其 NRIType。随后将指针向后移动 1 字节,指向真正的视频数据负载。

2. 循环切片

假设有效载荷大小为 PayloadSize,最大切片长度为 MAX_PAYLOAD_LEN(通常设为 1400):

  • 起始包 (Start Unit) :
    • S=1, E=0
    • Payload 包含 NALU 数据的前 1400 字节。
    • RTP Header 的 Marker 位通常为 0。
  • 中间包 (Intermediate Units) :
    • S=0, E=0
    • Payload 包含接下来的数据。
  • 结束包 (End Unit) :
    • S=0, E=1
    • Payload 包含剩余数据。
    • 关键点 :此时 RTP Header 的 Marker 位应置为 1,表示这一帧(Access Unit)结束。

3. RTP 序列号与时间戳

  • 序列号 (Sequence Number) :每个 FU-A 分片包的序列号必须连续递增(如 101, 102, 103)。
  • 时间戳 (Timestamp) :属于同一个 NALU 的所有分片包,必须使用完全相同的时间戳

解包与组装逻辑 (Receiver Side)

接收端在收到 RTP 包后,根据 Type=28 判定为 FU-A:

  1. 识别 S 位:创建一个缓冲区,跳过 FU 指示器和头部,存入第一个分片数据。
  2. 连续性校验:检查序列号是否连续。如果中间跳号(如收到了 101 和 103),则该 NALU 损坏,丢弃当前正在组装的所有分片。
  3. 识别 E 位:收到 E 位后,根据原始 Type 和 NRI 还原 NALU Header,将其与所有分片数据拼接。
  4. 递交解码器:将组装好的完整 NALU 传给 H.264 解码器(如 FFmpeg)。

常见问题与优化经验

1. Marker 位 (M) 的处理

很多开发者容易混淆 FU HeaderE位RTP HeaderM位

  • E=1 仅仅代表这一个 NALU 结束了。
  • M=1 代表这一 帧 (Frame) 结束了。
  • 注意 :在 H.264 中,一个帧可能包含多个 NALU(如 SPS, PPS, SEI, IDR)。通常做法是仅在最后一个 NALU 的最后一个 FU-A 分片上将 M 置为 1。

2. 内存拷贝优化

在高性能流媒体服务器中,频繁的 memcpy 会消耗 CPU。

  • 零拷贝建议:预留 14 字节的 Header 空间(12B RTP + 2B FU),直接在原始数据缓冲区上进行封包发送。

3. 花屏现象排查

如果出现"小范围正常,大范围花屏",通常是因为:

  • 分片丢失:UDP 丢包导致中间分片缺失,由于没有检查序列号连续性,接收端拼出了错误的 NALU。
  • MTU 设置过大:设置了 1460 导致加上 IP 头后超过了链路层限制,触发了 IP 层分片。

代码实现(c++)

封包流程 (Packer)

c++ 复制代码
#include <vector>
#include <cstring>
#include <cstdint>

// 模拟发送函数指针
typedef void (*RtpSendCallback)(const uint8_t* data, int len, uint32_t ts, bool marker);

/**
 * @brief H.264 NALU 封包为 FU-A
 * @param nalu       原始NALU数据(包含Header)
 * @param nalu_len   数据长度
 * @param mtu        最大传输单元 (通常1400左右)
 * @param timestamp  RTP时间戳
 * @param send_cb    发送回调
 */
void rtp_h264_pack_fu_a(const uint8_t* nalu, int nalu_len, int mtu, uint32_t timestamp, RtpSendCallback send_cb) {
    if (nalu_len <= mtu) {
        // 单包发送逻辑 (Single NALU Unit)
        send_cb(nalu, nalu_len, timestamp, true);
        return;
    }

    uint8_t nalu_header = nalu[0];
    uint8_t fu_indicator = (nalu_header & 0xE0) | 28; // Type 28 for FU-A
    uint8_t fu_header_base = (nalu_header & 0x1F);   // 原始 Type

    const uint8_t* payload = nalu + 1; // 跳过原始Header
    int payload_len = nalu_len - 1;
    int max_chunk = mtu - 2; // 扣除 FU indicator 和 FU header

    int offset = 0;
    while (offset < payload_len) {
        int remaining = payload_len - offset;
        int current_chunk = (remaining > max_chunk) ? max_chunk : remaining;
        
        uint8_t fu_header = fu_header_base;
        bool is_last = (offset + current_chunk == payload_len);

        if (offset == 0) 
            fu_header |= 0x80; // S = 1 (Start)
        else if (is_last)
            fu_header |= 0x40; // E = 1 (End)

        // 构造临时 RTP 载荷 (实际项目中建议使用预分配的 buffer 避免拷贝)
        std::vector<uint8_t> rtp_payload;
        rtp_payload.push_back(fu_indicator);
        rtp_payload.push_back(fu_header);
        rtp_payload.insert(rtp_payload.end(), payload + offset, payload + offset + current_chunk);

        // 发送分片,只有最后一个分片 Marker 置 1
        send_cb(rtp_payload.data(), (int)rtp_payload.size(), timestamp, is_last);

        offset += current_chunk;
    }
}

解包流程 (Unpacker)

c++ 复制代码
#include <iostream>

// 用于保存组装状态的全局/类变量
std::vector<uint8_t> g_nalu_buffer;

/**
 * @brief 处理收到的 RTP 载荷并解包
 * @param rtp_payload  RTP报文中的负载部分(不含RTP Header)
 * @param payload_len  负载长度
 */
void rtp_h264_unpack_fu_a(const uint8_t* rtp_payload, int payload_len) {
    if (payload_len < 2) return;

    uint8_t fu_indicator = rtp_payload[0];
    uint8_t fu_header = rtp_payload[1];
    uint8_t type = fu_indicator & 0x1F;

    if (type == 28) { // FU-A 分片
        bool start_bit = (fu_header & 0x80) != 0;
        bool end_bit = (fu_header & 0x40) != 0;
        uint8_t original_nalu_type = fu_header & 0x1F;

        if (start_bit) {
            // 第一个分片:还原 NALU Header 并开始组装
            g_nalu_buffer.clear();
            uint8_t reconstructed_header = (fu_indicator & 0xE0) | original_nalu_type;
            g_nalu_buffer.push_back(reconstructed_header);
        }

        // 拷贝负载数据(跳过 FU indicator 和 FU header)
        g_nalu_buffer.insert(g_nalu_buffer.end(), rtp_payload + 2, rtp_payload + payload_len);

        if (end_bit) {
            // 最后一个分片:组装完成,交付给解码器
            std::cout << "完整 NALU 组装完成,大小: " << g_nalu_buffer.size() << std::endl;
            // Decode(g_nalu_buffer.data(), g_nalu_buffer.size());
            g_nalu_buffer.clear();
        }
    } else {
        // 单包 NALU(非分片)
        std::cout << "收到单包 NALU,类型: " << (int)type << std::endl;
        // Decode(rtp_payload, payload_len);
    }
}

总结

FU-A 是 H.264 网络传输的基石。它巧妙地在 RTP 负载层解决了大数据块传输的问题,兼顾了协议的鲁棒性与解析的简便性。

核心组件 关键作用
FU Indicator 定义协议类型(Type 28)
FU Header 标记分片的首尾位置及原始 NALU 类型
Timestamp 保持同帧同步,不可在分片间递增
Marker Bit 标志完整图像帧的结束
相关推荐
ting_zh16 小时前
音频录制与播放-STM32F779I-EVAL
stm32·嵌入式硬件·音视频
罗兰Yolanda18 小时前
影视后期全流程的核心软件及工作站配置方案推荐
计算机视觉·音视频
大大祥1 天前
Android FFmpeg集成
android·ffmpeg·kotlin·音视频·jni·ndk·音视频编解码
开开心心_Every1 天前
视频无损压缩工具:大幅减小体积并保持画质
游戏·微信·pdf·excel·音视频·语音识别·tornado
EasyCVR1 天前
解析视频融合平台EasyCVR视频智能分析技术背后的技术支撑
人工智能·音视频
一只小bit1 天前
Qt 多媒体:快速解决音视频播放问题
前端·c++·qt·音视频·cpp·页面
老兵发新帖1 天前
实时视频流检测问题分析:11秒视频保存的标注视频只有3秒
音视频
纽格立科技1 天前
数字广播内容服务器NGA-101 DRM媒体编码器
网络·音视频·信息与通信·传媒·媒体
EasyCVR1 天前
视频汇聚平台EasyCVR智慧水利工程全域可视化视频监控技术应用实践
音视频