海思3518E开发笔记6.1——RTSP实时图传源码分析

目录

  • 源码框架分析
    • RtspServer_init
    • SAMPLE_VENC_720P_CLASSIC
  • 详细分析
  • RtspServer_init
    • RtspServerListen
      • ParseRequestString
      • OPTIONS
      • DESCRIBE
      • PLAY
    • vdRTPSendThread

源码框架分析

海思3518E开发笔记6.1——RTSP实时图传源码分析_第1张图片
主函数中只有两部分,初始化rtsp服务和视频编码程序

RtspServer_init

我们在编写代码前的设想是让开发板作为服务器,windows作为客户端。于是服务器一定是先运行,然后像socket那样初始化完成后阻塞监听,等待客户端连接。

S(server)C(client)模式,基本都是这样的结构。

初始化部分代码如下

void RtspServer_init(void)
{
	int i;
	pthread_t threadId = 0;

	memset(&g_rtp_playload,0,sizeof(g_rtp_playload));
	strcpy(&g_rtp_playload,"G726-32");
	g_audio_rate = 8000;
	pthread_mutex_init(&g_sendmutex,NULL);
	pthread_mutex_init(&g_mutex,NULL);
	pthread_cond_init(&g_cond,NULL);
	memset(&g_rtspClients,0,sizeof(RTSP_CLIENT)*MAX_RTSP_CLIENT);
	
	//pthread_create(&g_SendDataThreadId, NULL, SendDataThread, NULL);
	
	struct sched_param thdsched;
	thdsched.sched_priority = 2;
	//to listen visiting
	pthread_create(&threadId, NULL, RtspServerListen, NULL);
	//pthread_setschedparam(threadId,SCHED_RR,&thdsched);
	printf("RTSP:-----Init Rtsp server\n");

	pthread_create(&gs_RtpPid, 0, vdRTPSendThread, NULL);
	
	//exitok++;
}

一上来先申请一个线程,盲猜就是用来监听

接着为payload的存放申请空间,以RTP开头,是应为rtsp与rtp之间有联系,rtsp实际上是以rtp包来发送的,所以以rtp开头
g_rtp_playload是一个20个字节的数组,后面将G726-32放入数组,这表示是一种音频标准的payload

下面的g_audio_rate = 8000;就是音频的采样率,后续没有用到,如果做音频相关的东西就可以进行使用

接着初始化全局的mutex和cond

再为g_rtspClients申请空间,用于存放连接的客户端的信息,数据结构如下

typedef struct
{
	int index;
	int socket;
	int reqchn;
	int seqnum;
	int seqnum2;
	unsigned int tsvid;
	unsigned int tsaud;
	int status;
	int sessionid;
	int rtpport[2];
	int rtcpport;
	char IP[20];
	char urlPre[PARAM_STRING_MAX];
}RTSP_CLIENT;

接着配置sched_param结构体,看起来是和线程调度的优先级有关。如果线程比较多,对CPU资源进行争抢的时候,就需要设置,可以调用系统API进行线程的调度。
这里作为预留,实际代码中没有必要使用
pthread_setschedparam(threadId,SCHED_RR,&thdsched);注释掉了

RtspServer_init中真正被调用的函数是RtspServerListen,开启了一个线程进行监听。socket的转网络字节序、servaddr的配置、bind、listen都放在了这个线程里面。

vdRTPSendThread用于发送,也开启了一个线程进行

SAMPLE_VENC_720P_CLASSIC

这个函数就是标准的海思sample,但是加入了rtsp的内容

我们将sample修改为只用一路,码率控制模式改为固定fixQP

其他内容没有什么变动,具体参考专栏海思3518E开发笔记中2开头的内容

修改的部分是第六步,从venc编码通道(vb)中获取视频流并保存,这里改为获取视频流后不保存,直接放到rtsp通道中传输

于是在原来保存264码流的地方,换成通过rtsp发送出去
海思3518E开发笔记6.1——RTSP实时图传源码分析_第2张图片

