什么是SEI?
在 H.264 视频编码标准中,补充增强信息(Supplemental Enhancement Information,SEI) 是一种特殊的 NAL(网络抽象层)单元。它不像序列参数集(SPS)或图像参数集(PPS)那样直接影响解码过程,而是提供辅助性、非强制性的信息。
SEI 可以视为视频数据中的“便签”或“元数据”,这些信息可以被解码器或应用程序用来增强视频体验,但即使它们丢失了,解码器仍然可以正常解码和播放视频。SEI 单元的引入,让 H.264 码流在传输视频数据本身的同时,还能灵活地携带各种附加信息。
SEI的特点
SEI 的主要作用是为视频流提供额外的信息:
- 计时和同步: SEI 可以包含精确的计时信息,帮助解码器同步音频和视频,确保播放流畅。例如,
pic_timing
SEI 消息可以提供每一帧的显示时间。 - 错误隐藏: 当视频流在网络传输中发生丢包时,SEI 消息可以提供错误隐藏所需的信息,帮助解码器更好地处理损坏的图像。例如,
recovery_point
SEI 消息可以指示一个可以安全恢复解码的同步点。 - 显示与渲染: SEI 消息可以提供关于如何正确显示视频的信息。例如,它可以包含色彩空间、亮度范围等元数据,确保视频在不同设备上都能有正确的显示效果。
- 用户自定义数据: SEI 单元可以携带用户自定义的私有数据,这为开发者在视频流中嵌入自己的信息提供了便利。例如,它可以用来携带版权信息、相机设置或者其他任何应用程序所需的数据。
SEI结构体
// H.264 SEI 消息结构
typedef struct H264SEI {
int payloadType; // SEI 类型 (0,1,5等)
int payloadSize; // SEI 数据大小(字节数)
union {
// (payloadType == 0) HRD 缓冲控制
struct {
int cpb_removal_delay; // CPB 移除延时
int dpb_output_delay; // DPB 输出延时
} buffering_period;
// (payloadType == 1) 图像时间戳
struct {
int cpb_removal_delay; // CPB 移除延时
int dpb_output_delay; // DPB 输出延时
int pic_struct; // 图像结构 (0=帧,1=顶场,2=底场,3=顶+底场, etc.)
int clock_timestamp_flag;// 是否有时钟时间戳
int ct_type; // 时钟类型
int nuit_field_based_flag;// 时间戳是否基于场
int counting_type; // 计数类型
int full_timestamp_flag; // 是否输出完整时间戳
int seconds, minutes, hours; // 时间信息
} pic_timing;
// (payloadType == 5) 用户自定义数据
struct {
uint8_t uuid[16]; // 唯一标识符 (UUID)
std::vector<uint8_t> user_data; // 自定义数据
} user_data_unregistered;
// (payloadType == 6) 错误恢复点
struct {
int recovery_frame_cnt; // 距离下一个可解码帧的间隔
} recovery_point;
// 其他类型可继续扩展...
};
} H264SEI;
payloadType
payloadType | 名称 | 说明 |
---|---|---|
0 | buffering_period | HRD 缓冲控制参数 |
1 | pic_timing | 图像时间戳信息(CPB/DPB/显示时间戳) |
2 | pan_scan_rect | 平移扫描窗口 |
3 | filler_payload | 填充比特流(码率控制) |
4 | user_data_registered_itu_t_t35 | 用户数据(ITU-T T.35 标准注册) |
5 | user_data_unregistered | 用户自定义数据(常用于水印、UUID 信息) |
6 | recovery_point | 错误恢复点 |
9 | scene_info | 场景切换信息 |
45 | frame_packing_arrangement | 3D 视频左右眼帧排列 |
参数说明
payloadType / payloadSize
payloadType:SEI 类型,决定后续解析方式。
payloadSize:SEI 消息的字节长度。
Buffering period(payloadType = 0)
cpb_removal_delay:表示解码时间戳(DTS)相对于 CPB 的移除延时。
dpb_output_delay:表示显示时间戳(PTS)相对于解码的延时。
Picture timing(payloadType = 1)
pic_struct:指示图像显示方式
- 0 = 帧
- 1 = 顶场
- 2 = 底场
- 3 = 顶场+底场(逐行交错)
clock_timestamp_flag:是否包含时钟时间戳。
seconds/minutes/hours:可选的显示时间信息。
User data unregistered(payloadType = 5)
uuid:16 字节的唯一标识符,区分不同厂商/应用。
user_data:实际携带的数据(比如字幕、元数据、OSD 等)。
Recovery point(payloadType = 6)
- recovery_frame_cnt:表示从当前帧起,多少帧之后可恢复到无误码解码。
示例(c++)
#include <cstdint>
#include <vector>
#include <string>
#include <cstring>
#include <iostream>
// ========================
// H.264 SEI 结构体定义
// ========================
struct H264SEI {
int payloadType; // SEI 类型
int payloadSize; // 数据大小
// 不同类型的 SEI 数据
struct BufferingPeriod {
int cpb_removal_delay;
int dpb_output_delay;
};
struct PicTiming {
int cpb_removal_delay;
int dpb_output_delay;
int pic_struct;
bool clock_timestamp_flag;
int ct_type;
int nuit_field_based_flag;
int counting_type;
bool full_timestamp_flag;
int seconds, minutes, hours;
};
struct UserDataUnregistered {
uint8_t uuid[16];
std::vector<uint8_t> user_data;
};
struct RecoveryPoint {
int recovery_frame_cnt;
};
// union 方式存储不同 payload
BufferingPeriod buffering_period;
PicTiming pic_timing;
UserDataUnregistered user_data_unregistered;
RecoveryPoint recovery_point;
};
// ========================
// 工具类:BitReader
// ========================
class BitReader {
public:
BitReader(const uint8_t* data, int size) : data_(data), size_(size), bit_pos_(0) {}
uint32_t read_bits(int n) {
uint32_t val = 0;
for (int i = 0; i < n; i++) {
val <<= 1;
val |= read_bit();
}
return val;
}
uint32_t read_bit() {
if (bit_pos_ >= size_ * 8) return 0;
uint32_t val = (data_[bit_pos_ / 8] >> (7 - (bit_pos_ % 8))) & 0x01;
bit_pos_++;
return val;
}
uint32_t read_ue() { // 无符号 Exp-Golomb
int zeros = 0;
while (read_bit() == 0 && bit_pos_ < size_ * 8) zeros++;
uint32_t value = (1 << zeros) - 1 + read_bits(zeros);
return value;
}
int32_t read_se() { // 有符号 Exp-Golomb
uint32_t ueVal = read_ue();
int32_t val = (ueVal & 1) ? (int32_t)((ueVal + 1) >> 1) : -(int32_t)(ueVal >> 1);
return val;
}
private:
const uint8_t* data_;
int size_;
int bit_pos_;
};
// ========================
// H.264 SEI 解析器
// ========================
class H264SEIParser {
public:
static std::vector<H264SEI> parse(const uint8_t* data, int size) {
std::vector<H264SEI> seis;
int offset = 0;
while (offset < size) {
// payloadType
int payloadType = 0;
while (offset < size && data[offset] == 0xFF) {
payloadType += 255;
offset++;
}
if (offset < size) payloadType += data[offset++];
// payloadSize
int payloadSize = 0;
while (offset < size && data[offset] == 0xFF) {
payloadSize += 255;
offset++;
}
if (offset < size) payloadSize += data[offset++];
if (offset + payloadSize > size) break;
H264SEI sei;
sei.payloadType = payloadType;
sei.payloadSize = payloadSize;
const uint8_t* payload = data + offset;
switch (payloadType) {
case 0: // buffering_period
parse_buffering_period(payload, payloadSize, sei);
break;
case 1: // pic_timing
parse_pic_timing(payload, payloadSize, sei);
break;
case 5: // user_data_unregistered
parse_user_data_unregistered(payload, payloadSize, sei);
break;
case 6: // recovery_point
parse_recovery_point(payload, payloadSize, sei);
break;
default:
// 未实现的 SEI 类型
break;
}
seis.push_back(sei);
offset += payloadSize;
}
return seis;
}
private:
static void parse_buffering_period(const uint8_t* payload, int size, H264SEI& sei) {
BitReader br(payload, size);
sei.buffering_period.cpb_removal_delay = br.read_ue();
sei.buffering_period.dpb_output_delay = br.read_ue();
}
static void parse_pic_timing(const uint8_t* payload, int size, H264SEI& sei) {
BitReader br(payload, size);
sei.pic_timing.cpb_removal_delay = br.read_ue();
sei.pic_timing.dpb_output_delay = br.read_ue();
sei.pic_timing.pic_struct = br.read_bits(4);
sei.pic_timing.clock_timestamp_flag = br.read_bit();
if (sei.pic_timing.clock_timestamp_flag) {
sei.pic_timing.ct_type = br.read_bits(2);
sei.pic_timing.nuit_field_based_flag = br.read_bit();
sei.pic_timing.counting_type = br.read_bits(5);
sei.pic_timing.full_timestamp_flag = br.read_bit();
sei.pic_timing.seconds = br.read_bits(6);
sei.pic_timing.minutes = br.read_bits(6);
sei.pic_timing.hours = br.read_bits(5);
}
}
static void parse_user_data_unregistered(const uint8_t* payload, int size, H264SEI& sei) {
if (size < 16) return;
memcpy(sei.user_data_unregistered.uuid, payload, 16);
sei.user_data_unregistered.user_data.assign(payload + 16, payload + size);
}
static void parse_recovery_point(const uint8_t* payload, int size, H264SEI& sei) {
BitReader br(payload, size);
sei.recovery_point.recovery_frame_cnt = br.read_ue();
}
};
// ========================
// 示例 main()
// ========================
int main() {
// 模拟一个 SEI NALU(例子:user_data_unregistered)
uint8_t sei_nalu[] = {
0x05, 0x20, // payloadType=5, payloadSize=32
// UUID (16 bytes)
0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,
0x99,0xAA,0xBB,0xCC,0xDD,0xEE,0xFF,0x00,
// User Data (16 bytes)
'T','E','S','T','_','S','E','I','_','D','A','T','A','!','!','!'
};
auto seis = H264SEIParser::parse(sei_nalu, sizeof(sei_nalu));
for (auto& sei : seis) {
std::cout << "SEI Type: " << sei.payloadType
<< " Size: " << sei.payloadSize << std::endl;
if (sei.payloadType == 5) {
std::cout << "UUID: ";
for (int i = 0; i < 16; i++) printf("%02X", sei.user_data_unregistered.uuid[i]);
std::cout << std::endl;
std::string str(sei.user_data_unregistered.user_data.begin(),
sei.user_data_unregistered.user_data.end());
std::cout << "User Data: " << str << std::endl;
}
}
return 0;
}