海思项目学习记录 -4、H.264及RTSP协议实时传输
发布日期:2021-06-29 11:09:23 浏览次数:3 分类:技术文章

本文共 8833 字,大约阅读时间需要 29 分钟。

一、H.264介绍

1、h.264编码原理

(1)图像冗余信息:空间冗余(一副图片一个区域的RGB或YUV变化不大)、时间冗余(视频时间过去但是图像没有什么变化)

(2)视频编码关键点:压缩比、算法复杂度、还原度;(研究算法的关键就是这三者的平衡)
(3)H.264的2大组成部分:VCL(视频编码)和NAL(压缩之后的视频流如何被网络传输,那边如何解码播放)

2、编码相关的一些概念

宏块 MB(macroblock) 多个像素组成的一个区域,就是因为在一份图像中在不同的一小个区域是具有相似性的,因此将整个图像分成很多个宏块,并且每个宏块的像素相似度会很高,因此宏块也是视频压缩的基本单元。

片 slice 一副图片的一部分。
帧 frame 一帧图像有一个或多个slice ;帧是跟我们完成图像相对应一个概念,帧就是一整副图片,
一个帧就是一副图片,一个帧包含一个片或者多个片,一个片包含多个宏块,一个宏块包括多个像素。这样的组成结构的。
多个帧就组成一个序列,一些序列就是码流了。
像素->宏块->片->帧->序列->码流

帧又在h.264中分为很多种

I帧;非参考帧,就是没有参考任何东西的最原始的帧,就是只做了空间上的压缩,无法参考做时间压缩的。
B帧,P帧就是参考帧,进行了时间的冗余的压缩操作的,是需要参考前后帧的
在这里插入图片描述

帧率 fbs 就是1s中有多少帧,就能记录一些视频的细节。运动相机的帧率就要很高,安防的就不需要了。

3、H.264的NAL单元

3.1、引入目的

VCL只关心编码部分,重点在于编码算法以及在特定硬件平台的实现,VCL输出的是编码后的纯视频流信息,没有任何冗余头信息。
NAL关心的是VCL的输出纯视频流如何被表达和封包以利于网络传输,因为VCL出来的纯视频流没有一点相关信息记录因此他是不能被传输和解码的,别人不知道以什么格式传输什么格式解码,因此需要加入NAL进行封装,加入一些头信息。
SODB:String Of Data Bits 这个就是VCL部分输出的纯视频流。
RBSP:Raw Byte Sequence Payload 就是SODB最纯的信息加一些信息即使RBSP
NALU:Network Abstraction Layer Units 在RBSP的基础上加一个字节的头,那么就组成一个NALU。
关系:
SODB + RBSP trailing bits = RBSP
NAL header(1 byte) + RBSP = NALU

如何分析NALU的;就是取第一个字节分析NAL就可以得知一些信息从而可以在RBSP中取到纯视频流,然后再根据之前压缩编码的步骤进行解压缩还原的操作就可以播放出来了。

总结;做编码器的人关心的是VCL部分;做视频传输和解码播放的人关心的是NAL部分,因为采集芯片处理出来后就是SODB视频流了。

3.2、工具介绍

(1)雷神作品:SpecialVH264.exe 是根据规定协议解析二级制数位的不同意义将结果解析出来

在这里插入图片描述

(2)国外工具:Elecard StreamEye Tools 可以将帧与图片一一对应起来变化

在这里插入图片描述

(3)二进制工具:winhex 将文件以二进制打开

在这里插入图片描述
(4)网络抓包工具:wireshark
(5)播放器:vlc
audacity2.3.1 播放制作音频
MediaInfo_GUI_0.7.73_Windows 音视频信息
YUVPlayer-Deluxe 播放YUV文件

查看雷神的工具可以得知

海思平台编码出来的H.264码流都是一个序列sequence包含:1sps+1pps+1sei+1I帧+若干p帧
码流就是一些二级制的,这些二进制会组成很多个序列,每个序列都有自己的组成关系。

3.3、了解H.264的一些概念

(1)序列 sequence