HI_S32 SAMPLE_COMM_VENC_Sentjin(VENC_STREAM_S *pstStream)
{
    HI_S32 i,flag=0;

    for(i=0;i<MAX_RTSP_CLIENT;i++)//have atleast a connect
    {
		if(g_rtspClients[i].status == RTSP_SENDING)
		{
		    flag = 1;
		    break;
		}
    }
    if(flag)
    {
	    for (i = 0; i < pstStream->u32PackCount; i++)
	    {
		HI_S32 lens=0,j,lastadd=0,newadd=0,showflap=0;
		char sendbuf[320*1024];
		//char tmp[640*1024];
		lens = pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;
		memcpy(&sendbuf[0],pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset,lens);
		//printf("lens = %d, count= %d\n",lens,count++);
		VENC_Sent(sendbuf,lens);		
		lens = 0;
	    }

	
    }
    return HI_SUCCESS;
}

实际应用场景中,可能有很多路的rtsp流,这里我们宏定义只用一路

下面对rtsp的状态进行判断,如果处于发送状态就进行发送

然后调用VENC_Sent将准备好的包发出去

详细分析

RtspServer_init

RtspServerListen

这里主要就是网络编程中配置端口号,修改网络字节序的内容
端口号使用的是554,这是RTSP的默认端口号

setsockopt来设置socket属性,设置了SO_REUSEADDR可以用来复用

然后就是bind和listen

接下来到accept
如果没人连接,就阻塞到while的判断中,有人连接就进入while
并且将连接过来的ip地址打印出来

接着再进行socket的设置,选项是SO_SNDBUF,即设置发送缓冲区的大小

然后对新连接的rtspClient的status进行判断,如果等于RTSP_IDLE就进行处理。由于g_rtspClients是全局变量,定义之后自动初始化为0,就是status枚举里的第一个。
这样处理,只要是新的ip地址,就一定是空闲状态,进if后,就把他的状态换成connected,避免反复操作

进入if之后,将接入的客户端的属性进行一一填充。

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++;
strcpy(g_rtspClients[i].IP,inet_ntoa(addrAccept.sin_addr));

填充完属性后建立一个线程,进行当前接入客户端的处理。

如果超过最大限制,那么就将超过的覆盖到原来的0,这样循环覆盖下去(设计方面的思路,也可以超过直接不再连接)


处理的函数是RtspClientMsg,只服务当前接入的客户端
在看这一段代码的时候,就需要对RTSP传输协议有一定的了解,函数中ParseRequestString对RTSP消息头进行解析

RtspClientMsg函数一进去,做了一个RTSP_CLIENT * pClient来接受传进来的结构体,不直接用是避免修改原来结构体里的内容
当连接进来的客户端的状态不是架空的时候,就进行数据的receive,一次只读RTSP_RECV_SIZE这么多,没有读完的部分留到下一次继续读,如果读出来的字节数小于0,那么是错误的,把状态重新改为置空

接下来定义了一系列的局部变量,作为ParseRequestString的输出参数。
该函数原型如下

int ParseRequestString(char const* reqStr,
		       unsigned reqStrSize,
		       char* resultCmdName,
		       unsigned resultCmdNameMaxSize,
		       char* resultURLPreSuffix,
		       unsigned resultURLPreSuffixMaxSize,
		       char* resultURLSuffix,
		       unsigned resultURLSuffixMaxSize,
		       char* resultCSeq,
		       unsigned resultCSeqMaxSize) 

第一个参数为前面socket接收到的字符串,就是进行分析的目标
第二个参数是接受到字符串的大小
第三个参数用来存放解析出来的RTSP method
第四个参数是函数执行前申请存放第三个参数的数组的大小
第五个参数是解析出来的URL前缀
第六个参数是用来存放第五个参数的数组大小
第七个参数是解析出来的URL后缀
第八个参数是存放第七个参数的数组大小
第九个参数是解析出来的会话序列号
第十个参数是存放第九个参数的数组大小

以上解析出来的参数都和RTSP会话内容对应,详细参考我之前写的RTSP协议详解

后面的内容就是对解析出来的RTSP methods进行对应操作

