FFmpeg 解码 H.264 流提取 SEI 用户数据实战

一、开发环境准备

本文基于以下环境开发测试,可根据实际场景调整:

  • 操作系统:Ubuntu 22.04 LTS
  • FFmpeg 版本:4.4.3(支持 H.264 解析器和比特流过滤器)
  • 编译工具:GCC 11.3.0
  • 依赖库:libavformat-dev、libavcodec-dev、libavutil-dev、libcjson-dev
  • 硬件平台:X86 PC 可直接编译测试,ARM 嵌入式平台交叉编译流程一致

二、SEI 核心原理

2.1 SEI 标准定义与结构

根据 H.264/AVC 标准,SEI(补充增强信息)属于非核心解码单元,不影响视频帧的正常解码,专门用于传输业务自定义辅助数据,常见场景包括时间戳同步、位置信息嵌入、告警标记等。

H.264 码流中 SEI 对应 NALU 类型为 6,未注册用户自定义 SEI 的结构如下:

bash 复制代码
SEI NALU(类型6)
├─ SEI payload类型(未注册用户数据固定为5)
├─ SEI payload长度
└─ SEI payload内容
   ├─ 16字节UUID(用于区分业务类型)
   └─ 用户自定义负载

2.2 FFmpeg 默认处理逻辑

FFmpeg 的 SEI 解码核心逻辑位于libavcodec/h264_sei.c源码中,默认处理流程为:

  1. 入口函数ff_h264_decode_sei拆分 SEI payload
  2. 根据 payload 类型调用对应处理函数,未注册用户数据对应decode_unregistered_user_data
  3. 默认实现仅读取 UUID 后直接跳过自定义负载,不会将数据暴露给应用层,这就是默认解码流程无法获取自定义 SEI 的核心原因。

2.3 关键规则:防竞争字节处理

H.264 使用0x00 0x00 0x01作为 NALU 起始码,为避免 SEI 负载中出现相同序列导致解析错误,编码时会在0x00 0x00后插入0x03字节:

  • 原始0x00 0x00 0x01 → 编码后0x00 0x00 0x03 0x01
  • 原始0x00 0x00 0x02 → 编码后0x00 0x00 0x03 0x02
    解析时需要反向移除插入的0x03字节,还原原始负载数据。

2.4 容器对 SEI 解析的影响

MP4 等封装容器会移除原始 NALU 起始码,重排 NALU 顺序,导致直接从容器提取 SEI 容易失败。此类场景需要通过h264_mp4toannexb比特流过滤器,还原为 Annexb 格式的原始码流后再解析。

三、前置准备:生成测试 SEI 码流

使用 FFmpeg 内置的h264_metadata比特流过滤器,可以快速注入自定义 SEI 用于测试:

bash 复制代码
ffmpeg -i input.mp4 -c:v copy \
-bsf:v h264_metadata=sei_user_data='086f3693-b7b3-4f2c-9653-21492feee5b8+{"timestamp":1717200000,"lon":116.397,"lat":39.908}' \
test.h264

该命令将携带时间戳、经纬度的 JSON 数据注入 SEI,生成可用于测试的 H.264 裸流文件。

四、核心代码实现

4.1 防竞争字节处理函数

以下是完整的防竞争字节移除实现,包含边界检查和异常处理:

objectivec 复制代码
/**
 * @brief 移除SEI负载中的防竞争字节
 * @param src 原始负载数据
 * @param src_len 原始数据长度
 * @param dst 输出缓冲区
 * @param dst_len 输出缓冲区最大长度
 * @return 处理后的数据长度
 */
int remove_emulation_bytes(uint8_t *src, int src_len, uint8_t *dst, int dst_len) {
    int i = 0, j = 0;
    if (!src || !dst || src_len <= 0 || dst_len <= 0) {
        return -1;
    }

    while (i < src_len && j < dst_len) {
        // 检测到 0x00 0x00 0x03 序列,跳过第三个字节
        if (i + 2 < src_len && src[i] == 0x00 && src[i+1] == 0x00 && src[i+2] == 0x03) {
            dst[j++] = src[i++];
            dst[j++] = src[i++];
            i++; // 跳过插入的防竞争字节0x03
        } else {
            dst[j++] = src[i++];
        }
    }
    return j;
}

4.2 SEI 解析主流程

以下是完整的 SEI 筛选与解析核心代码:

objectivec 复制代码
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <cjson/cJSON.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

// 目标业务UUID,与注入时保持一致
static const uint8_t TARGET_UUID[16] = {
    0x08, 0x6f, 0x36, 0x93, 0xb7, 0xb3, 0x4f, 0x2c,
    0x96, 0x53, 0x21, 0x49, 0x2f, 0xee, 0xe5, 0xb8
};

