我们在编写代码前的设想是让开发板作为服务器,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,但是加入了rtsp的内容
我们将sample修改为只用一路,码率控制模式改为固定fixQP
其他内容没有什么变动,具体参考专栏海思3518E开发笔记中2开头的内容
修改的部分是第六步,从venc编码通道(vb)中获取视频流并保存,这里改为获取视频流后不保存,直接放到rtsp通道中传输
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
将准备好的包发出去
这里主要就是网络编程中配置端口号,修改网络字节序的内容
端口号使用的是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
函数
这个函数的作用就是解析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字段
以上就是作为服务器的开发板对客户端发来的请求信息的解析,解析完毕后做相应的处理
以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规则
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;
}
传进来的参数
函数中,先获取了服务器本地的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");
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的设计
因为考虑实时,所以用的是UDP
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
发送
发送完一个节点,就把这个节点拿掉,并且把内存释放