接下来仔细分析ParseRequestString函数

ParseRequestString

这个函数的作用就是解析RTSP协议中CS交互信息,多为一些字符串操作

第一段

// Read everything up to the first space as the command name:
 int parseSucceeded = FALSE;
 unsigned i;
 for (i = 0; i < resultCmdNameMaxSize-1 && i < reqStrSize; ++i) 
 {
   char c = reqStr[i];
   if (c == ' ' || c == '\t') 
   {
     parseSucceeded = TRUE;
     break;
   }

   resultCmdName[i] = c;
 }
 resultCmdName[i] = '\0';
 if (!parseSucceeded) return FALSE;

首先做了一个flag,来表示是否成功分析
接着对获取的数据进行分段,标志是空格或制表符
这个循环就获取了RTSP 的method
由于解析出来的是字符串,就需要将最后一个字符替换为字符串的结束标志

第二段

// Skip over the prefix of any "rtsp://" or "rtsp:/" URL that follows:
  unsigned j = i+1;
  while (j < reqStrSize && (reqStr[j] == ' ' || reqStr[j] == '\t')) ++j; // skip over any additional white space
  for (j = i+1; j < reqStrSize-8; ++j) {
    if ((reqStr[j] == 'r' || reqStr[j] == 'R')
	&& (reqStr[j+1] == 't' || reqStr[j+1] == 'T')
	&& (reqStr[j+2] == 's' || reqStr[j+2] == 'S')
	&& (reqStr[j+3] == 'p' || reqStr[j+3] == 'P')
	&& reqStr[j+4] == ':' && reqStr[j+5] == '/') {
      j += 6;
      if (reqStr[j] == '/') {
	// This is a "rtsp://" URL; skip over the host:port part that follows:
	++j;
	while (j < reqStrSize && reqStr[j] != '/' && reqStr[j] != ' ') ++j;
      } else {
	// This is a "rtsp:/" URL; back up to the "/":
	--j;
      }
      i = j;
      break;
    }
  }

 // Look for the URL suffix (before the following "RTSP/"):
  parseSucceeded = FALSE;
  unsigned k;
  for (k = i+1; k < reqStrSize-5; ++k) {
    if (reqStr[k] == 'R' && reqStr[k+1] == 'T' &&
	reqStr[k+2] == 'S' && reqStr[k+3] == 'P' && reqStr[k+4] == '/') {
      while (--k >= i && reqStr[k] == ' ') {} // go back over all spaces before "RTSP/"
      unsigned k1 = k;
      while (k1 > i && reqStr[k1] != '/' && reqStr[k1] != ' ') --k1;
      // the URL suffix comes from [k1+1,k]

      // Copy "resultURLSuffix":
      if (k - k1 + 1 > resultURLSuffixMaxSize) return FALSE; // there's no room
      unsigned n = 0, k2 = k1+1;
      while (k2 <= k) resultURLSuffix[n++] = reqStr[k2++];
      resultURLSuffix[n] = '\0';

      // Also look for the URL 'pre-suffix' before this:
      unsigned k3 = --k1;
      while (k3 > i && reqStr[k3] != '/' && reqStr[k3] != ' ') --k3;
      // the URL pre-suffix comes from [k3+1,k1]

      // Copy "resultURLPreSuffix":
      if (k1 - k3 + 1 > resultURLPreSuffixMaxSize) return FALSE; // there's no room
      n = 0; k2 = k3+1;
      while (k2 <= k1) resultURLPreSuffix[n++] = reqStr[k2++];
      resultURLPreSuffix[n] = '\0';

      i = k + 7; // to go past " RTSP/"
      parseSucceeded = TRUE;
      break;
    }
  }
  if (!parseSucceeded) return FALSE;

首先用j获取第二段关键信息,就是用上次的最后地址i再往后偏移一个地址
然后兼容大小写,作用是跳过rtsp://或rtsp:/
然后找到/后面的不是/或者空格的那一个地址,就是解析出来的URL的首地址