// 防竞争处理函数声明见上文
int remove_emulation_bytes(uint8_t *src, int src_len, uint8_t *dst, int dst_len);

/**
 * @brief 处理SEI NALU,提取目标自定义数据
 */
void process_sei(uint8_t *data, int size) {
    // 跳过起始码(3字节)+ NALU头(1字节)
    if (size < 4) return;
    uint8_t *sei_ptr = data + 4;
    int sei_remaining = size - 4;

    // 解析SEI payload类型
    int payload_type = 0;
    while (sei_remaining > 0 && *sei_ptr == 0xFF) {
        payload_type += 0xFF;
        sei_ptr++;
        sei_remaining--;
    }
    if (sei_remaining <= 0) return;
    payload_type += *sei_ptr++;
    sei_remaining--;

    // 解析SEI payload长度
    int payload_size = 0;
    while (sei_remaining > 0 && *sei_ptr == 0xFF) {
        payload_size += 0xFF;
        sei_ptr++;
        sei_remaining--;
    }
    if (sei_remaining <= 0 || payload_size > sei_remaining) return;
    payload_size += *sei_ptr++;
    sei_remaining--;

    // 仅处理未注册用户数据,且必须包含16字节UUID
    if (payload_type != 5 || payload_size < 16) return;

    // 匹配目标UUID,不匹配则跳过
    if (memcmp(sei_ptr, TARGET_UUID, 16) != 0) return;

    // 提取自定义负载
    uint8_t *payload = sei_ptr + 16;
    int payload_len = payload_size - 16;
    if (payload_len <= 0) return;

    // 申请输出缓冲区,处理防竞争字节
    uint8_t *dest = av_malloc(payload_len);
    if (!dest) return;
    int dest_len = remove_emulation_bytes(payload, payload_len, dest, payload_len);
    if (dest_len <= 0) {
        av_free(dest);
        return;
    }

    // 解析JSON格式的位置数据
    cJSON *root = cJSON_ParseWithLength((char*)dest, dest_len);
    if (!root) {
        av_free(dest);
        return;
    }

    // 提取字段并输出
    cJSON *timestamp = cJSON_GetObjectItemCaseSensitive(root, "timestamp");
    cJSON *lon = cJSON_GetObjectItemCaseSensitive(root, "lon");
    cJSON *lat = cJSON_GetObjectItemCaseSensitive(root, "lat");
    if (cJSON_IsNumber(timestamp) && cJSON_IsNumber(lon) && cJSON_IsNumber(lat)) {
        printf("✅ SEI解析成功: 时间戳=%.0f, 经度=%.3f, 纬度=%.3f\n",
               timestamp->valuedouble, lon->valuedouble, lat->valuedouble);
    }

    // 资源释放
    cJSON_Delete(root);
    av_free(dest);
}

五、实战技术流程:无人机 GPS SEI 提取

5.1 场景问题描述

某嵌入式无人机巡检项目,飞控端将 GPS 位置信息注入 H.264 裸流的 SEI 单元,要求地面站解码端实时提取位置信息并与视频帧同步输出,且 SEI 解析错误不能影响正常视频解码流程。

5.2 完整实现步骤

步骤 1:环境配置与基础解码验证

首先安装 FFmpeg 开发依赖,编写最小化解码程序,验证基础解码流程正常:

bash 复制代码
sudo apt install libavformat-dev libavcodec-dev libavutil-dev libcjson-dev

基础解码初始化代码:

objectivec 复制代码
int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("用法: %s <h264文件路径>\n", argv[0]);
        return -1;
    }

    // 注册FFmpeg所有组件
    av_register_all();
    avcodec_register_all();

    // 初始化解码器
    const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
    AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
    if (!codec_ctx) return -1;
    if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
        avcodec_free_context(&codec_ctx);
        return -1;
    }

    // 初始化H.264解析器
    AVCodecParserContext *parser = av_parser_init(AV_CODEC_ID_H264);
    AVPacket *pkt = av_packet_alloc();
    FILE *fp = fopen(argv[1], "rb");
    if (!fp || !parser || !pkt) {
        // 资源释放略
        return -1;
    }
步骤 2:循环读取码流拆分 NALU

循环读取文件数据,通过 FFmpeg 解析器拆分出独立 NALU:

