音视频学习(八十五):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 标志完整图像帧的结束
相关推荐
tedcloud12310 小时前
OmniRoute怎么部署?开源AI模型路由平台Linux部署教程
linux·服务器·人工智能·开源·音视频
潜创微科技14 小时前
8K HDMI 2.1 多源切换芯片方案,一颗芯片搞定长线不黑屏
音视频
音视频牛哥14 小时前
一款优秀的视频直播软件应具备哪些核心功能?结合 SmartMediaKit 谈实时音视频系统的能力边界
音视频·视频直播·低延迟rtsp播放器·gb28181接入·rtsp服务器·低延迟rtmp播放器·鸿蒙next rtsp
AI小码14 小时前
把动作「画」给视频世界模型,跨本体双向推演,李飞飞参与
大数据·人工智能·算法·ai·大模型·音视频·编程
嵌入式-老费17 小时前
esp32开发与应用(边采样边播放音频)
音视频
中微极客18 小时前
Veo视频生成与Gemini Agent平台集成实践
数据库·人工智能·oracle·音视频
码龙-DragonCoding19 小时前
一键批量提取音频、提取视频
android·音视频·提取
阿童木写作19 小时前
Temu商品图批量翻译工具免费,AI视频字幕翻译+智能抠图
人工智能·python·音视频
中微极客20 小时前
2026年AI视频生成模型技术选型与工程实践
人工智能·音视频
cellurw20 小时前
20260723 六组件全流程编译打通与音频项目启动
linux·服务器·音视频