第三段

  // Look for "CSeq:", skip whitespace,
  // then read everything up to the next \r or \n as 'CSeq':
  parseSucceeded = FALSE;
  for (j = i; j < reqStrSize-5; ++j) {
    if (reqStr[j] == 'C' && reqStr[j+1] == 'S' && reqStr[j+2] == 'e' &&
	reqStr[j+3] == 'q' && reqStr[j+4] == ':') {
      j += 5;
      unsigned n;
      while (j < reqStrSize && (reqStr[j] ==  ' ' || reqStr[j] == '\t')) ++j;
      for (n = 0; n < resultCSeqMaxSize-1 && j < reqStrSize; ++n,++j) {
	char c = reqStr[j];
	if (c == '\r' || c == '\n') {
	  parseSucceeded = TRUE;
	  break;
	}

	resultCSeq[n] = c;
      }
      resultCSeq[n] = '\0';
      break;
    }
  }
  if (!parseSucceeded) return FALSE;

用于解析出CSeq字段

以上就是作为服务器的开发板对客户端发来的请求信息的解析,解析完毕后做相应的处理

OPTIONS

以OptionAnswer为例

int OptionAnswer(char *cseq, int sock)
{
	if (sock != 0)
	{
		char buf[1024];
		memset(buf,0,1024);
		char *pTemp = buf;
		pTemp += sprintf(pTemp,"RTSP/1.0 200 OK\r\nCSeq: %s\r\n%sPublic: %s\r\n\r\n",
			cseq,dateHeader(),"OPTIONS,DESCRIBE,SETUP,PLAY,PAUSE,TEARDOWN");
	
		int reg = send(sock, buf,strlen(buf),0);
		if(reg <= 0)
		{
			return FALSE;
		}
		else
		{
			printf(">>>>>%s\n",buf);
		}
		return TRUE;
	}
	return FALSE;
}

服务端收到客户端的请求后,向客户端返回信息
返回的信息放在buf中,通过socket通道发送
返回的信息按照RTSP规则

DESCRIBE

int DescribeAnswer(char *cseq,int sock,char * urlSuffix,char* recvbuf)
{
	if (sock != 0)
	{
		char sdpMsg[1024];
		char buf[2048];
		memset(buf,0,2048);
		memset(sdpMsg,0,1024);
		char*localip;
		localip = GetLocalIP(sock);
		
		char *pTemp = buf;
		pTemp += sprintf(pTemp,"RTSP/1.0 200 OK\r\nCSeq: %s\r\n",cseq);
		pTemp += sprintf(pTemp,"%s",dateHeader());
		pTemp += sprintf(pTemp,"Content-Type: application/sdp\r\n");
		

		char *pTemp2 = sdpMsg;
		pTemp2 += sprintf(pTemp2,"v=0\r\n");
		pTemp2 += sprintf(pTemp2,"o=StreamingServer 3331435948 1116907222000 IN IP4 %s\r\n",localip);
		pTemp2 += sprintf(pTemp2,"s=H.264\r\n");
		pTemp2 += sprintf(pTemp2,"c=IN IP4 0.0.0.0\r\n");
		pTemp2 += sprintf(pTemp2,"t=0 0\r\n");
		pTemp2 += sprintf(pTemp2,"a=control:*\r\n");
		

		/*H264 TrackID=0 RTP_PT 96*/
		pTemp2 += sprintf(pTemp2,"m=video 0 RTP/AVP 96\r\n");
		pTemp2 += sprintf(pTemp2,"a=control:trackID=0\r\n");
		pTemp2 += sprintf(pTemp2,"a=rtpmap:96 H264/90000\r\n");
		pTemp2 += sprintf(pTemp2,"a=fmtp:96 packetization-mode=1; sprop-parameter-sets=%s\r\n", "AAABBCCC");
#if 1
		
		/*G726*/
		
		pTemp2 += sprintf(pTemp2,"m=audio 0 RTP/AVP 97\r\n");
		pTemp2 += sprintf(pTemp2,"a=control:trackID=1\r\n");
		if(strcmp(g_rtp_playload,"AAC")==0)
		{
			pTemp2 += sprintf(pTemp2,"a=rtpmap:97 MPEG4-GENERIC/%d/2\r\n",16000);
			pTemp2 += sprintf(pTemp2,"a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1410\r\n");
		}
		else
		{
			pTemp2 += sprintf(pTemp2,"a=rtpmap:97 G726-32/%d/1\r\n",8000);
			pTemp2 += sprintf(pTemp2,"a=fmtp:97 packetization-mode=1\r\n");
		}	
#endif
		pTemp += sprintf(pTemp,"Content-length: %d\r\n", strlen(sdpMsg));     
		pTemp += sprintf(pTemp,"Content-Base: rtsp://%s/%s/\r\n\r\n",localip,urlSuffix);
		
		//printf("mem ready\n");
		strcat(pTemp, sdpMsg);
		free(localip);
		//printf("Describe ready sent\n");
		int re = send(sock, buf, strlen(buf),0);
		if(re <= 0)
		{
			return FALSE;
		}
		else
		{
			printf(">>>>>%s\n",buf);
		}
	}

	return TRUE;
}

