经过前两篇文章的学习,大家对H.264视频编解码标准以及RTSP协议都有了一定的了解。接下来,在这篇文章我们结合前面学到的知识以及海思平台来实现视频流传输功能。
(1)获取源码
链接:https://pan.baidu.com/s/1nS2EWkQeq5OYNaThGQZDZg
提取码:vhbx
--来自百度网盘超级会员V5的分享
(1)从venc/sample_venc.c中的main主函数文件开始分析(前边我们也分析过海思的官方例程以及添加ortp库后的代码,现在我们来分析使用rtsp协议进行传输的代码。重点关注与以前代码的不同之处,添加了那些东西)
int main(int argc, char *argv[])
{
HI_S32 s32Ret;
MPP_VERSION_S mppVersion;
HI_MPI_SYS_GetVersion(&mppVersion);
printf("MPP Ver %s\n",mppVersion.aVersion);
RtspServer_init();
s32Ret = SAMPLE_VENC_720P_CLASSIC();
return HI_FAILURE;
}
主要函数调用框架:
main
RtspServer_init();//构建一个rtsp服务器
SAMPLE_VENC_720P_CLASSIC();
这份源码最终实现的效果是:在开发板构建一个服务器,使vlc播放器作为一个客户端进行视频流的接收与解析。
(2)RTP、RTCP、RTSP的关系:
通常说的RTSP包括RTSP协议、RTP协议、RTCP协议。对于这些协议的作用简单的理解如下:
RTSP协议:负责服务器与客户端之间的请求与响应
RTP协议:负责传输媒体数据
RTCP协议:在RTP传输过程中提供传输信息
rtsp承载与rtp和rtcp之上,rtsp并不会发送媒体数据,而是使用rtp协议传输
rtp并没有规定发送方式,可以选择udp发送或者tcp发送
在这里我给大家提供一个有关RTSP协议的文档,便于大家学习,建议大家学习了上篇博客中我给的 RTSP学习的链接后,再大致浏览下下面这个文档,有助于大家理解上边我提供的海思代码。
链接:https://pan.baidu.com/s/14JGYgZqgoK68m0JZpOtm7Q
提取码:3oes
--来自百度网盘超级会员V5的分享
我给大家列一下我分析后的函数调用关系,大家再浏览我提供的代码时,可以跟着去分析:
main(int argc, char *argv[]) (sample_venc.c 1168行附近)
RtspServer_init();
pthread_create(&threadId, NULL, RtspServerListen, NULL);//创建一个监听线程,监听客户端请求
RtspServerListen()
pthread_create(&threadIdlsn, NULL, RtspClientMsg, &g_rtspClients[i]);
pthread_create(&gs_RtpPid, 0, vdRTPSendThread, NULL);//创建一个使用RTP协议传输数据的线程,采用环状buffer的方式发送
vdRTPSendThread(HI_VOID *p)
VENC_Sent(p->buf,p->len);//rtsp分包发送h.264视频流
s32Ret = SAMPLE_VENC_720P_CLASSIC();//进行系统初始化,开始视频采集以及视频编解码
s32Ret = SAMPLE_COMM_VENC_StartGetStream(s32ChnNum);
pthread_create(&gs_VencPid, 0, SAMPLE_COMM_VENC_GetVencStreamProc, (HI_VOID*)&gs_stPara);
SAMPLE_COMM_VENC_GetVencStreamProc_Svc_t()
SAMPLE_COMM_VENC_Sentjin(&stStream);//这个就是直接发送,隐去该语句,打开下面那个函数saveStream
VENC_Sent(sendbuf,lens);//发送数据,这个是直接发送模式,等客户端连接好后就发送
saveStream(&stStream);//就会开启环状buffer发送,先将数据保存,而不是直接发送
void * RtspServerListen(void*pParam)//rtsp服务器的建立至开始监听
{
int s32Socket;
struct sockaddr_in servaddr;//存储服务器的IP地址的结构体
int s32CSocket;
int s32Rtn;
int s32Socket_opt_value = 1;
int nAddrLen;
struct sockaddr_in addrAccept;//存储IP地址的结构体
int bResult;
memset(&servaddr, 0, sizeof(servaddr));//清空后设置服务器的相关属性
servaddr.sin_family = AF_INET;//设置地址族为ipv4
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//设置ip地址
servaddr.sin_port = htons(RTSP_SERVER_PORT); //设置地址的端口号信息
//第一步:使用socket打开文件描述符,即创建套接字
s32Socket = socket(AF_INET, SOCK_STREAM, 0);//socket所传参数表明使用tcp协议进行传输
if (setsockopt(s32Socket ,SOL_SOCKET,SO_REUSEADDR,&s32Socket_opt_value,sizeof(int)) == -1)
{//setsockopt函数用于设置socket属性,该参数表明:设置调用close(socket)后,仍可继续重用该socket。
//调用close(socket)一般不会立即关闭socket,而经历TIME_WAIT的过程。
return (void *)(-1);
}
//第二步:bind绑定sockfd和当前开发板的ip地址以及端口号
s32Rtn = bind(s32Socket, (struct sockaddr *)&servaddr, sizeof(struct sockaddr_in));
if(s32Rtn < 0)
{
return (void *)(-2);
}
//第三步,使用listen监听端口
s32Rtn = listen(s32Socket, 50);//50:表示允许连接的客户端数
if(s32Rtn < 0)
{
return (void *)(-2);
}
nAddrLen = sizeof(struct sockaddr_in);
int nSessionId = 1000;
//第四步:accept阻塞等待客户端接入,返回连接connect_fd文件描述符
while ((s32CSocket = accept(s32Socket, (struct sockaddr*)&addrAccept, &nAddrLen)) >= 0)
{
printf("<<<, inet_ntoa(addrAccept.sin_addr));
//第五步:建立连接后就可以通信了
int nMaxBuf = 10 * 1024;
if(setsockopt(s32CSocket, SOL_SOCKET, SO_SNDBUF, (char*)&nMaxBuf, sizeof(nMaxBuf)) == -1)
printf("RTSP:!!!!!! Enalarge socket sending buffer error !!!!!!\n");
int i;
int bAdd=FALSE;
for(i=0;i<MAX_RTSP_CLIENT;i++)//检查是否超过最大可连接数
{
if(g_rtspClients[i].status == RTSP_IDLE)
{
memset(&g_rtspClients[i],0,sizeof(RTSP_CLIENT));
g_rtspClients[i].index = i;
g_rtspClients[i].socket = s32CSocket;
g_rtspClients[i].status = RTSP_CONNECTED ;//RTSP_SENDING;
g_rtspClients[i].sessionid = nSessionId++;//对话id
strcpy(g_rtspClients[i].IP,inet_ntoa(addrAccept.sin_addr));
pthread_t threadIdlsn = 0;
struct sched_param sched;
sched.sched_priority = 1;//与优先级相关的,可以去掉
//to return ACKecho,创建一个线程去处理客户端的请求
pthread_create(&threadIdlsn, NULL, RtspClientMsg, &g_rtspClients[i]);
bAdd = TRUE;
break;
}
}
if(bAdd==FALSE)//上边for循环中的各个循环量都不满足条件,则采用这个默认的响应
{ //在我们这里去掉也不影响
memset(&g_rtspClients[0],0,sizeof(RTSP_CLIENT));
g_rtspClients[0].index = 0;
g_rtspClients[0].socket = s32CSocket;
g_rtspClients[0].status = RTSP_CONNECTED ;//RTSP_SENDING;
g_rtspClients[0].sessionid = nSessionId++;
strcpy(g_rtspClients[0].IP,inet_ntoa(addrAccept.sin_addr));
pthread_t threadIdlsn = 0;
struct sched_param sched;
sched.sched_priority = 1;
//to return ACKecho
pthread_create(&threadIdlsn, NULL, RtspClientMsg, &g_rtspClients[0]);
bAdd = TRUE;
}
//if(exitok){ exitok++;return NULL; }
}
if(s32CSocket < 0)
{
// HI_OUT_Printf(0, "RTSP listening on port %d,accept err, %d\n", RTSP_SERVER_PORT, s32CSocket);
}
printf("----- INIT_RTSP_Listen() Exit !! \n");
return NULL;
}
该函数关键部分分析:
g_rtspClients[i]:描述客户端属性的一个结构体数组
RTSP_CLIENT g_rtspClients[MAX_RTSP_CLIENT];
typedef struct
{
int index;
int socket;//套接字
int reqchn;
int seqnum;
int seqnum2;
unsigned int tsvid;
unsigned int tsaud;
int status;//状态
int sessionid;//会话id
int rtpport[2];//rtp端口号
int rtcpport;//rtcp端口号
char IP[20];//IP地址
char urlPre[PARAM_STRING_MAX];
}RTSP_CLIENT;
pthread_create(&threadIdlsn, NULL, RtspClientMsg, &g_rtspClients[i]);
RtspClientMsg()
ParseRequestString()//解析读取到的客户端的请求
//下面的几个函数就是对客户端不同请求的处理
OptionAnswer(cseq,pClient->socket);
DescribeAnswer(cseq,pClient->socket,urlSuffix,p);
SetupAnswer(cseq,pClient->socket,pClient->sessionid,urlPreSuffix,p,&rtpport,&rtcpport);
PlayAnswer(cseq,pClient->socket,pClient->sessionid,g_rtspClients[pClient->index].urlPre,p);
PauseAnswer(cseq,pClient->socket,p);
TeardownAnswer(cseq,pClient->socket,pClient->sessionid,p);
在分析代码时,大家要注意到一个点,就是在建立连接时,使用的是TCP进行通信,而进行码流数据传输时使用的是UDP。原因在于TCP是面向连接的,需要进行三次握手,而实时视频流传输时注重时效性,UDP更为合适。在PlayAnswer()这个函数中实现了这一转变。
直接发送:发送是与编码有关的,编码编出来一帧数据就发送一帧数据。实现起来比较简单,但缺点在于与内部编码速度有关,通用性差(若编码速度发送速度不适配则需要一个缓冲空间),实际开发一般不采用这种方式。
环状buffer发送:类似于一种循环队列的思想,可将存放数据与读取数据理解为队首队尾两个指针,二者总会间隔一定的距离,具有一定的缓冲作用。
阅读参考:http://www.360doc.com/content/13/0121/10/2200926_261505993.shtml
必读:
rtp_timestamp:
https://blog.csdn.net/jasonhwang/article/details/7316128
fu_indicator:
https://www.cnblogs.com/yjg2014/p/6144977.html
RTP_FIXED_HEADER *rtp_hdr;
struct _RTP_FIXED_HEADER
{
/**//* byte 0 */
unsigned char csrc_len:4; /**//* expect 0 */
unsigned char extension:1; /**//* expect 1, see RTP_OP below */
unsigned char padding:1; /**//* expect 0 */
unsigned char version:2; /**//* expect 2 */
/**//* byte 1 */
unsigned char payload:7; /**//* RTP_PAYLOAD_RTSP */
unsigned char marker:1; /**//* expect 1 */
/**//* bytes 2, 3 */
unsigned short seq_no;
/**//* bytes 4-7 */
unsigned long timestamp;
/**//* bytes 8-11 */
unsigned long ssrc; /**//* stream number is used here. */
} __PACKED__;
typedef struct _RTP_FIXED_HEADER RTP_FIXED_HEADER;
NALU_HEADER *nalu_hdr;
struct _NALU_HEADER
{
//byte 0
unsigned char TYPE:5;
unsigned char NRI:2;
unsigned char F:1;
}__PACKED__; /**//* 1 BYTES */
typedef struct _NALU_HEADER NALU_HEADER;
SendTo是一个计算机函数,指向一指定目的地发送数据,SendTo()适用于发送未建立连接的UDP数据包 (参数为SOCK_DGRAM)。
Linux C函数 sendto(经socket传送数据)
相关函数
send , sendmsg,recv , recvfrom , socket
表头文件
#include < sys/types.h >
#include < sys/socket.h >
定义函数
int sendto ( socket s , const void * msg, int len, unsigned int flags, const struct sockaddr * to , int tolen ) ;
函数说明
sendto() 用来将数据由指定的socket传给对方主机。参数s为已建好连线的socket,如果利用UDP协议则不需经过连线操作。参数msg指向欲连线的数据内容,参数flags 一般设0,详细描述请参考send()。参数to用来指定欲传送的网络地址,结构sockaddr请参考bind()。参数tolen为sockaddr的结构长度。
返回值
成功则返回实际传送出去的字符数,失败返回-1,错误原因存于errno 中。
错误代码
EBADF 参数s非法的socket处理代码。
EFAULT 参数中有一指针指向无法存取的内存空间。
ENOTSOCK 参数 s为一文件描述词,非socket。
EINTR 被信号所中断。
EAGAIN 此动作会令进程阻断,但参数s的socket为不可阻断的。
ENOBUFS 系统的缓冲内存不足。
EINVAL 传给系统调用的参数不正确。
HI_S32 VENC_Sent(char *buffer,int buflen)
{
HI_S32 i;
int is=0;
int nChanNum=0;
for(is=0;is<MAX_RTSP_CLIENT;is++)
{
if(g_rtspClients[is].status!=RTSP_SENDING)
{
continue;
}
int heart = g_rtspClients[is].seqnum % 10000;//序列号
char* nalu_payload;
int nAvFrmLen = 0;//AV表示其可用于音频也可用于视频
int nIsIFrm = 0;
int nNaluType = 0;
char sendbuf[500*1024+32];
nAvFrmLen = buflen;//用于音频还是用于视频,取决于buf的长度
struct sockaddr_in server;
server.sin_family=AF_INET;
server.sin_port=htons(g_rtspClients[is].rtpport[0]);
server.sin_addr.s_addr=inet_addr(g_rtspClients[is].IP);
int bytes=0;
unsigned int timestamp_increse=0;//使用时钟频率计算而来表示时间的,表示每帧的时间
/*
由于一个帧(如I帧)可能被分成多个RTP包,所以多个相同帧的RTP timestamp相等。
(可以通过每帧最后一个RTP的marker标志区别帧,但最可靠的方法是查看相同
RTP timestamp包为同一帧。)
两帧之间RTP timestamp的增量 = 时钟频率 / 帧率
其中时钟频率可从SDP中获取(在服务器对客户端的describe请求的处理中有描述),如:
m=video 2834 RTP/AVP 96
a=rtpmap:96 H264/90000
其时钟频率为90000(通常视频的时钟频率),若视频帧率为25fps,则相邻帧间
RTP timestamp增量值 = 90000/25 = 3600。
另外,通常音频的时钟频率一般为8000。
*/
timestamp_increse=(unsigned int)(90000.0 / 25);
rtp_hdr =(RTP_FIXED_HEADER*)&sendbuf[0];//rtp_hdr表示rtp头信息,这里将其与sendbuf建立连接
//开始填充RTP包的头信息,所填充的内容都将存放到sendbuf中
rtp_hdr->payload = RTP_H264;
rtp_hdr->version = 2;//可通过查阅我们上边提供的rtsp的pdf文档
rtp_hdr->marker = 0;
rtp_hdr->ssrc = htonl(10);
if(nAvFrmLen<=nalu_sent_len)//每包数据的大小,我们定义nalu_sent_len 1400
{
rtp_hdr->marker=1;//信源标记
rtp_hdr->seq_no = htons(g_rtspClients[is].seqnum++); //序列号
nalu_hdr =(NALU_HEADER*)&sendbuf[12]; //与上边对rtp_hdr的操作相同
nalu_hdr->F=0;
nalu_hdr->NRI= nIsIFrm;
nalu_hdr->TYPE= nNaluType;
nalu_payload=&sendbuf[13];//与上边对rtp_hdr的操作相同
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)//若一个nalu(即一帧数据)过大,则采用分包发送
{
int k=0,l=0;
k=nAvFrmLen/nalu_sent_len;//几个整包
l=nAvFrmLen%nalu_sent_len;//最后一包余留的长度
int t=0;
g_rtspClients[is].tsvid = g_rtspClients[is].tsvid+timestamp_increse;
rtp_hdr->timestamp=htonl(g_rtspClients[is].tsvid);//转换成网络字节序
while(t<=k)
{
rtp_hdr->seq_no = htons(g_rtspClients[is].seqnum++);
//之所以要分成以下三种情况,是因为nalu头填充的数据有区别
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_hdr =(FU_HEADER*)&sendbuf[13];
fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->S=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));
t++;
}
else if(k==t)
{
rtp_hdr->marker=1;//最后一包数据的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;
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(t<k && 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_hdr =(FU_HEADER*)&sendbuf[13];
//fu_hdr->E=0;
fu_hdr->R=0;
fu_hdr->S=0;
fu_hdr->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++;
}
}
}
}
}
时隔许久,小嵌终于把这章节的第三篇文章补上了,要想学懂其中的内容,需要大家了解Linux C网络编程,了解TCP/UDP、RTSP等协议,了解多线程操作的一些知识,了解和h.264编码原理等等,需要大家花时间去学习,好事多磨吧!
注:本文章参考了《朱老师物联网大讲堂笔记》,并结合了自己的实际开发经历以及网上他人的技术文章,综合整理得到。如有侵权,联系删除!水平有限,欢迎各位在评论区交流。