因为视频流是一个持续很长的一个过程,因此它是有必要进行分段传输的,而分的段就叫序列。并且也是存在I帧,P帧,如果长时间只有一个I帧,其余都参考同一个I帧则失去了参考帧的意义了,因此分段传输多个I帧都是非常有必要的。每个序列都是独立的,即使前面那个序列出现了问题也不会影响后面序列的变化,序列与帧率有关系的,1s一个序列,1s的头一帧都是I帧。一个序列 = I帧+fbs-1个P帧。
(2)分隔符 00 00 00 01在码流传输中就是用来分割一个个片的,这个片可能是SPS,PPS,SEI,或I帧,P帧的,这些片的开头都是以分隔符来开头的,
如果在有效数据里面存在00 00 00 01的情况,那么h.264那边规定在有效数据里面不能出现三个00的情况,如果有那么会在第二个00的后面加一个03进去 变成00 00 03 00 01.
在这里插入图片描述
(3)sps
peofile 和level字节
Profile是对视频压缩特性的描述(CABAC呀、颜色采样数等等)。就是对视频压缩的一个等级的一个描述
H.264 Profiles;Baseline Profile 、Main Profile、High Profile三个级别,对应的profile_id就是66,77,88,而sps的第一个有效字节(除开NALU)就是profile_id,如上图是0x42 = 66 对应 Baseline Profile。在新的编码继续引进不同的压缩等级。
Level是对视频本身特性的描述(码率、分辨率、fps)。
简单说;Profile越高,就说明采用了越高级的压缩特性。Level越高,视频的码率、分辨率、fps越高。
从sps里面还可以得知图像基本信息等。
参考博客;
https://www.cnblogs.com/wainiwann/p/7477794.html
https://blog.csdn.net/xiaojun111111/article/details/52090185

(4)pps

图像参数集合
(5)sei
(6)NALU

NALU简介与I帧的判断,找I帧是关键,I帧是无参考帧,并且I帧是每个序列的靠头帧,因此I帧是十分关键的。

https://blog.csdn.net/jefry_xdz/article/details/8461343

以00 00 00 01分割之后的下一个字节就是NALU类型,将其转为二进制数据后,解读顺序为从左往右算,如下:

(1)第1位禁止位,值为1表示语法出错
(2)第2~3位为参考级别 这帧在视频流中的重要性的表征,在网络不好的情况需要保证实时性则需要丢掉一些东西,则可以根据重要性来丢包
(3)第4~8为是nal单元类型
在这里插入图片描述
其中0x67的二进制码为:
0110 0111
4-8为00111,转为十进制7,参考第二幅图:7对应序列参数集SPS

其中0x68的二进制码为:

0110 1000
4-8为01000,转为十进制8,参考第二幅图:8对应图像参数集PPS

其中0x65的二进制码为:

0110 0101
4-8为00101,转为十进制5,参考第二幅图:5对应IDR图像中的片(I帧)

解码器都是必须从sps开始解码的,从其他地方开始不知道怎么解码,因此如果随意截取一段码流开始解码如果是P帧那么会直接跳过直到遇到sps才开始解码,因此可能会跳过过多的P帧,则在这些特殊情况下我们需要在P帧码流的中间插入SPS PPS SEI这20个字节,便于从中间解码的时候也能成功解码。

参考博客https://www.cnblogs.com/wainiwann/p/7477794.html

sequence

(1)一段h.264的码流其实就是多个sequence组成的
(2)每个sequence均有固定结构:1sps+1pps+1sei+1I帧+若干p帧
(3)sps和pps和sei描述该sequence的图像信息,这些信息有利于网络传输或解码
(4)I帧是关键,丢了I帧整个sequence就废了,每个sequence有且只有1个I帧
(5)p帧的个数等于fps-1
(6)I帧越大则P帧可以越小,反之I帧越小则P帧会越大
(7)I帧的大小取决于图像本身内容,和压缩算法的空间压缩部分
(8)P帧的大小取决于图像变化的剧烈程度
(9)CBR和VBR下P帧的大小策略会不同,CBR图像的码率是稳定的,也就是P帧大小基本恒定,而VBR时变化会比较剧烈,不管码率的稳定,
VBR主要是维持图像的清晰度,因此码率有可能会有很大变化,突然传输的字节就变得很多了。
因为每帧都是1s的传输时间,而帧的大小始于压缩算法有关的,VBR就实时维持图像本身,而CBR就是抱枕每帧大小不会剧烈变化。

二、rtsp实时传输协议

1、RTSP 概述

RTSP;实时流传输协议, 是 TCP/IP 协议体系中的一个应用层协议。专门用于实时传输码流的通讯协议。

RTSP是双向的;HTTP 与RTSP 相比, HTTP 请求由客户机发出, 服务器作出响应, 使用 RTSP 时, 客户机和服务器都可以发出请求, 即RTSP 可以是双向的;
RTSP可以同时传输多个串流;RTSP 是用来控制声音或影像多媒体串流协议, 并允许同时多个串流需求控制, 传输时所用的网络通信协定并不在其定义范围内。
RTSP 协议默认端口: 554, 默认承载协议为 TCP。

1.2、RTSP 特性

流控分离;传输控制信息请求回复的关键字这些信息的传输和数据流的传输是分割开来的,因此也就才有了在数据流传输过程中,客户端可以发送请求TEARDOWN进行关闭传输,代码体现就是不同的线程操作。并且数据流传输为了保证实时性一般使用udp协议,而控制信息传输而是TCP协议