传进来的参数

  • cseq——会话的序列号
  • sock——socket建立的网络通道
  • urlSuffix——解析出来的URL字符串
  • recvbuf——服务器接收到的整个信息

函数中,先获取了服务器本地的IP地址
接下来,就按照RTSP的DESCRIBE标准进行回复

RTP协议中,使用VLC进行播放,需要有一个SDP文件进行解析才能播放,RTP传的是裸流,没有控制信息,需要本地配置
而RTSP协议中,SDP包含在交互信息中,不需要用VLC打开SDP文件,通过解析交互信息,客户端就能够知道怎么去解析视频流,代码中的体现如下

char *pTemp2 = sdpMsg;
pTemp2 += sprintf(pTemp2,"v=0\r\n");
pTemp2 += sprintf(pTemp2,"o=StreamingServer 3331435948 1116907222000 IN IP4 %s\r\n",localip);
pTemp2 += sprintf(pTemp2,"s=H.264\r\n");
pTemp2 += sprintf(pTemp2,"c=IN IP4 0.0.0.0\r\n");
pTemp2 += sprintf(pTemp2,"t=0 0\r\n");
pTemp2 += sprintf(pTemp2,"a=control:*\r\n");

PLAY

int PlayAnswer(char *cseq, int sock,int SessionId,char* urlPre,char* recvbuf)
{
	if (sock != 0)
	{
		char buf[1024];
		memset(buf,0,1024);
		char *pTemp = buf;
		char*localip;
		localip = GetLocalIP(sock);
		pTemp += sprintf(pTemp,"RTSP/1.0 200 OK\r\nCSeq: %s\r\n%sRange: npt=0.000-\r\nSession: %d\r\nRTP-Info: url=rtsp://%s/%s;seq=0\r\n\r\n",
			cseq,dateHeader(),SessionId,localip,urlPre);

		free(localip);
		
		int reg = send(sock, buf,strlen(buf),0);
		if(reg <= 0)
		{
			return FALSE;
		}
		else
		{
			printf(">>>>>%s",buf);
			udpfd = socket(AF_INET,SOCK_DGRAM,0);//UDP
			struct sockaddr_in server;
			server.sin_family=AF_INET;
		   	server.sin_port=htons(g_rtspClients[0].rtpport[0]);          
		   	server.sin_addr.s_addr=inet_addr(g_rtspClients[0].IP);
			connect(udpfd,(struct sockaddr *)&server,sizeof(server));
    		printf("udp up\n");
		}
		return TRUE;
	}
	return FALSE;
}

客户端play请求后,服务端开启一个UDP的SOCKET通道进行裸流的传输,命令走的是TCP通道。这就是RTSP的设计
海思3518E开发笔记6.1——RTSP实时图传源码分析_第3张图片
因为考虑实时,所以用的是UDP

vdRTPSendThread

