H264码流插入和读取自定义数据(SEI字段)

news/2024/11/16 15:28:26/

目录

  • 1. 前言
  • 2. H264码流结构
    • 2.1 文字说明
    • 2.2 码流格式图解
  • 3. 自定义数据封装以及H264码流插入
    • 3.1 将自定义数据封装成SEI字段通用格式即可插入H264码流中
    • 3.2 编码逻辑
  • 4. 代码
  • 5. 总结

1. 前言

本文主要记录,如何在已有的H264码流中插入自定义的数据,并且不破坏H264码流结构,改造后的H264码流可以通过解码器正常解析出来,插入的自定义数据对H264码流解析不造成任何影响。
代码在文末附上,基于C/C++实现。

2. H264码流结构

2.1 文字说明

(1)H264码流,是由一个个独立的数据块NALU组成,NALU数据块之间相互关联,NALU数据块的顺序不可以调换,但NALU数据块之间可以插入指定格式的数据,理论上插入数据的长度不限。
(2)解码器在解码时,首先遍历码流中的Start code(00 00 00 01),找到Start code后紧跟着解析NALU单元,解析时从前往后遍历,直至找到下一个Start code,并开始下一帧NALU单元的解析。
(3)H264码流中插入自定义数据,首先将自定义数据按特定的格式封装成一个数组,然后在码流的每个I帧或者P帧之前插入自定义数据即可

2.2 码流格式图解

在这里插入图片描述
图2.1 H264码流基础数据块
在这里插入图片描述
图2.2 H264码流数据块组合示例

3. 自定义数据封装以及H264码流插入

3.1 将自定义数据封装成SEI字段通用格式即可插入H264码流中

在这里插入图片描述
图3.1 自定义数据封装格式

SEI字段自定义封装包格式为:

Start code:每个NALU单元的起始码
NRI:高四位的第一位为禁止位0,决定该NALU单元是否有效;高四位中间两位表示该NALU单元的重要程度,解码时根据该数值决定该NALU单元解析或丢弃。高四位的最后一位与低四位组成NALU单元识别码,06表示该单元为SEI字段。
payload type:05表示该SEI字段的编码格式符合H264标准格式
UUID:用户自定义的识别码,16字节长度,只要不与Start code冲突即可
自定义数据长度:用两个字节表示封装自定义数据的长度,解码时依据该数值做校验
自定义数据:用户自己封装的数据,类型必须为unsigned char
结尾对齐码:80表示SEI字段的结束对齐

3.2 编码逻辑

对H264码流进行for循环遍历,依据Start code 以及NALU头识别NALU单元类型,当识别到I帧或P帧时,将封装好的SEI数据包直接插入

4. 代码

h264Process.h

#include <stdio.h>
#include <string.h>
// uuid: 54 80 83 97 f0 23 47 4b b7 f7 4f 32 b5 4e 06 ac
// 定义SEI格式固定识别码
namespace SEI{static unsigned char start_code[] = {0x00,0x00,0x00,0x01};  //起始码static unsigned char sei_uuid[] = { 0x54, 0x8f, 0x83, 0x97, 0xf3, 0x23, 0x97, 0x4b, 0xb7, 0xc7, 0x4f, 0x3a, 0xb5, 0x6e, 0x89, 0x52 };   //自定义标识码static int sei_uuid_size = sizeof(sei_uuid);    //自定义标识码长度static unsigned char nal_type = 0x06;   //NAL类型,0x06标识该NAL单元为SEIstatic unsigned char payload_type = 0x05;   //0x05标识当前SEI编码格式为标准格式static unsigned char sei_tail = 0x80;   //SEI结尾对齐码
}
// 定位当前位置是否为SEI起始位置,返回值为SEI字段长度
int FindSei(unsigned char* h264_buf, int cur_index,int buf_size);// 定位当前位置是否为I帧起始位置,若当前字节为I帧开始,返回1,否则返回0
int IFramelocat(unsigned char* h264_buf, int cur_index,int buf_size);// 定位当前位置是否为P帧起始位置,若当前字节为P帧开始,返回1,否则返回0
int PFramelocat(unsigned char* h264_buf, int cur_index,int buf_size);// 读取H264码流buffer,删除码流中原始SEI字段,并插入用户自定义数据,写入tmp文件中
int H264DelSeiInsertSeiBeforeFrame(unsigned char* h264_buf,int buf_size,unsigned char* sei_buf,int sei_buf_size,FILE *tmp);// 将用户自定义数据user_data按照SEI格式封装进sei_buffer里
int FillSeiPacket(unsigned char* sei_buffer, int sei_size, unsigned char* user_data, int user_data_size);// 从H264码流中解析SEI封装包,并从SEI封装包中解析用户数据
int H264GetSeiUserdata(unsigned char* h264_buf, int h264_buf_size, unsigned char* user_buf, int user_buf_size);