可扩展性;RTSP 协议是基于文本的协议, 所以具有较强的可扩展性。
安全机制;RTSP 使用网页安全机制。
记录设备控制;协议可控制记录和回放设备。
代理和防火墙友好;协议可由应用和传输防火墙代理, 防火墙需要理解 SETUP 方法, 为 UDP 流打开一个“缺口” 。

1.3、RTSP 状态转换图解

在这里插入图片描述

状态直接箭头上的关键字就是控制命令。
如最首先是服务器这边初始化之后在INIT状态,等待客户端的SETUP请求之后就转换到READY就绪状态,等待客户端传来PLAY请求则开始播放进入PLAYING运行状态的,在运行状态也可以接收TEARDOWN请求进行停止传输。

1.4、RTSP 消息格式

在这里插入图片描述

在这里插入图片描述

1.5、RTSP 中的 C(Client) 与 S(Server) 交互流程图解

在这里插入图片描述

1.6、SDP 在视频传输过程中信息解释

1) m 字段: “m= ” :

样例: m=video 0 RTP/AVP 96
解释: 作为媒体描述的重要组成部分描述了媒体信息的详细内容: 表示一个 Session 的 video 是通过 RTP 格式来传送的, 其中 Payload 值是 96(表示是H.264文件), 传输端口开没有确定(0表示不确定); 其中 Payload 值请参考 RFC 文档。
2) b 字段: “b=:” :
样例: b=AS:70
解释: 70 是 VIDEO 的 bitrate
3) a 字段: “a=:” :
样例: a=control:trackID=0
解释: 表示通过流媒体 0 来传输 VIDEO, a 的属性有很多种, 常用的是 control, 比如“a=range:npt=0-72.080000 ”表示流媒体的长度, 再比如:“a=rtpmap:96 MPEG4-GENERIC/32000/2” 表示音频为 AAC 的其 sample
为 32000 等等, 根据具体的要求来使用。
4) v 字段: 表示 SDP 的版本, 一般填 0。
5) o 字段: 定义了 SDP 一些源信息,“o=

username: 该服务器的名称; session id: 会话 ID, 版本信息, 网络类型: IPV4 或 IPV6 传输地址;
样例: “o=StreamingServer 3331435948 1116907222000 IN IP4 192.168.1.123”
6) c 字段: “c=IN IP4 0.0.0.0 //connect 的信息, 分别描述了: 网络协议, 地址的类型, 连接地址”
7) t 字段: “t=0 0 //时间信息, 分别表示开始的时间和结束的时间, 一般在流媒体的直播的时移中见的比较多” 。

1.7、RTP timestamp 每帧的时间

RTP timestamp表示每帧的时间,由于一个帧(如I帧)可能被分成多个RTP包,所以多个相同帧的RTP timestamp相等。(可以通过每帧最后一个RTP的marker标志区别帧,但最可靠的方法是查看相同RTP timestamp包为同一帧。)

具体说明可参考一下博文
https://blog.csdn.net/jasonhwang/article/details/7316128

1.8、NALU和fu_indicator和fu_head

在发送过程中因为一帧很长,那么可能需要涉及拆包,但是拆包也必须保证每包都能被解析出来,因此需要把传输的信息也要加入到包头。根据之前帧和NALU的了解,我们可以知道每一帧都有一个NALU来记录帧里面的数据信息便于传输和解码。那么现在要将一帧拆开分成很多包,那么我们就需要把一些信息也封装起来添加到没有NALU的包的前面,便于传输和解码播放,这个就引入了fu_indicator和fu_head。

简而言之;fu_indicator:就是NALU被分了子包之后的一个替代
具体细节可参考博客
https://www.cnblogs.com/yjg2014/p/6144977.html
里面讲述了fu_indicator其实就是NALU的一种、type为FU分片 28的NALU
截取一段我们关心的在发送分包的时候要怎么处理的,在代码发送分包发送数据中就要用到。
在这里插入图片描述

2、RTSP代码实战

还是在mmp/sample/venc测试中进行修改,修改为通过RTSP传输

视频编解码那边基本不需要动,只是在开头添加一下对RTSP初始化,然后再之前保留到文件或者ORTP传输的地方修改为通过RTSP传输方式即完成。

2.2、流程分析

同样分析代码函数调用框架