HI_VOID* vdRTPSendThread(HI_VOID *p)
{
	while(1)
	{
		if(!list_empty(&RTPbuf_head))
		{
			
			RTPbuf_s *p = get_first_item(&RTPbuf_head,RTPbuf_s,list);
			VENC_Sent(p->buf,p->len);
			list_del(&(p->list));
			free(p->buf);
			free(p);
			p = NULL;
			count--;
			//printf("count = %d\n",count);
		
		}
		usleep(5000);
	}
}

在client连接后,server收到play指令,创建裸流的UDP通道

发送是在vdRTPSendThread线程中进行。每隔5微妙在RTPbuf_head链表里查找数据,有数据就进行发送

get_first_item用来取出链表里的第一个非空节点
然后将节点中的内容通过VENC_Sent发送出去,函数的内容主要是为了H264封RTP包添加包头
发送完就将它释放掉
这里的send是消费者,不断发送链表中的数据

有消费者就有生产者,生产在编码的地方

发送有两种方法,一种直接发送,一种是环形buffer发送

直接发送的方法是编码一帧,就发一帧。直接发送的方法不是在vdRTPSendThread线程中,而是在编码的地方发送,编完一帧就发一帧

HI_S32 SAMPLE_COMM_VENC_Sentjin(VENC_STREAM_S *pstStream)
{
    HI_S32 i,flag=0;

    for(i=0;i<MAX_RTSP_CLIENT;i++)//have atleast a connect
    {
		if(g_rtspClients[i].status == RTSP_SENDING)
		{
		    flag = 1;
		    break;
		}
    }
    if(flag)
    {
	    for (i = 0; i < pstStream->u32PackCount; i++)
	    {
		HI_S32 lens=0,j,lastadd=0,newadd=0,showflap=0;
		char sendbuf[320*1024];
		//char tmp[640*1024];
		lens = pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;
		memcpy(&sendbuf[0],pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset,lens);
		//printf("lens = %d, count= %d\n",lens,count++);
		VENC_Sent(sendbuf,lens);		
		lens = 0;
	    }

	
    }
    return HI_SUCCESS;
}

直接发送的方式优势是简单,但实际应用中不会这么去做。实际情况中会比较复杂,编码和传输的速度不一定适配。
如果发送的速度比编码的速度块,那么发送就要等编码。时间上会有冗余

环形buffer就可以有一定的缓冲。如果发送比编码快,那么就阻塞,就像视频缓冲;如果编码比发送快,甚至套了发送一圈了,那么可以增大环形buffer,但是早晚会有崩溃的一天,编码快得多就要考虑系统的设计。
实际谁快谁慢是不确定的,比如网络波动会影响发送的速度。有时候发送慢了,那编码的帧排列准备;有时候发送快了,那么在排队的编码完的帧等待的数量少一点。

环形buffer发送的代码如下

HI_S32 saveStream(VENC_STREAM_S *pstStream)
{
    HI_S32 i,j,lens=0;

    for(j=0;j<MAX_RTSP_CLIENT;j++)//have atleast a connect
    {
		if(g_rtspClients[j].status == RTSP_SENDING)
		{
		    for (i = 0; i < pstStream->u32PackCount; i++)
		    {
				RTPbuf_s *p = (RTPbuf_s *)malloc(sizeof(RTPbuf_s));
				INIT_LIST_HEAD(&(p->list));

				lens = pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset;
				p->buf = (char *)malloc(lens);
				p->len = lens;
				memcpy(p->buf,pstStream->pstPack[i].pu8Addr+pstStream->pstPack[i].u32Offset,lens);

				list_add_tail(&(p->list),&RTPbuf_head);
				count++;
				//printf("count = %d\n",count);
		    }
    	}
    }

    return HI_SUCCESS;
}

环形buffer是通过链表实现
count是用来记录剩余编码完的帧的数量,发送的话就去减减,到0说明编码完的帧用完了。
这里就是上面说的生产者,把编码完的帧送到全局的链表中,由vdRTPSendThread发送
发送完一个节点,就把这个节点拿掉,并且把内存释放

你可能感兴趣的:(海思Hi3518E开发笔记)