h264Process.cpp

#include "h264Process.h"int FindSei(unsigned char* h264_buf, int cur_index,int buf_size){int cur_index_bak = cur_index;if(cur_index > buf_size-5)  return 0;else if(h264_buf[cur_index]==0x00 && h264_buf[cur_index+1]==0x00 && h264_buf[cur_index+2]==0x00 && h264_buf[cur_index+3]==0x01 && h264_buf[cur_index+4]==0x06){   cur_index += 5;while(cur_index < buf_size){if(h264_buf[cur_index]==0 && h264_buf[cur_index+1]==0 && h264_buf[cur_index+2]==0 && h264_buf[cur_index+3]==1)break;else++cur_index;}// printf("cur_index:%d,cur_index_bak:%d,len is:%d\n",cur_index,cur_index_bak,cur_index-cur_index_bak);return cur_index-cur_index_bak;   //如果找到SEI字段,返回SEI字段长度}else return cur_index-cur_index_bak;  //如果没找到SEI字段,则返回0
}int IFramelocat(unsigned char* h264_buf, int cur_index,int buf_size){if(cur_index > buf_size-5)  return 0;else if(h264_buf[cur_index]==0x00 && h264_buf[cur_index+1]==0x00 && h264_buf[cur_index+2]==0x00 && h264_buf[cur_index+3]==0x01 && h264_buf[cur_index+4]==0x25)return 1;else return 0;
}int PFramelocat(unsigned char* h264_buf, int cur_index,int buf_size){if(cur_index > buf_size-5)  return 0;else if(h264_buf[cur_index]==0x00 && h264_buf[cur_index+1]==0x00 && h264_buf[cur_index+2]==0x00 && h264_buf[cur_index+3]==0x01 && h264_buf[cur_index+4]==0x21)return 1;else return 0;
}int H264DelSeiInsertSeiBeforeFrame(unsigned char* h264_buf,int buf_size,unsigned char* sei_buf,int sei_buf_size,FILE *tmp){for(int i=0; i<buf_size; i++){int len_sei = FindSei(h264_buf,i,buf_size);   //查找SEI字段位置,若当前位置为SEI开始字符,则返回SEI长度,否则返回0if(len_sei == 0){         //SEI字段长度为0,未找到SEI字段,原始内容直接写入if(IFramelocat(h264_buf,i,buf_size))   //SEI字段插入I帧之前fwrite(sei_buf, 1, sei_buf_size, tmp);else if(PFramelocat(h264_buf,i,buf_size))    //SEI字段插入P帧之前fwrite(sei_buf, 1, sei_buf_size, tmp);fwrite(&h264_buf[i], 1, 1, tmp);    //将原始码流写入文件}else if(len_sei > 0){i = i+len_sei-1;    //找到SEI字段,跳过码流buffer中SEI字段长度,相当于删除码流中原始SEI字段// --i;    //判断程序处理完之后,当前下标已经为下一个单元的起始位,for循环还会再加一,因为在此需要减一}else printf("error: findSei() return value min i!\n");}return 1;
}int FillSeiPacket(unsigned char* sei_buffer, int sei_size, unsigned char* user_data, int user_data_size)
{if(sei_size-user_data_size < 25) return -1;unsigned char* data = (unsigned char*)sei_buffer;unsigned char user_data_size_high = user_data_size >> 8;    // 取用户数据长度高8位unsigned char user_data_size_low = user_data_size & 0xFF;   // 取用户数据长度低8位memcpy(data, SEI::start_code, sizeof(unsigned int)); // 插入4字节NALU开始码data += sizeof(unsigned int);   *data++ = SEI::nal_type; // 插入1字节NAL类型识别码,data指针偏移*data++ = SEI::payload_type; // 插入1字节SEI字段编码格式识别码,data指针偏移memcpy(data, SEI::sei_uuid, SEI::sei_uuid_size);  // 插入16字节UUID自定义识别码data += SEI::sei_uuid_size;   *data++ = user_data_size_high;  // 插入1字节用户数据长度高8位*data++ = user_data_size_low;  // 插入1字节用户数据长度低8位memcpy(data, user_data, user_data_size);    // 插入n字节用户自定义数据data += user_data_size;     sei_buffer[sei_size-1] = SEI::sei_tail;  // 插入1字节SEI封装包结尾对齐码if(data-sei_buffer>sei_size) return -2;else return 1;
}int H264GetSeiUserdata(unsigned char* h264_buf, int h264_buf_size, unsigned char* user_buf, int user_buf_size){int sei_num=0,not_match_num=0;unsigned char* user_buf_point = user_buf;for(int i=0; i<h264_buf_size; i++){int len_sei = FindSei(h264_buf,i,h264_buf_size);    //获取码流中当前位置SEI字段长度if(len_sei>0){++sei_num;int not_match_flag=0;for(int j=0; j<16; j++){    //判断SEI字段中UUID是否匹配if(h264_buf[i+6+j] == SEI::sei_uuid[j])continue;else{++not_match_flag;break;}}if(not_match_flag)  //如果not_match_flag为1,则该SEI字段不是我们定义的not_match_num += not_match_flag;else{   //否则该SEI是我们自定义的int h264_user_data_len = (h264_buf[i+22]<<8) + (h264_buf[i+23]&0xFF);printf("h264_buf[i+22]:%d,h264_buf[i+23]:%d,len_sei:%d\n",h264_buf[i+22],h264_buf[i+23],len_sei);printf("h264_user_data_len is %d\n",h264_user_data_len);if(h264_user_data_len!=(len_sei-25)){    //如果读取到的用户数据长度与实际测到的数据长度不匹配printf("error:User data contains a stream header!\n");printf("h264_buf[i+22]:%d,h264_buf[i+23]:%d,len_sei:%d\n",h264_buf[i+22],h264_buf[i+23],len_sei);printf("h264_user_data_len is %d\n",h264_user_data_len);}else{   //若匹配,则正常提取用户数据unsigned char* h264_usr_data = h264_buf+i+24;if(h264_user_data_len > (user_buf_size+user_buf-user_buf_point)){ //判定当前写入的用户数据长度是否大于接收BUF的长度printf("error:recv user_buf_size is too min! \n");break;}memcpy(user_buf_point,h264_usr_data,sizeof(unsigned char)*h264_user_data_len);//将解析出的用户数据复制到接收BUF中user_buf_point += h264_user_data_len;    //接收BUF指针偏移,方便下一帧数据写入for(int k=0;k<10;k++)   //用户数据后面添加10个0作为用户数据分割标识*user_buf_point++ = 0;}}i+=len_sei-1;}else continue;}// printf("not_match_num is %d\n",not_match_num);return sei_num;
}

main.cpp

// #include <stdio.h>
#include <unistd.h>
#include <malloc.h>
#include "h264Process.h"// #define H264_SIZE 1*1024*1024   //读取1MB文件大小
#define H264_SIZE 2*1024*1024   //读取0.1MB文件大小,大概包含2-3帧图像
#define USER_DATA_SIZE 512   //定义用户自定义数据大小
#define SEI_FIX_SIZE 25     //SEI封装包固有长度为25//SEI字段插入H264码流文件程序
int insertSeiProcess(){//读取H264源文件,删去SEI字段 FILE* h264_file=NULL;FILE *tmp = NULL;static unsigned char fullBuffer[H264_SIZE+4] = {0};unsigned char* buffer = fullBuffer;h264_file = fopen("test.h264", "rb+");  //带SEI字段的H264源文件tmp = fopen("tmp.h264","wb+");  //写入自定义数据的的新H264文件if (!h264_file){printf("ERROR:Open h264 file fialed.\n");return -1;}int size = fread(buffer, 1, H264_SIZE, h264_file);printf("h264 size:%d\n",size);int user_data_size = 512;int sei_size = user_data_size + SEI_FIX_SIZE;unsigned char* user_data_buf = (unsigned char*)malloc(sizeof(unsigned char)*user_data_size);unsigned char* sei_buf = (unsigned char*)malloc(sizeof(unsigned char)*sei_size);if(user_data_buf==NULL || sei_buf==NULL){printf("malloc faild!\n");return -1;}memset(user_data_buf,0,sizeof(unsigned char)*user_data_size);memset(sei_buf,0,sizeof(unsigned char)*sei_size);for(int i=0; i<100; i++){   //填充自定义数据user_data_buf[i]=i+1;// printf("%x ",user_data_buf[i]);}     // printf("\n");int sei_stat = FillSeiPacket(sei_buf, sei_size, user_data_buf, user_data_size);   //将用户自定义数据封装成SEI格式包if(sei_stat < 0) printf("fillSeiPacket faild\n");else printf("fillSeiPacket successed\n");// for(int i=0; i<sei_size;i++) printf("%x ",sei_buf[i]);// printf("\n");H264DelSeiInsertSeiBeforeFrame(buffer,size,sei_buf,sei_size,tmp);free(user_data_buf);    //释放动态内存空间free(sei_buf);fclose(h264_file);fclose(tmp);
}//从H264文件中提取自定义SEI字段
int getSeiProcess(){FILE* h264_file = fopen("all_sei.h264","rb+");	//码流源文件FILE* sei_file = fopen("tmp.txt","w+");	//将读取到的自定义数据写入txt文档FILE* tmp1_file = fopen("h264.txt","w+");	//将H264码流数据读出来以便debugif (!h264_file || !sei_file){printf("ERROR:Open tmp.h264 or tmp.txt fialed.\n");return -1;}fseek(h264_file, 0, SEEK_END);      //将文件指针偏移到文件尾int file_size = ftell(h264_file);   //获取h264文件大小fseek(h264_file, 0, SEEK_SET);      //将文件指针偏移到文件头unsigned char* h264_buf = (unsigned char*)malloc(sizeof(unsigned char)*file_size);  //开辟h264_buf空间memset(h264_buf,0,sizeof(unsigned char)*file_size);     //初始化h264_bufint h264_size = fread(h264_buf, 1, file_size, h264_file);   //将h264文件读到h264_buf中printf("h264 file size is %d\n",h264_size);unsigned char* user_buf = (unsigned char*)malloc(sizeof(unsigned char)*h264_size);  //开辟SEI接收B空间memset(user_buf,0,sizeof(unsigned char)*h264_size);     //初始化h264_bufint find_sei_num = H264GetSeiUserdata(h264_buf, h264_size, user_buf, h264_size);printf("found sei num is %d\n",find_sei_num);for(int i=0; i<h264_size; i++)fprintf(tmp1_file, "%x ", h264_buf[i]);for(int i=0; i<file_size; i++)fprintf(sei_file, "%d ", user_buf[i]);printf("\n");free(h264_buf);free(user_buf);fclose(h264_file);fclose(sei_file);fclose(tmp1_file);
}int main(int argc, char** argv)
{insertSeiProcess();//getSeiProcess();return 0;
}

5. 总结

以上就是H264码流中插入自定义数据的代码,H264码流文件是一个二进制文件,该代码是直接对二进制文件进行改造,不依赖于FFMPEG或者X264库。本代码中识别的I帧和P帧的帧头为25和21,常见的帧头为65和61,区别只是解码器识别该帧的重要程度,可自行修改。

转自:https://blog.csdn.net/weixin_42289213/article/details/125279309


http://www.ppmy.cn/news/580859.html

相关文章

android 微博分享 linkcard,#游戏王OCG# 1106 仪式枪管登场 ... - @NW任天堂世界游戏王区 的微博精选 - 微博国际站...

#游戏王OCG# 1106 仪式枪管登场 デュアルウィール・ドラゴン(双持手枪龙) 暗 8星 龙 2500 1500 这个卡名的①②的效果1回合各能使用1次。①&#xff1a;把这张卡解放&#xff0c;以自己墓地2只卡名不同的「ヴァレット」怪兽为对象才能发动。那些怪兽守备表示特殊召唤。②&#…

ygo游戏王卡组_【YGO游戏王】异色眼——卡组构筑(文字版)

计划以后把自己做的卡组pp做成专栏&#xff0c;方便浏览&#xff0c;最后附上卡组.txt&#xff0c;打开笔记本复制保存&#xff0c;把格式.txt改成.ydk就可生成卡组文件啦。(卡组复制虽简单&#xff0c;全是个人的看法&#xff0c;学习思路并修改&#xff0c;大家都是游戏王&am…

【游戏王arc-v卡片力量SP改名字ID教程】

【游戏王arc-v卡片力量SP改名字ID教程】&#xff1a;本教程适用于手机和电脑的PPSSPP模拟器1.2.1版本以上&#xff0c;其他的不确定。同时本教程方法大致方法适用于同系列游戏王或部分PSP游戏&#xff0c;记住&#xff0c;是大致方法&#xff0c;因为内存地址每个游戏不一样。 …

写给毕业季的学生们|我的五次 offer 选择经历

最近临近毕业季&#xff0c;群里有好多朋友在问面试和 offer 选择的问题&#xff0c;我分享下我过往的相关经历&#xff0c;希望能给各位朋友有所启发。 我是谁&#xff1f; 大家好&#xff0c;我是拭心&#xff0c;内蒙古人&#xff0c;16 年本科毕业于西安电子科技大学&#…

游戏王

游戏王惊天动地私服发布站

QGIS批量将EXCEL中内容连接到对应矢量要素属性表中

要在QGIS中批量将Excel表格中的内容连接到对应矢量要素的属性表中&#xff0c;可以使用PyQGIS编程来完成。以下是详细的示例代码&#xff1a; from qgis.core import QgsVectorLayer, QgsProject import csv # 设置矢量图层文件路径 layer_file /path/to/your_vector_layer.s…

对我国超级计算机的应用,要加强我国超级计算机应用人才储备

钱德沛(863计划重点专项专家组组长): 本报讯(记者田雅婷)首届“中国大学生超级计算机竞赛暨ISC12国际大学生超级计算机竞赛中国区选拔赛”日前启动,国家863计划重点专项专家组组长钱德沛在启动仪式上呼吁,要加强我国超级计算机应用人才储备,消除发展应用瓶颈,使之真正成为我国科…

通过统计7万多真实人名生成可信的随机人名

简介 随机生成人名有很多应用场景&#xff0c;比如数据库系统学习&#xff0c;网络攻防等。为了生成真实可信的人名&#xff0c;本文通过对约7.1万真实的人名进行频率学习&#xff0c;统计出最常用的100个姓&#xff0c;100个男名和100个女名&#xff08;均为双字&#xff09;…