objectivec 复制代码
    uint8_t buf[4096];
    int buf_size;
    while ((buf_size = fread(buf, 1, sizeof(buf), fp)) > 0) {
        uint8_t *data_ptr = buf;
        int remaining = buf_size;

        while (remaining > 0) {
            int ret = av_parser_parse2(parser, codec_ctx, &pkt->data, &pkt->size,
                                      data_ptr, remaining, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
            if (ret < 0) {
                // 解析错误,跳过当前数据块,不中断解码
                break;
            }

            data_ptr += ret;
            remaining -= ret;

            // 筛选SEI类型NALU(类型6)
            if (pkt->size > 0 && (parser->unit_type & 0x1F) == 6) {
                process_sei(pkt->data, pkt->size);
            }

            // 送入解码器正常解码视频帧
            if (pkt->size > 0) {
                avcodec_send_packet(codec_ctx, pkt);
                AVFrame *frame = av_frame_alloc();
                if (avcodec_receive_frame(codec_ctx, frame) == 0) {
                    // 此处可添加视频帧处理逻辑
                    av_frame_free(&frame);
                }
            }
        }
    }
步骤 3:资源释放与退出
objectivec 复制代码
{  
    fclose(fp);
    av_packet_free(&pkt);
    av_parser_close(parser);
    avcodec_free_context(&codec_ctx);
    return 0;
}

5.3 结果验证

编译运行程序后,输出结果如下:

复制代码
✅ SEI解析成功: 时间戳=1717200000, 经度=116.397, 纬度=39.908

测试验证结论:

  1. 注入的 SEI 数据 100% 解析成功,与原始数据完全一致
  2. 单帧 SEI 解析耗时小于 1ms,满足嵌入式实时性要求
  3. 注入无效 SEI 数据时,解析错误被正常捕获,不会中断视频解码流程
  4. 包含防竞争序列的负载可以正确解析,还原后数据无错误

六、实战总结

6.1 两种提取方案对比

方案 优点 缺点 适用场景
手动解析 NALU 兼容性好,不依赖 FFmpeg 内部实现,可处理任意格式码流 开发量略大 裸流输入场景、嵌入式资源受限场景
Hook 解码回调 集成度高,无需自行处理 NAL 拆分 依赖 FFmpeg 版本,兼容性差 已接入完整 FFmpeg 解码、版本固定的项目

6.2 常见问题总结

  1. SEI 数据提取不全:多为多 SEI 合并场景未循环处理所有 payload,或比特流过滤器配置错误
  2. SEI 解析失败 :多为未做防竞争字节处理,或 MP4 容器未通过h264_mp4toannexb还原 NAL 边界
  3. 解码被 SEI 错误打断:未做错误隔离,需确保 SEI 解析异常不影响核心解码流程

6.3 开发经验

自定义 SEI 优先使用标准未注册 UUID 格式,避免与标准 SEI 冲突;负载长度建议控制在 1KB 以内,避免被编码器截断;必须添加数据校验,避免无效数据影响主流程;使用 NVIDIA 硬件解码时,FFmpeg 5.0 以上版本已支持 SEI 提取,可直接通过回调获取。

6.4 延伸说明

H.265 SEI 与 H.264 SEI 核心结构类似,仅 NALU 类型不同(H.265 为 39),解析逻辑可直接复用,只需修改 NALU 类型判断和解析器初始化参数即可。

相关推荐
VimorphTech2 小时前
ffmpeg.wasm 实战:浏览器提取视频音频,如何处理进度、取消和资源释放?
ffmpeg
小小龙学IT3 小时前
FFmpeg 开源多媒体处理框架深度解析:从命令行到 libav* 全家桶
c语言·ffmpeg·开源
ofoxcoding1 天前
Gemini TTS 与 ElevenLabs 语音合成实测:同一段视频旁白效果对比
ai·ffmpeg·音视频
_大峰_2 天前
索尼 A7M4 误删视频恢复实战:基于原始卡镜像重建 MP4 数据
ffmpeg·数据恢复·音视频·mp4·exfat
Anhty2 天前
3 款手机音频剪辑 APP 横评,短视频配音后期,免费工具怎么选?
功能测试·ffmpeg·音视频
VidDown3 天前
自动给视频挑一张能看的封面:帧评分、构图与批量生成
python·ffmpeg·音视频·视频
可乐鸡翅yeah_4 天前
HLS 业务 Referer‑Policy 网页元标签引发播放异常排错
前端·javascript·网络·ffmpeg·php·音视频
开开心心就好5 天前
本地文件搜索工具推荐,占用小速度还快
智能手机·ffmpeg·ocr·word·音视频·网络攻击模型·安全架构
我科绝伦(Huanhuan Zhou)5 天前
oracle坏块导致备份不了
数据库·oracle·ffmpeg