- 概述
本文档描述如何在 H.264 视频流中动态插入自定义 SEI信息。SEI 是 H.264/AVC 标准中定义的一种元数据格式,可用于携带时间戳、版权信息、场景标记等附加数据。本方案基于 FFmpeg 的 AVPacket 结构和标准 H.264 NAL 单元格式实现,支持与视频帧的精确同步。 - SEI 核心概念
- SEI 的作用
1. 元数据承载:传递与视频帧相关的非压缩数据(如 GPS 信息、时间戳)。
2. 解码辅助:提供解码器增强信息(如颜色空间描述)。
3. 流分析:用于视频流完整性校验或内容标记。 - SEI 结构
字段 长度 描述
NAL Header 1 byte NAL 单元类型(0x06 表示 SEI)
Payload Type 可变长度 SEI 数据类型(如用户自定义=5)
Payload Size 可变长度 负载数据的字节长度
Payload Data N bytes 实际负载内容 - 实现原理
- 整体流程
A[输入H.264数据包] --> B[生成SEI NAL单元]
B --> C[合并到原始数据前]
C --> D[更新AVPacket]
D --> E[写入封装层] - 关键技术点
1. NAL 单元封装:遵循 H.264 Annex B 格式,添加 0x00000001 起始码。
2. 可变长度编码:对 payload_type 和 payload_size 进行多字节编码。
3. 内存管理:使用 FFmpeg 的 AVBufferRef 安全管理数据内存。 - 代码实现详解
- 代码实现
// 将字符串转换为二进制表示
std::vector<uint8_t> string_to_binary(const std::string& str) {
return std::vector<uint8_t>(str.begin(), str.end());
}
// 编码payloadType和payloadSize
void encode_sei_header(std::vector<uint8_t>& nal_unit, uint32_t payload_type, size_t payload_size) {
while (payload_type > 0x7F) {
nal_unit.push_back((payload_type & 0x7F) | 0x80);
payload_type >>= 7;
}
nal_unit.push_back(payload_type);
while (payload_size > 0x7F) {
nal_unit.push_back((payload_size & 0x7F) | 0x80);
payload_size >>= 7;
}
nal_unit.push_back(payload_size);
}
// H.264起始码
const uint8_t H264_START_CODE[] = {0x00, 0x00, 0x00, 0x01};
void append_custom_sei_to_packet(AVPacket *pkt, const std::string &sei_message)
{
// 创建SEI负载
std::vector<uint8_t> sei_payload = string_to_binary(sei_message);
// 构建SEI NAL单元
std::vector<uint8_t> sei_nal;
sei_nal.insert(sei_nal.end(), H264_START_CODE, H264_START_CODE + 4); // 添加起始码
sei_nal.push_back(0x06); // SEI NAL单元头
// 编码SEI消息头
encode_sei_header(sei_nal, 5, sei_payload.size()); // payloadType=5 表示用户数据未注册
// 合并NAL头和负载
sei_nal.insert(sei_nal.end(), sei_payload.begin(), sei_payload.end());
// 分配新内存并复制数据
size_t new_size = pkt->size + sei_nal.size();
uint8_t *new_data = (uint8_t *)av_malloc(new_size);
if (!new_data)
{
fprintf(stderr, "Memory allocation failed\n");
return;
}
memcpy(new_data, sei_nal.data(), sei_nal.size());
memcpy(new_data + sei_nal.size(), pkt->data, pkt->size);
// 更新pkt的数据和大小
av_packet_unref(pkt);
pkt->data = new_data;
pkt->size = new_size;
// 创建AVBufferRef
AVBufferRef *buffer_ref = av_buffer_create(new_data, new_size, av_buffer_default_free, NULL, 0);
if (!buffer_ref)
{
fprintf(stderr, "Failed to create AVBufferRef\n");
av_free(new_data);
return;
}
pkt->buf = buffer_ref;
}
void MP4Recorder::receiveH264Stream(const uint8_t *data, size_t size, const std::string &sei)
{
if (!m_bIsRecording || !ofmt_ctx) return;
AVPacket pkt;
init_packet(&pkt);
pkt.data = const_cast<uint8_t*>(data);
pkt.size = size;
// 动态生成并附加SEI到当前帧
append_custom_sei_to_packet(&pkt, sei);
write_video_packet(pkt);
av_packet_unref(&pkt);
}
2. 函数说明
1. string_to_binary:将字符串转换为二进制数据
2. encode_sei_header:编码 SEI 头部(Payload Type + Size),nal_unit: 输出的 NAL 单元容器,payload_type: SEI 类型(如 5=用户未注册数据),payload_size: 负载数据长度,每个字节最高位为延续标志(1 表示后续还有字节),其余7 位存储数据,按小端序排列
3. append_custom_sei_to_packet:将 SEI 添加到 AVPacket 开头,构建 SEI NAL 单元(起始码 + NAL头 + 负载),合并原始视频数据,更新 AVPacket 内存引用