main	RtspServer_init//初始化、创建两个线程,一个用于监听一个用于发送		RtspServerListen   用于服务器监听 单服务器多客户端架构			while(accept)   //while阻塞等待在accept等待连接这里				RtspClientMsg   //有客户连接则又开一个线程来处理与客户端的通信					(主要处理控制关键字信息的相互通信)					客户端发送OPTIONS:调用OptionAnswer 准备自己支持的关键字信息并send					客户端发送DESCRIBE:调用DescribeAnswer  准备sdp文件内容并send					客户端发送SETUP:调用SetupAnswer  记录客户端的信息进入就绪状态并应答					客户端发送PLAY:调用PlayAnswer    应答并为数据发送建立udpfd的链接					客户端发送TEARDOWN:调用TeardownAnswer 应答并关闭udpfd以及连接的socket		vdRTPSendThread //环状发送就使用这里,在编码完成后不发送而是存到循环队列中			while(1)//就在里面判断链表是否为空,取第一个,VENC_Sent发送,删除掉发送的节点				list_empty				get_first_item				VENC_Sent				list_del	SAMPLE_VENC_720P_CLASSIC  //如果直接发送则在编码完成后就进行发送		SAMPLE_COMM_VENC_StartGetStream //只有第六步存储这里有变化			SAMPLE_COMM_VENC_Sentjin  //调用则表示直接发送				VENC_Sent 就存在拆包的情况			saveStream  //调用存储则表示走环状发送,真正发送在vdRTPSendThread				INIT_LIST_HEAD				list_add_tail //将编码好的数据存储到双向链表里面,发送直接从链表中取即可

在这里插入图片描述

2.3、细节分析

1、在RtspServerListen函数

有g_rtspClients 存放rtsp客户端信息的数组的全局变量,这样才能保证单服务器多客户端的流程,都是在轮询那个客户端条件许可了才进入下一步与客户端进行通信的。

2、RtspClientMsg 重点是对客户端发过来的请求进行解析以及发送response

pthread_detach(pthread_self());设置进程自分离
创建线程的时候把该客户端的基本信息都传递到该线程来了pParam
recv 非阻塞读取信息
ParseRequestString 这里请求的数据解析出来的就是请求格式 ;方法 URL 消息头 消息体
PlayAnswer 注意在应答客户端之后就直接创建了一个udpfd的并connect,udp协议只需连接就可以进行通信了,在send的时候就可以直接使用udp发送了。
注意在play的时候就把g_rtspClients[pClient->index].status = RTSP_SENDING;把该客户端的状态进行了一下设置。

3、VENC_Sent拆包发送

if(nAvFrmLen<=nalu_sent_len)//要发送的信息不足一包{
则发送的格式就是 RTP包头 + NALU + 数据信息 //RTP 报头格式 rtp_hdr->marker=1; rtp_hdr->seq_no = htons(g_rtspClients[is].seqnum++); nalu_hdr =(NALU_HEADER*)&sendbuf[12]; nalu_hdr->F=0; //第一个都为0 nalu_hdr->NRI= nIsIFrm; //重要性级别 nalu_hdr->TYPE= nNaluType;//该帧的类型 0没定义 //RTP头12字节,NALU 1个字节 数据从13开始 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)//整包{
int k=0,l=0; k=nAvFrmLen/nalu_sent_len; l=nAvFrmLen%nalu_sent_len; //拆包根据将NALU拆成两个字节 FU indicator、FU header while(t<=k) {
if(t==0)//整包的第一包 {
//多一个字节了 rtp_hdr->marker=0; fu_ind =(FU_INDICATOR*)&sendbuf[12]; fu_ind->F= 0; fu_ind->NRI= nIsIFrm; fu_ind->TYPE=28; //就表示FU类型的NALU fu_hdr =(FU_HEADER*)&sendbuf[13]; fu_hdr->E=0; fu_hdr->R=0; fu_hdr->S=1;//表示第1帧 fu_hdr->TYPE=nNaluType; nalu_payload=&sendbuf[14]; memcpy(nalu_payload,buffer,nalu_sent_len); bytes=nalu_sent_len+14; sendto( udpfd, sendbuf, bytes, 0, (struct sockaddr *)&server,sizeof(server));}else if(k==t)//整包拆包后的最后一包 {
最后一包与第一包的区别就是fu_head的s = 0 e = 1; } else if(t

4、直接发送或环状发送

直接发送;编码编出来一帧就发送一帧,系统没有缓冲,编码时速和网络传输的速度是可能不匹配的,直接发送可以在编码完成之后直接发送而存储到其他地方开线程专门发送了
环状buffer;就是在直接发送的基础上增加缓冲,就是增加一个队列(双向链表)队列需要生产者和消费者两个指针操作,一个放,一个取。主要是缓解网上变化的问题,有时候网上比编码块,有时候比编码慢的情况使用buff缓冲是个很好的方案。
如果不够存,要么增大buf,要么就是系统设计有问题

转载地址:https://blog.csdn.net/zw1996/article/details/114366204 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:FFmepg-1、VS和QT环境搭建
下一篇:C++面向对象思维刷题

发表评论

最新留言

网站不错 人气很旺了 加油
[***.192.178.218]2024年04月14日 12时10分41秒