目录
1、H264编码原理和基本概念
1.1、h.264编码原理
1.2、h.264编码相关的一些概念
2、H264的NAL单元详解
2.1、VCL和NAL的关系
2.2、H.264视频流分析工具
2.3、h264视频流总体分析
2.4、相关概念
3、H264的NAL单元---sps和pps
3.1、sps和pps详解
3.2、H264的profile和level
3.3、sequence
4、rtsp协议和源码框架
4.1、源码框架函数调用关系
4.2、直接发送与环状buffer发送
4.3、RTP发送一帧数据的两种不同发送格式->整发送和分包发送
(1)图像冗余信息:空间冗余、时间冗余
(2)视频编码关键点:压缩比高、算法复杂度小、还原度高
(3)H.264的2大组成部分:VCL和NAL
VCL关心如何进行视频压缩
NAL关心压缩后的视频流如何被网络传输和解码播放
(1)宏块 MB(macroblock):多个像素组成的一块,宏块是视频压缩算法的最基本单位
由于图像本身在局部,颜色具有相似性,所以可以把一幅图像分成若干个宏块
(2)片 slice:构成帧的一部分
一帧图像=若干个slice(可以是一个slice)
一个slice=若干个MB
一个MB=多个像素
(3)帧 frame:一整幅完整的图像
(4)I帧、B帧、P帧:帧有好几种类型
I帧:非参考帧,这一帧图像的内容只和本身有关,和前一帧后一帧图像的内容无关,一般作为起始帧,因为这一帧没有任何参考,所以只能对这一帧进行帧内压缩(空间冗余上的优化)
B帧:参考帧,这一帧图像的内容不光和本身有关,还和前一帧或后一帧图像的内容有关(空间冗余+时间冗余的优化)
P帧:参考帧,这一帧图像的内容不光和本身有关,还和前一帧图像的内容有关(空间冗余+时间冗余的优化)
(5)帧率 fps:一秒中有多少帧,帧率高(慢动作),帧率低(快动作)
(6)像素->宏块->片->帧->序列->码流
(1)VCL只关心编码部分,重点在于编码算法以及在特定硬件平台的实现,VCL输出的是编码后的纯视频流信息,没有任何头信息
(2)NAL关心的是VCL的输出纯视频流如何被表达和封包以利于网络传输
(3)SODB:String Of Data Bits(VCL输出的纯视频流)
(4)RBSP:Raw Byte Sequence Payload
(5)NALU:Network Abstraction Layer Units NALU是H264文件的基本组成单元
(6)关系:
SODB + RBSP trailing bits = RBSP
NALU header(1 byte) + RBSP = NALU
H264文件由若干个序列组成 -> 序列由若干个帧/slice组成 -> 帧/slice由分隔符和NALU单元组成 -> 去掉NALU header得到RBSP -> 去掉RBSP trailing得到SODB-> VLC播放器解码播放SODB
(7)总结:做编码器的人关心的是VCL部分;做视频传输和解码播放的人关心的是NAL部分
(1)雷神作品:SpecialVH264.exe
(2)国外工具:Elecard StreamEye Tools
(3)二进制工具:winhex
(4)网络抓包工具:wireshark
(5)播放器:vlc
(1)h264标准有多个版本,可能会有差异,具体差异不详
(2)网上看的资料有时讲法会有冲突,或者无法验证的差异
(3)这里以海思平台为主、为准、为案例,不能保证其他平台也完全一样
(4)海思平台编码出来的H.264码流(就是一个二进制文件)都是一个一个序列组成,序列包含:1sps+1pps+1sei+1I帧+若干p帧
(1)序列 sequence,一般一秒发一个sequence,也就是说sequence(除去sps、pps、sei)和帧率相等
(2)分隔符:00 00 00 01的四字节组合,是用来做识别的,不是有效数据,表示一个slice的开始,表示我们有一个新的片到来。
帧的有效数据部分是不会出现(00 00 00 01)的,h264的标准规定有效数据不允许出现连续的3个00,会在第二个00后面,第三个00前面添加一个03进去,用(00 00 03 00)来表示(00 00 00)。
(3)sps
(4)pps
(5)sei
(6)NALU header:分隔符后的第一个字节
NALU header有8个位
bit7(forbidden_zero_bit):禁止位,默认位0,值为1表示语法出错
bit6~5(nal_ref_idc):重要性,代表这一帧的内容在视频流中的重要程度,重要程度由大到小11>10>01>00
bit4~1(nal_unit_type):NAL单元类型
1:表示 非I帧,具体是P还是B帧不知道
5:表示 I帧
6:表示 SEI
7:表示 SPS
8:表示 PPS
参考博文:
H264(NAL简介与I帧判断)_h264 判断i帧_jefry_xdz的博客-CSDN博客
参考博文:
https://www.cnblogs.com/wainiwann/p/7477794.html
在H.264标准协议中规定了多种不同的NAL Unit类型,其中类型7表示该NAL Unit内保存的数据为Sequence Paramater Set(序列参数集),描述这个序列的参数信息都在这里。在H.264的各种语法元素中,SPS中的这些参数信息至关重要。如果其中的数据丢失或出现错误,那么解码过程很可能会失败。SPS和PPS中的信息会用于播放器的初始化使用。
SPS中保存了一组视频序列的全局参数。所谓的视频序列即原始视频的一帧一帧的图像数据经过编码之后组成的序列。而每一帧的编码后数据所依赖的参数保存于图像参数集(PPS)中,例如P帧需要参考前面的帧进行编码,那么是怎么参考的,这个参考数据就保存在PPS中。
一般情况SPS和PPS位于整个码流的起始位置。但在某些特殊情况下,在码流中间也可能出现这两种结构,主要原因可能为:
视频播放器为了让后续的解码过程可以使用SPS中包含的参数,必须对SPS其中的数据进行解析。其中H.264标准协议中规定的SPS格式位于文档的7.3.2.1.1部分,如下图所示:
(1) profile_idc:
标识当前H.264码流的profile。我们知道,H.264中定义了三种常用的档次profile:
基准档次:baseline profile;
主要档次:main profile;
扩展档次:extended profile;
在H.264的SPS中,第一个字节表示profile_idc,根据profile_idc的值可以确定码流符合哪一种档次。判断规律为:
profile_idc = 66(0x42) → baseline profile;
profile_idc = 77 → main profile;
profile_idc = 88 → extended profile;
在新版的标准中,还包括了High、High 10、High 4:2:2、High 4:4:4、High 10 Intra、High 4:2:2 Intra、High 4:4:4 Intra、CAVLC 4:4:4 Intra等,每一种都由不同的profile_idc表示。
当前码流中,profile_idc = 0x42 = 66 ,因此profile档次为 baseline profile;
(2) level_idc
标识当前码流的Level。编码的Level定义了某种条件下的最大视频分辨率、最大视频帧率等参数,码流所遵从的level由level_idc指定。
当前码流中,level_idc = 0x1F = 31,因此码流的级别为3.1。
Level为3.1的特性:
参考博文:
h264中profile和level的含义_xiaojun11-的博客-CSDN博客
Profile是对视频压缩特性的描述(CABAC呀、颜色采样数等等),简而言之就是压缩算法的档次。Level是对视频本身特性的描述(码率、分辨率、fps)。
简单来说,Profile越高,就说明采用了越高级的压缩特性(越高级的压缩算法)。Level越高,视频的码率、分辨率、fps越高。
一些移动设备(手机、游戏机、PMP)由于性能有限,不支持全部高级视频压缩特性和高分辨率图像,只支持基础压缩特性和分辨率低一些的图像。为了让这个限制更加清晰明了,H264从低到高划分了很多Profile和Level。
profile主要是定义了编码工具(压缩算法)的集合,不同的profile,包含了不同的编码技术;
h.264的profile有三种Baseline Profile(基本的)、Main Profile(主线的)、High Profile(高等级的)
(1)一段h.264的码流其实就是多个sequence组成的
(2)每个sequence均有固定结构:1sps+1pps+1sei+1I帧+若干p帧
(3)sps和pps和sei描述该sequence的图像信息,这些信息有利于网络传输或解码
(4)I帧是关键,丢了I帧整个sequence就废了,因为I帧是自参考的,其它的帧都参考于它,每个sequence有且只有1个I帧
(5)p帧的个数等于fps-1
(6)I帧越大则P帧可以越小,反之I帧越小则P帧会越大
I帧越大,说明I帧本身压缩比例不高,参考信息比较多,比较详细,所以P帧就越好参考,就可以越小
(7)I帧的大小取决于图像本身内容,和压缩算法的空间压缩部分
如果图像本身色彩丰富、画面复制,那么I帧就会很大
(8)P帧的大小取决于图像变化的剧烈程度
(9)CBR和VBR下P帧的大小策略会不同,CBR时P帧大小基本恒定,VBR时变化会比较剧烈
CBR:固定码率
VBR:可变码率
rtsp协议参考:
链接:https://pan.baidu.com/s/1IHU8vaFShMq3mOUz3c2iDw?pwd=0000
提取码:0000
main
RtspServer_init
RtspServerListen //线程函数---达到PLAY状态
socket //创建TCP套接字
setsockopt //设置端口复用
bind //绑定服务器IP和端口
listen //监听、并告诉linux系统这是个服务器
while(accept) //不断等待客户端连接
setsockopt
for //实际只执行一次
RtspClientMsg //此函数是重点,创建一个线程去处理这此连接
pthread_detach //分离线程,子线程结束后自动回收资源
while //如果当前不是未连接状态
{
recv //读1024个字节
if(没读到)
{
退出线程
}
ParseRequestString //解析客户端发来的Requst请求并填充字符串数组
if(是OPTIONS请求)
OptionAnswer //返回服务器提供的可用方法
sprintf //组包
strcat //连接RTSP头和sdp信息
send //发送
if(是DESCRIBE请求)
DescribeAnswer //返回sdp包给播放器client去配置解码
GetLocalIP //获取本地IP
ioctl
sprintf //组包
send //发送
if(是SETUP请求)
SetupAnswer
ParseTransportHeader //解析客户端发来的SETUP Request,然后填充到变量中
GetLocalIP //获取本地IP
sprintf //组包
send //发送
if(是PLAY请求)
PlayAnswer
sprintf //组包
send //发送
socket //创建一个UDPsocket
//由此可见,之前的TCPsocket是一个命令通道,这里的UDPsocket是一个传输数据的通道
if(是PAUSE请求) //暂停请求
PauseAnswer //源码分析得知,这里并没有实现暂停码流的传输
sprintf //组包
send //发送
if(是TEARDOWN请求)
TeardownAnswer
sprintf //组包
send //发送
close(udp) //关闭数据传输通道UDPsocket
close(tcp) //关闭命令传输通道TCPsocket
}
vdRTPSendThread //线程函数---发送流媒体的数据
while(1) //每5ms判断当前是否有数据要发送
{
if(!list_empty) //链表为空,则表明当前没有数据要发送
{
get_first_item //取出链表中第一个非空节点
VENC_Sent//发送码流数据
sendto //UDP发送
list_del //从链表中把节点去掉
}
usleep(5000); //延时5ms
}
SAMPLE_VENC_720P_CLASSIC
SAMPLE_COMM_VENC_StartGetStream
SAMPLE_COMM_VENC_GetVencStreamProc //线程函数
HI_MPI_VENC_GetChnAttr
SAMPLE_COMM_VENC_GetFilePostfix
HI_MPI_VENC_GetFd
while
{
HI_MPI_VENC_Query
malloc
HI_MPI_VENC_GetStream //获取一帧编码完的码流
//以下两种方式二选一
SAMPLE_COMM_VENC_Sentjin //此函数是重点---编码完后直接发送
VENC_Sent //直接发送
saveStream //此函数是重点---编码完后保存到环状buf中
malloc //生成一个链表节点
INIT_LIST_HEAD //初始化这个节点的next指针和prev指针都指向自己
填充节点内容
list_add_tail //将节点加入链表
//当我将编码完的数据保存到环形链表后,此时链表就有节点数据了
//然后就可以在 vdRTPSendThread 线程中发送了
HI_MPI_VENC_ReleaseStream
free
}
直接发送:编码完直接发送,编多少发多少
代码体现在:
SAMPLE_COMM_VENC_GetVencStreamProc线程函数:
HI_MPI_VENC_GetStream //获取编码完的码流
SAMPLE_COMM_VENC_Sentjin //发送函数
缺陷:视频采集和编码速度 与 网络传输速度 都是变动的,速度不一定一致,会导致系统进入漫长的等待,不能更好的实时视频监控
环状buffer发送:编码完放到环状buffer中,待发送
环状buffer即环形链表,当生产者(获取码流的线程)编码完一帧数据就将这帧数据作为节点加入链表,消费者(发送线程)只管从中取。
对比代码:SAMPLE_COMM_VENC_GetVencStreamProc线程函数中,调用HI_MPI_VENC_GetStream获取编码完的码流,调用saveStream将码流数据节点加入链表,vdRTPSendThread线程从链表中取走节点,然后调用VENC_Sent发送。
当链表中没有节点,则消费者要阻塞,等待生产者编码完将数据节点加入链表。环形链表相比数组的优点是使用内存可以足够大,直到内存撑爆。
代码体现在:
SAMPLE_COMM_VENC_GetVencStreamProc线程函数:
HI_MPI_VENC_GetStream //获取编码完的码流
saveStream //编码完后保存到环状buf中
vdRTPSendThread线程函数:
get_first_item //取出链表中第一个非空节点
VENC_Sent //发送码流数据
list_del //从链表中把节点去掉
接下来就只用关注VENC_Sent函数内是怎么组包,然后通过UDP传输码流数据了。。
关于rtp头的timestamp位:
RTP timestamp与帧率及时钟频率的关系_帧率 时钟频率_jasonhwang的博客-CSDN博客
关于RTP发送一帧数据的两种不同发送格式:整包发送和分包发送
https://www.cnblogs.com/yjg2014/p/6144977.html
VENC_Sent函数全解:
HI_S32 VENC_Sent(char *buffer,int buflen)
{
HI_S32 i;
int is=0;
int nChanNum=0;
for(is=0;ispayload = RTP_H264;
rtp_hdr->version = 2; //RTP版本号
rtp_hdr->marker = 0;
rtp_hdr->ssrc = htonl(10); //信源标记
if(nAvFrmLen<=nalu_sent_len) //如果这一帧数据的长度<=1400
{
rtp_hdr->marker=1; //标记一帧的结束
rtp_hdr->seq_no = htons(g_rtspClients[is].seqnum++); //下一帧的序列号
nalu_hdr =(NALU_HEADER*)&sendbuf[12];
nalu_hdr->F=0;
nalu_hdr->NRI= nIsIFrm;
nalu_hdr->TYPE= nNaluType;
nalu_payload=&sendbuf[13];
memcpy(nalu_payload,buffer,nAvFrmLen);
g_rtspClients[is].tsvid = g_rtspClients[is].tsvid+timestamp_increse;
rtp_hdr->timestamp=htonl(g_rtspClients[is].tsvid);
bytes=nAvFrmLen+ 13 ;
sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
}
else if(nAvFrmLen>nalu_sent_len) //如果这一帧数据的长度>1400
{
int k=0,l=0;
k=nAvFrmLen/nalu_sent_len; //这一帧数据的长度/1400
l=nAvFrmLen%nalu_sent_len; //这一帧数据的长度对1400取余
int t=0;
g_rtspClients[is].tsvid = g_rtspClients[is].tsvid+timestamp_increse; //计算timestamp
rtp_hdr->timestamp=htonl(g_rtspClients[is].tsvid); //把timestamp放入RTP包头
while(t<=k) //循环k+1次,也就是发k+1个包,代表这一帧发完
{
rtp_hdr->seq_no = htons(g_rtspClients[is].seqnum++);
if(t==0) //第一包
{
rtp_hdr->marker=0; //填充完RTP_header的12个字节后,接下来填充的是NALU的分包形式
fu_ind =(FU_INDICATOR*)&sendbuf[12];//fu_indicato是NALU头的分包形式的第一部分
fu_ind->F= 0;
fu_ind->NRI= nIsIFrm;
fu_ind->TYPE=28; //Type为FU-A
fu_hdr =(FU_HEADER*)&sendbuf[13]; //fu_header 是NALU头的分包形式的第二部分
fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->S=1; //表示帧开始
fu_hdr->TYPE=nNaluType; //NALU type
nalu_payload=&sendbuf[14]; //填充完NALU的分包形式后,接下来开始填充有效数据
memcpy(nalu_payload,buffer,nalu_sent_len);
bytes=nalu_sent_len+14; //发送的长度是有效数据长度+14(14就是前面添加的这些信息)
sendto( udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
t++;
}
else if(k==t) //最后一包
{
rtp_hdr->marker=1; //标记一帧的结束
fu_ind =(FU_INDICATOR*)&sendbuf[12];
fu_ind->F= 0 ;
fu_ind->NRI= nIsIFrm ;
fu_ind->TYPE=28;
fu_hdr =(FU_HEADER*)&sendbuf[13];
fu_hdr->R=0;
fu_hdr->S=0;
fu_hdr->TYPE= nNaluType;
fu_hdr->E=1; //表示帧结束,和marker一样
nalu_payload=&sendbuf[14];
memcpy(nalu_payload,buffer+t*nalu_sent_len,l);
bytes=l+14;
sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
t++;
}
else if(tmarker=0;
fu_ind =(FU_INDICATOR*)&sendbuf[12];
fu_ind->F=0;
fu_ind->NRI=nIsIFrm;
fu_ind->TYPE=28;
fu_hdr =(FU_HEADER*)&sendbuf[13];
//fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->S=0;
fu_hdr->E=0; //S=0,E=0表示既不是开头也不是结尾
fu_hdr->TYPE=nNaluType;
nalu_payload=&sendbuf[14];
memcpy(nalu_payload,buffer+t*nalu_sent_len,nalu_sent_len);
bytes=nalu_sent_len+14;
sendto(udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));
t++;
}
}
}
}
//------------------------------------------------------------
}