上一部分说了Socket通讯的一些东西,这部分就结合代码来说说RTSP交互的过程。
在放实现代码之前先说说概念上的东西吧,关于RTSP这个流媒体网络协议。RTSP作为一个应用层协议,提供了一个可供扩展的框架,使得流媒体的受控和点播变得可能,它主要用来控制具有实时特性的数据的发送,但其本身并不用于传送流媒体数据,而必须依赖下层传输协议(如RTP/RTCP)所提供的服务来完成流媒体数据的传送。RTSP负责定义具体的控制信息、操作方法、状态码,以及描述与RTP之间的交互操作。所以具体的码流数据其实是用RTP封装传输的,第三部分我会详细讲码流数据的处理和发送。


一次基本的RTSP交互过程如上图所示,C表示客户端即请求码流的用户,S为服务器即网络摄像机。
首先客户端连接到流媒体服务器并发送一个RTSP描述请求(DESCRIBE request),服务器通过一个SDP(Session DescriptionProtocol)描述来进行反馈(DESCRIBEresponse),反馈信息包括流数量、媒体类型等信息。客户端分析该SDP描述,并为会话中的每一个流发送一个RTSP连接建立请求(SETUPrequest),该命令会告诉服务器用于接收媒体数据的端口,服务器响应该请求(SETUP response)并建立连接之后,就开始传送媒体流(RTP包)到客户端。在播放过程中客户端还可以向服务器发送请求来控制快进、快退和暂停等。最后,客户端可发送一个终止请求(TEARDOWN request)来结束流媒体会话。

RTSP最基本的东西就是这些,其他复杂的东西我也不想说太多了,有兴趣的可以查查RFC2326(假装复杂的东西我懂似的),OK,讲代码。上一部分我放了我main函数写的东西,在tcp_listen()这个函数之后我建立了一个Socket连接,并把套接字传到了EventLoop()这个函数里面,上面说了RTSP并不负责传输具体视音频数据,这部分是由RTP传输的,所以在tcp_listen建立的套接字是用来做RTSP消息传输的这里的SOCKET是TCP,后面我还会再建立新的UDP SOCKET用以传输具体的视频数据,这个具体后面会说这里提一句。

    static int s32ConCnt = 0;//已经连接的客户端数int s32Fd = -1;static RTSP_buffer *pRtspList=NULL;RTSP_buffer *p=NULL;unsigned int u32FdFound;/*接收连接,创建一个新的socket*/if (s32ConCnt!=-1){s32Fd= tcp_accept(s32MainFd);}/*处理新创建的连接*/if (s32Fd >= 0){/*查找列表中是否存在此连接的socket*/for (u32FdFound=0,p=pRtspList; p!=NULL; p=p->next){if (p->fd == s32Fd){printf("### exit socket Fd ###\n");u32FdFound=1;break;}}if (!u32FdFound){/*创建一个连接,增加一个客户端*/if (s32ConCnt<MAX_CONNECTION){++s32ConCnt;AddClient(&pRtspList,s32Fd);}else{fprintf(stderr, "exceed the MAX client, ignore this connecting\n");return;}num_conn++;fprintf(stderr, "%s Connection reached: %d\n", __FUNCTION__, num_conn);}}/*对已有的连接进行调度*/ScheduleConnections(&pRtspList,&s32ConCnt);

上面是EventLoop()函数的源码,讲一下RTSP_buffer这个结构体,定义在下面。因为这个直播是一对多的场景,即一个摄像头,多个用户同时观看,所以注定连接数肯定是大于1的,那么,多一个连接,这个新的连接的套接字等信息肯定也不一样,所以将每一个连接的属性做一个统一的结构体,且设置为链表的结构便于处理。

typedef struct _RTSP_buffer {int fd;    /*socket文件描述符*/unsigned int port;/*端口号*/struct sockaddr stClientAddr;char in_buffer[RTSP_BUFFERSIZE];/*接收缓冲区*/unsigned int in_size;/*接收缓冲区的大小*/char out_buffer[RTSP_BUFFERSIZE+MAX_DESCR_LENGTH];/*发送缓冲区*/int out_size;/*发送缓冲区大小*/unsigned int rtsp_cseq;/*序列号*/char descr[MAX_DESCR_LENGTH];/*描述*/RTSP_session *session_list;/*会话链表*/struct _RTSP_buffer *next; /*指向下一个结构体,构成了链表结构*/
} RTSP_buffer;

OK,EventLoop()这个函数我们可以看出来首先是判断是不是一个新的套接字:

  1. 如果是一个新的套接字,给这个新的套接字建立一个新的连接,即加一个客户端。进到AddClient()函数
RTSP_buffer *pRtsp=NULL,*pRtspNew=NULL;#ifdef RTSP_DEBUGfprintf(stderr, "%s, %d\n", __FUNCTION__, __LINE__);
#endif//在链表头部插入第一个元素if (*ppRtspList==NULL){/*分配空间*/if ( !(*ppRtspList=(RTSP_buffer*)calloc(1,sizeof(RTSP_buffer)) ) ){fprintf(stderr,"alloc memory error %s,%i\n", __FILE__, __LINE__);return;}pRtsp = *ppRtspList;}else{//向链表中插入新的元素for (pRtsp=*ppRtspList; pRtsp!=NULL; pRtsp=pRtsp->next){pRtspNew=pRtsp;}/*在链表尾部插入*/if (pRtspNew!=NULL){if ( !(pRtspNew->next=(RTSP_buffer *)calloc(1,sizeof(RTSP_buffer)) ) ){fprintf(stderr, "error calloc %s,%i\n", __FILE__, __LINE__);return;}pRtsp=pRtspNew->next;pRtsp->next=NULL;}}//设置最大轮询id号if(g_s32Maxfd < fd){g_s32Maxfd = fd;}/*初始化新添加的客户端*/RTSP_initserver(pRtsp,fd);fprintf(stderr,"Incoming RTSP connection accepted on socket: %d\n",pRtsp->fd);

从上面的AddClient()函数我们可以看到其实就是给新的连接的链表分配空间,初始化套接字,缓冲区等信息。

2.如果不是一个新的套接字,即不是新的连接,进到ScheduleConnections()进行已有连接的交互操作

int res;RTSP_buffer *pRtsp=*rtsp_list,*pRtspN=NULL;RTP_session *r=NULL, *t=NULL;while (pRtsp!=NULL){if ((res = RtspServer(pRtsp))!=ERR_NOERROR){if (res==ERR_CONNECTION_CLOSE || res==ERR_GENERIC){/*连接已经关闭*/if (res==ERR_CONNECTION_CLOSE)fprintf(stderr,"fd:%d,RTSP connection closed by client.\n",pRtsp->fd);elsefprintf(stderr,"fd:%d,RTSP connection closed by server.\n",pRtsp->fd);/*客户端在发送TEARDOWN 之前就截断了连接,但是会话却没有被释放*/if (pRtsp->session_list!=NULL){r=pRtsp->session_list->rtp_session;/*释放所有会话*/while (r!=NULL){t = r->next;RtpDelete((unsigned int)(r->hndRtp));schedule_remove(r->sched_id);r=t;}/*释放链表头指针*/free(pRtsp->session_list);pRtsp->session_list=NULL;g_s32DoPlay--;if (g_s32DoPlay == 0) {printf("user abort! no user online now resetfifo\n");ringreset();/* 重新将所有可用的RTP端口号放入到port_pool[MAX_SESSION] 中 */RTP_port_pool_init(RTP_DEFAULT_PORT);}fprintf(stderr,"WARNING! fd:%d RTSP connection truncated before ending operations.\n",pRtsp->fd);}// wait forclose(pRtsp->fd);--*conn_count;num_conn--;/*释放rtsp缓冲区*/if (pRtsp==*rtsp_list){//链表第一个元素就出错,则pRtspN为空printf("first error,pRtsp is null\n");*rtsp_list=pRtsp->next;free(pRtsp);pRtsp=*rtsp_list;}else{//不是链表中的第一个,则把当前出错任务删除,并把next任务存放在pRtspN(上一个没有出错的任务)//指向的next,和当前需要处理的pRtsp中.printf("dell current fd:%d\n",pRtsp->fd);pRtspN->next=pRtsp->next;free(pRtsp);pRtsp=pRtspN->next;printf("current next fd:%d\n",pRtsp->fd);}/*适当情况下,释放调度器本身*/if (pRtsp==NULL && *conn_count<0){fprintf(stderr,"to stop cchedule_do thread\n");stop_schedule=1;}}else{ printf("current fd:%d\n",pRtsp->fd);pRtsp = pRtsp->next;}}else{//没有出错//上一个处理没有出错的list存放在pRtspN中,需要处理的任务放在pRtst中pRtspN = pRtsp;pRtsp = pRtsp->next;}}

上面的源码主要是循环处理所有RTSP_buffer链表中的RTSP报文,具体处理过程在RtspServer()函数中,若其中某个连接有问题就会从链表中清除此连接并释放相关的内存。我们来看看RtspServer():

fd_set rset,wset;       /*读写I/O描述集*/struct timeval t;int size;static char buffer[RTSP_BUFFERSIZE+1]; /* +1 to control the final '\0'*/int n;int res;struct sockaddr ClientAddr;if (rtsp == NULL){return ERR_NOERROR;}/*变量初始化*/FD_ZERO(&rset);FD_ZERO(&wset);t.tv_sec=0;               /*select 时间间隔*/t.tv_usec=100000;FD_SET(rtsp->fd,&rset);/*调用select等待对应描述符变化*/if (select(g_s32Maxfd+1,&rset,0,0,&t)<0){fprintf(stderr,"select error %s %d\n", __FILE__, __LINE__);send_reply(500, NULL, rtsp);return ERR_GENERIC; //errore interno al server}/*有可供读进的rtsp包*/if (FD_ISSET(rtsp->fd,&rset)){memset(buffer,0,sizeof(buffer));size=sizeof(buffer)-1;  /*最后一位用于填充字符串结束标识*//*读入数据到缓冲区中*/
#ifdef RTSP_DEBUGfprintf(stderr, "tcp_read, %d\n", __LINE__);
#endifn= tcp_read(rtsp->fd, buffer, size, &ClientAddr);if (n==0){return ERR_CONNECTION_CLOSE;}if (n<0){fprintf(stderr,"read() error %s %d\n", __FILE__, __LINE__);send_reply(500, NULL, rtsp);                //服务器内部错误消息return ERR_GENERIC;}//检查读入的数据是否产生溢出if (rtsp->in_size+n>RTSP_BUFFERSIZE){fprintf(stderr,"RTSP buffer overflow (input RTSP message is most likely invalid).\n");send_reply(500, NULL, rtsp);return ERR_GENERIC;//数据溢出错误}#ifdef RTSP_DEBUGfprintf(stderr,"INPUT_BUFFER was:%s\n", buffer);
#endif/*填充数据*/memcpy(&(rtsp->in_buffer[rtsp->in_size]),buffer,n);rtsp->in_size+=n;//清空buffermemset(buffer, 0, n);//添加客户端地址信息memcpy(  &rtsp->stClientAddr, &ClientAddr, sizeof(ClientAddr));/*处理缓冲区的数据,进行rtsp处理*/if ((res=RTSP_handler(rtsp))==ERR_GENERIC){fprintf(stderr,"Invalid input message.\n");return ERR_NOERROR;}}/*有发送数据*/if (rtsp->out_size>0){//将数据发送出去n= tcp_write(rtsp->fd,rtsp->out_buffer,rtsp->out_size);        fprintf(stderr,"S");fflush(stderr);if (n<0){fprintf(stderr,"tcp_write error %s %i\n", __FILE__, __LINE__);send_reply(500, NULL, rtsp);return ERR_GENERIC; //errore interno al server}#ifdef  RTSP_DEBUGfprintf(stderr,"OUTPUT_BUFFER length %d\n%s\n", rtsp->out_size, rtsp->out_buffer);
#endif//清空发送缓冲区memset(rtsp->out_buffer, 0, rtsp->out_size);rtsp->out_size = 0;}//如果需要RTCP在此出加入对RTCP数据的接收,并存放在缓存中。//继而在schedule_do线程中对其处理。//rtcp控制处理,检查读入RTCP数据报return ERR_NOERROR;

很熟悉吧?是我们上部分说过的非阻塞的传输方式,通过Socket获取客户端发过来的rtsp信报,然后将rtsp信报传到RTSP_handler()函数进行处理

int s32Meth;while(pRtspBuf->in_size){s32Meth = RTSP_validate_method(pRtspBuf);if (s32Meth < 0){//错误的请求,请求的方法不存在fprintf(stderr,"Bad Request %s,%d\n", __FILE__, __LINE__);printf("bad request, requestion not exit %d",s32Meth);send_reply(400, NULL, pRtspBuf);}else{//进入到状态机,处理接收的请求RTSP_state_machine(pRtspBuf, s32Meth);printf("exit Rtsp_state_machine\r\n");}//丢弃处理之后的消息RTSP_discard_msg(pRtspBuf);printf(" After RTSP_discard_msg\r\n");}return ERR_NOERROR;

RTSP_validate_method(pRtspBuf)这个函数是通过sscanf()来按格式读取rtsp数据报,这里简要分析一下这个函数的一些简单用法

  1. 常见用法。

charstr[512]={0};
  sscanf(“123456”,"%s",str);
  printf(“str=%s”,str);

2. 取指定长度的字符串。如在下例中,取最大长度为4字节的字符串。

sscanf(“123456”,"%4s",str);
  printf(“str=%s”,str);

3. 取到指定字符为止的字符串。如在下例中,取遇到空格为止字符串。

sscanf(“123456abcdedf”,"%[^]",str);
  printf(“str=%s”,str);

4. 取仅包含指定字符集的字符串。如在下例中,取仅包含1到9和小写字母的字符串。

sscanf(“123456abcdedfBCDEF”,"%[1-9a-z]",str);
  printf(“str=%s”,str);

5. 取到指定字符集为止的字符串。如在下例中,取遇到大写字母为止的字符串。

sscanf(“123456abcdedfBCDEF”,"%[^A-Z]",str);
  printf(“str=%s”,str);
 RTSP_validate_method()通过sscanf()这个函数来读取客户端rtsp信报中当前的方法来设置状态并返回。
 这里重点讲一下RTSP_state_machine()这个函数,前面获取的当前方法会传入这个函数,这函数其实就是一个状态机,来实现各个方法的回传的信报拼接并传回客户端。

    /*除了播放过程中发送的最后一个数据流,*所有的状态迁移都在这里被处理* 状态迁移位于stream_event中*/char *s;RTSP_session *pRtspSess;long int session_id;char trash[255];char szDebug[255];/*找到会话位置*/if ((s = strstr(pRtspBuf->in_buffer, HDR_SESSION)) != NULL){if (sscanf(s, "%254s %ld", trash, &session_id) != 2){fprintf(stderr,"Invalid Session number %s,%i\n", __FILE__, __LINE__);send_reply(454, 0, pRtspBuf);              /* 没有此会话*/return;}}/*打开会话列表*/pRtspSess = pRtspBuf->session_list;if (pRtspSess == NULL){return;}#ifdef RTSP_DEBUGsprintf(szDebug,"state_machine:current state is  ");strcat(szDebug,((pRtspSess->cur_state==0)?"init state":((pRtspSess->cur_state==1)?"ready state":"play state")));printf("%s\n", szDebug);
#endif/*根据状态迁移规则,从当前状态开始迁移*/switch (pRtspSess->cur_state){case INIT_STATE:                    /*初始态*/{#ifdef RTSP_DEBUGfprintf(stderr,"current method code is:  %d  \n",method);
#endifswitch (method){case RTSP_ID_DESCRIBE:  //状态不变RTSP_describe(pRtspBuf);//printf("3\r\n");break;case RTSP_ID_SETUP:                //状态变为就绪态//printf("4\r\n");if (RTSP_setup(pRtspBuf) == ERR_NOERROR){//printf("5\r\n");pRtspSess->cur_state = READY_STATE;fprintf(stderr,"TRANSFER TO READY STATE!\n");}break;case RTSP_ID_TEARDOWN:       //状态不变RTSP_teardown(pRtspBuf);break;case RTSP_ID_OPTIONS:if (RTSP_options(pRtspBuf) == ERR_NOERROR){pRtspSess->cur_state = INIT_STATE;         //状态不变}break;case RTSP_ID_PLAY:          //method not valid this state.case RTSP_ID_PAUSE:send_reply(455, 0, pRtspBuf);break;default:send_reply(501, 0, pRtspBuf);break;}break;}case READY_STATE:{#ifdef RTSP_DEBUGfprintf(stderr,"current method code is:%d\n",method);
#endifswitch (method){case RTSP_ID_PLAY:                                      //状态迁移为播放态if (RTSP_play(pRtspBuf) == ERR_NOERROR){fprintf(stderr,"\tStart Playing!\n");pRtspSess->cur_state = PLAY_STATE;}break;case RTSP_ID_SETUP:if (RTSP_setup(pRtspBuf) == ERR_NOERROR)    //状态不变{pRtspSess->cur_state = READY_STATE;}break;case RTSP_ID_TEARDOWN:RTSP_teardown(pRtspBuf);                 //状态变为初始态 ?break;case RTSP_ID_OPTIONS:if (RTSP_options(pRtspBuf) == ERR_NOERROR){pRtspSess->cur_state = INIT_STATE;          //状态不变}break;case RTSP_ID_PAUSE:                  // method not valid this state.send_reply(455, 0, pRtspBuf);break;case RTSP_ID_DESCRIBE:RTSP_describe(pRtspBuf);break;default:send_reply(501, 0, pRtspBuf);break;}break;}case PLAY_STATE:{switch (method){case RTSP_ID_PLAY:// Feature not supportedfprintf(stderr,"UNSUPPORTED: Play while playing.\n");send_reply(551, 0, pRtspBuf);        // Option not supportedbreak;
/*              //不支持暂停命令case RTSP_ID_PAUSE:                //状态变为就绪态if (RTSP_pause(pRtspBuf) == ERR_NOERROR){pRtspSess->cur_state = READY_STATE;}break;
*/case RTSP_ID_TEARDOWN:RTSP_teardown(pRtspBuf);        //状态迁移为初始态break;case RTSP_ID_OPTIONS:break;case RTSP_ID_DESCRIBE:RTSP_describe(pRtspBuf);break;case RTSP_ID_SETUP:break;}break;}/* PLAY state */default:{/* invalid/unexpected current state. */fprintf(stderr,"%s State error: unknown state=%d, method code=%d\n", __FUNCTION__, pRtspSess->cur_state, method);}break;}/* end of current state switch */#ifdef RTSP_DEBUGprintf("leaving rtsp_state_machine!\n");
#endif

以上就是处理rtsp交互的状态机,结合上面列出的RTSP交互过程,describe和setup两个方法将在初始态处理,当setup成功后,状态变为就绪态,这个状态下会给一些必要的属性重新赋值,这些属性是控制底层数据读取的标志,之后变成PLAY态,这个状态下会不断从底层取数据处理并封装发送到客户端,一旦连接中断,状态又会变回初始态。下面我讲一下各个方法的处理代码。
首先是Describe

char object[255], trash[255];char *p;unsigned short port;char s8Url[255];char s8Descr[MAX_DESCR_LENGTH];char server[128];char s8Str[128];/*根据收到的请求请求消息,跳过方法名,分离出URL*/if (!sscanf(pRtsp->in_buffer, " %*s %254s ", s8Url))//%*s表示第一个匹配到的%s被过滤掉{fprintf(stderr, "Error %s,%i\n", __FILE__, __LINE__);send_reply(400, 0, pRtsp);                           /* bad request */printf("get URL error");return ERR_NOERROR;}/*验证URL */switch (ParseUrl(s8Url, server, &port, object, sizeof(object))){case 1: /*请求错误*/fprintf(stderr, "Error %s,%i\n", __FILE__, __LINE__);send_reply(400, 0, pRtsp);printf("url request error");return ERR_NOERROR;break;case -1: /*内部错误*/fprintf(stderr,"url error while parsing !\n");send_reply(500, 0, pRtsp);printf("inner error");return ERR_NOERROR;break;default:break;}/*取得序列号,并且必须有这个选项*/if ((p = strstr(pRtsp->in_buffer, HDR_CSEQ)) == NULL){fprintf(stderr, "Error %s,%i\n", __FILE__, __LINE__);send_reply(400, 0, pRtsp);  /* Bad Request */printf("get serial num error");return ERR_NOERROR;}else{if (sscanf(p, "%254s %d", trash, &(pRtsp->rtsp_cseq)) != 2){fprintf(stderr, "Error %s,%i\n", __FILE__, __LINE__);send_reply(400, 0, pRtsp);   /*请求错误*/printf("get serial num 2 error");return ERR_NOERROR;}}//获取SDP内容GetSdpDescr(pRtsp, s8Descr, s8Str);//发送Describe响应//printf("----------------1\r\n");SendDescribeReply(pRtsp, object, s8Descr, s8Str);//printf("2\r\n");return ERR_NOERROR;

这里说一下一些常用的字段处理函数:
①strstr

strstr(str1,str2)

strstr()函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。
② strncmp

int strncmp ( const char * str1, const char * str2, size_t n );

str1, str2 为需要比较的两个字符串,n为要比较的字符的数目。若str1与str2的前n个字符相同,则返回0;若s1大于s2,则返回大于0的值;若s1 小于s2,则返回小于0的值。
③strchr

char *strchr(const char *s,char c)

查找字符串s中首次出现字符c的位置。
④strncpy

char*strncpy(char *dest, const char *src, int n)

把src所指向的字符串中以src地址开始的前n个字节复制到dest所指的数组中,并返回被复制后的dest
⑤strcpy

char *strcpy(char* dest, const char *src);

把从src地址开始且含有NULL结束符(’\0’)的字符串复制到以dest开始的地址空间,src和dest所指内存区域不可以重叠且dest必须有足够的空间来容纳src的字符串。
⑥strcat

char *strcat(char *dest, const char *src);

把src所指向的字符串(包括“\0”)复制到dest所指向的字符串后面(删除dest原来末尾的“\0”)。要保证dest足够长,以容纳被复制进来的*src。*src中原有的字符不变。返回指向dest的指针。

通过以上这些字段处理函数提取RTSP信报中想要的信息,并在GetSdpDescr(pRtsp, s8Descr, s8Str);这个函数中组建自己的SDP。这里贴一下组的SDP:

struct ifreq stIfr;//linux 网络接口用来配置ip地址,激活接口,配置MTUchar pSdpId[128];//char rtp_port[5];strcpy(stIfr.ifr_name, "br0");if(ioctl(pRtsp->fd, SIOCGIFADDR, &stIfr) < 0){//printf("Failed to get host eth0 ip\n");strcpy(stIfr.ifr_name, "eth0");if(ioctl(pRtsp->fd, SIOCGIFADDR, &stIfr) < 0){printf("Failed to get host br0 or eth0 ip\n");}}sock_ntop_host(&stIfr.ifr_addr, sizeof(struct sockaddr), s8Str, 128);GetSdpId(pSdpId);strcpy(pDescr, "v=0\r\n");    strcat(pDescr, "o=-");strcat(pDescr, pSdpId);strcat(pDescr," ");strcat(pDescr, pSdpId);strcat(pDescr," IN IP4 ");strcat(pDescr, s8Str);strcat(pDescr, "\r\n");strcat(pDescr, "s=H.264 Video, streamed by the Test Media Server\r\n");//(session name)strcat(pDescr, "i=test.h264\r\n");//session的信息strcat(pDescr, "t=0 0\r\n");//时间信息,分别表示开始的时间和结束的时间,一般在流媒体的直播的时移中见的比较多strcat(pDescr, "a=tool:Test Streaming Media v2018.11.30\r\n");//创建任务描述的工具的名称及版本号 strcat(pDescr, "a=type:broadcast\r\n");//会议类型strcat(pDescr, "a=control:*\r\n");strcat(pDescr, "m=video 0 RTP/AVP 96\r\n");strcat(pDescr, "\r\n");     strcat(pDescr,"b=AS:500\r\n");/**** Dynamically defined payload ****/strcat(pDescr,"a=rtpmap:96");strcat(pDescr," ");  strcat(pDescr,"H264/90000");strcat(pDescr, "\r\n");strcat(pDescr,"a=fmtp:96 packetization-mode=1;");strcat(pDescr,"profile-level-id=");strcat(pDescr,psp.base64profileid);strcat(pDescr,";sprop-parameter-sets=");strcat(pDescr,psp.base64sps);strcat(pDescr,",");strcat(pDescr,psp.base64pps);strcat(pDescr, "\r\n");strcat(pDescr,"a=control:track1");strcat(pDescr, "\r\n");printf("\n\n%s,%d===>psp.base64profileid=%s,psp.base64sps=%s,psp.base64pps=%s\n\n",__FUNCTION__,__LINE__,psp.base64profileid,psp.base64sps,psp.base64pps);

关于SDP协议我这里不写太多

https://blog.csdn.net/jobbofhe/article/details/78477407

这篇博文写得挺全了我觉得,想了解多一点的可以在上面的网址看看。这里注意一下,sprop-parameter-sets=后面跟的是Base64编码后的SPS和PPS。组好SDP之后发送出去

 char *pMsgBuf;            /* 用于获取响应缓冲指针*/int s32MbLen;/* 分配空间,处理内部错误*/s32MbLen = 2048;pMsgBuf = (char *)malloc(s32MbLen);if (!pMsgBuf){fprintf(stderr,"send_describe_reply(): unable to allocate memory\n");send_reply(500, 0, rtsp);    /* internal server error */if (pMsgBuf){free(pMsgBuf);}return ERR_ALLOC;}/*构造describe消息串*/sprintf(pMsgBuf, "%s %d %s"RTSP_EL"CSeq: %d"RTSP_EL"Server: %s/%s"RTSP_EL, RTSP_VER, 200, get_stat(200), rtsp->rtsp_cseq, PACKAGE, VERSION);add_time_stamp(pMsgBuf, 0);                 /*添加时间戳*/strcat(pMsgBuf, "Content-Type: application/sdp"RTSP_EL);   /*实体头,表示实体类型*//*用于解析实体内相对url的 绝对url*/sprintf(pMsgBuf + strlen(pMsgBuf), "Content-Base: rtsp://%s/%s/"RTSP_EL, s8Str, object);sprintf(pMsgBuf + strlen(pMsgBuf), "Content-Length: %d"RTSP_EL, strlen(descr)); /*消息体的长度*/strcat(pMsgBuf, RTSP_EL);/*消息头结束*//*加上消息体*/strcat(pMsgBuf, descr);    /*describe消息*//*向缓冲区中填充数据*/bwrite(pMsgBuf, (unsigned short) strlen(pMsgBuf), rtsp);free(pMsgBuf);return ERR_NOERROR;

Describe 之后是SetUp

char s8TranStr[128], *s8Str;char *pStr;RTP_transport Transport;int s32SessionID=0;RTP_session *rtp_s, *rtp_s_prec;RTSP_session *rtsp_s;if ((s8Str = strstr(pRtsp->in_buffer, HDR_TRANSPORT)) == NULL){fprintf(stderr, "Error %s,%i\n", __FILE__, __LINE__);send_reply(406, 0, pRtsp);     // Not Acceptableprintf("not acceptable");return ERR_NOERROR;}//检查传输层子串是否正确if (sscanf(s8Str, "%*10s %255s", s8TranStr) != 1){fprintf(stderr,"SETUP request malformed: Transport string is empty\n");send_reply(400, 0, pRtsp);       // Bad Requestprintf("check transport 400 bad request");return ERR_NOERROR;}fprintf(stderr,"*** transport: %s ***\n", s8TranStr);//如果需要增加一个会话if ( !pRtsp->session_list ){pRtsp->session_list = (RTSP_session *) calloc(1, sizeof(RTSP_session));}rtsp_s = pRtsp->session_list;//建立一个新会话,插入到链表中if (pRtsp->session_list->rtp_session == NULL){pRtsp->session_list->rtp_session = (RTP_session *) calloc(1, sizeof(RTP_session));rtp_s = pRtsp->session_list->rtp_session;}else{for (rtp_s = rtsp_s->rtp_session; rtp_s != NULL; rtp_s = rtp_s->next){rtp_s_prec = rtp_s;}rtp_s_prec->next = (RTP_session *) calloc(1, sizeof(RTP_session));rtp_s = rtp_s_prec->next;}//起始状态为暂停rtp_s->pause = 1;rtp_s->hndRtp = NULL;Transport.type = RTP_no_transport;if((pStr = strstr(s8TranStr, RTSP_RTP_AVP))){//Transport: RTP/AVPpStr += strlen(RTSP_RTP_AVP);if ( !*pStr || (*pStr == ';') || (*pStr == ' ')){//单播if (strstr(s8TranStr, "unicast")){//如果指定了客户端端口号,填充对应的两个端口号if( (pStr = strstr(s8TranStr, "client_port")) ){pStr = strstr(s8TranStr, "=");sscanf(pStr + 1, "%d", &(Transport.u.udp.cli_ports.RTP));pStr = strstr(s8TranStr, "-");sscanf(pStr + 1, "%d", &(Transport.u.udp.cli_ports.RTCP));}//服务器端口if (RTP_get_port_pair(&Transport.u.udp.ser_ports) != ERR_NOERROR){fprintf(stderr, "Error %s,%d\n", __FILE__, __LINE__);send_reply(500, 0, pRtsp);/* Internal server error */return ERR_GENERIC;}//建立RTP套接字rtp_s->hndRtp = (struct _tagStRtpHandle*)RtpCreate((unsigned int)(((struct sockaddr_in *)(&pRtsp->stClientAddr))->sin_addr.s_addr), Transport.u.udp.cli_ports.RTP, _h264nalu);printf("<><><><>Creat RTP<><><><>\n");Transport.u.udp.is_multicast = 0;}else{printf("multicast not codeing\n");//multicast 多播处理....}Transport.type = RTP_rtp_avp;}else if (!strncmp(s8TranStr, "/TCP", 4)){if( (pStr = strstr(s8TranStr, "interleaved")) ){pStr = strstr(s8TranStr, "=");sscanf(pStr + 1, "%d", &(Transport.u.tcp.interleaved.RTP));if ((pStr = strstr(pStr, "-")))sscanf(pStr + 1, "%d", &(Transport.u.tcp.interleaved.RTCP));elseTransport.u.tcp.interleaved.RTCP = Transport.u.tcp.interleaved.RTP + 1;}else{}Transport.rtp_fd = pRtsp->fd;
//          Transport.rtcp_fd_out = pRtsp->fd;
//          Transport.rtcp_fd_in = -1;}}printf("pstr=%s\n",pStr);if (Transport.type == RTP_no_transport){fprintf(stderr,"AAAAAAAAAAA Unsupported Transport,%s,%d\n", __FILE__, __LINE__);send_reply(461, 0, pRtsp);// Bad Requestreturn ERR_NOERROR;}memcpy(&rtp_s->transport, &Transport, sizeof(Transport));//如果有会话头,就有了一个控制集合if ((pStr = strstr(pRtsp->in_buffer, HDR_SESSION)) != NULL){if (sscanf(pStr, "%*s %d", &s32SessionID) != 1){fprintf(stderr, "Error %s,%i\n", __FILE__, __LINE__);send_reply(454, 0, pRtsp); // Session Not Foundreturn ERR_NOERROR;}}else{//产生一个非0的随机的会话序号struct timeval stNowTmp;gettimeofday(&stNowTmp, 0);srand((stNowTmp.tv_sec * 1000) + (stNowTmp.tv_usec / 1000));s32SessionID = 1 + (int) (10.0 * rand() / (100000 + 1.0));if (s32SessionID == 0){s32SessionID++;}}pRtsp->session_list->session_id = s32SessionID;pRtsp->session_list->rtp_session->sched_id = schedule_add(rtp_s);send_setup_reply(pRtsp, rtsp_s, rtp_s);return ERR_NOERROR;

这里重点讲一下两地方吧,一个是RtpCreate()函数

HndRtp hRtp = NULL;struct timeval stTimeval;struct ifreq stIfr;int s32Broadcast = 1;struct sockaddr_in addr;hRtp = (HndRtp)calloc(1, sizeof(StRtpObj));if(NULL == hRtp){printf("Failed to create RTP handle\n");goto cleanup;}hRtp->s32Sock = -1;if((hRtp->s32Sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0){printf("Failed to create socket\n");goto cleanup;}if(0xFF000000 == (u32IP & 0xFF000000)){if(-1 == setsockopt(hRtp->s32Sock, SOL_SOCKET, SO_BROADCAST, (char *)&s32Broadcast, sizeof(s32Broadcast))){printf("Failed to set socket\n");goto cleanup;}}memset(&addr, 0, sizeof(addr));while(1){addr.sin_port = BigLittleSwap16(server_port);addr.sin_family = AF_INET;addr.sin_addr.s_addr = INADDR_ANY;hRtp->stServAddr.sin_family = AF_INET;hRtp->stServAddr.sin_port = BigLittleSwap16(s32Port);hRtp->stServAddr.sin_addr.s_addr = u32IP;bzero(&(hRtp->stServAddr.sin_zero), 8);if (bind(hRtp->s32Sock, (struct sockaddr *)&addr, sizeof(addr))){printf("can't bind !!!!!!!!!!!!!!!!!!!!!!!!!!!");server_port++;}elsebreak;}//初始化序号hRtp->u16SeqNum = 0;//初始化时间戳hRtp->u32TimeStampInc = 0;hRtp->u32TimeStampCurr = 0;//获取当前时间if(gettimeofday(&stTimeval, NULL) == -1){printf("Failed to get os time\n");goto cleanup;}hRtp->u32PrevTime = stTimeval.tv_sec * 1000 + stTimeval.tv_usec / 1000;hRtp->emPayload = emPayload;//获取本机网络设备名strcpy(stIfr.ifr_name, "br0");if(ioctl(hRtp->s32Sock, SIOCGIFADDR, &stIfr) < 0){//printf("Failed to get host ip\n");strcpy(stIfr.ifr_name, "eth0");if(ioctl(hRtp->s32Sock, SIOCGIFADDR, &stIfr) < 0){printf("Failed to get host eth0 or wlan0 ip\n");goto cleanup;}}hRtp->u32SSrc = BigLittleSwap32(((struct sockaddr_in *)(&stIfr.ifr_addr))->sin_addr.s_addr);local_ip = hRtp->u32SSrc;//hRtp->u32SSrc = htonl(((struct sockaddr_in *)(&stIfr.ifr_addr))->sin_addr.s_addr);printf("!!!!!!!!!!!!!!!!!!!!!!rtp create:addr:%x,port:%d,local%x\n",u32IP,s32Port,hRtp->u32SSrc);printf("<><><><>success creat RTP<><><><>\n");return (unsigned int)hRtp;
cleanup:if(hRtp){if(hRtp->s32Sock >= 0){close(hRtp->s32Sock);}free(hRtp);}

这个函数主要是创建了一个用于RTP传输的Socket,用来传输封装好的RTP数据包,这一部分我会在第三部分讲。另一个要注意的地方就是在

pRtsp->session_list->rtp_session->sched_id = schedule_add(rtp_s);

schedule_add(rtp_s)这个函数里面给每一个连接的关键参数置位,这些参数是用来控制取底层数据并保存在缓冲区的判断依据,还有就是设置了RtpSend()这个回调函数,这个回调函数会在Play阶段被调用,用来封装底层上来的码流数据并发送。

Play和Teardown比较简单,我这边就不放代码了,PLay主要还是设置属性,使进入读取底层数据并保存的判定为真,Teardown主要是释放一些内存,以及释放RTSP链表的一些操作。

总结一下吧,关于RTSP交互这一块,首先就是建立一个Socket用以接受发送RTSP的数据报文的,通过这个Socket进行我上面介绍过的服务器和客户端的交互,在Setup阶段创建新的Socket连接用来做具体的码流数据传输,Play阶段就是不断的取底层数据进行封装发送,TearDown阶段断开连接并释放相关的指针或链表。

关于具体的码流是怎么封装的我会在第三部分讲。

网络摄像头Rtsp直播方案(二)相关推荐

  1. 网络摄像头Rtsp直播方案(一)

    前段时间写完了RTMP的直播方案,因为是基于librtmp的库来实现的,所以比较简单.之后花了一个月吧,参照海思的rtsp推流框架,慢慢的写了一个基于RealTek为底层的网络摄像头Rtsp直播功能的 ...

  2. 网络摄像头RTSP直播方案(三)

    前面的部分讲了关于RTSP连接的交互过程,在RTSP推流的过程中,RTSP协议只是做一个控制作用,底层真正进行传输的流媒体协议还是RTP协议.做这一部分主要是要先了解RTP协议的封装格式,这里我不详细 ...

  3. ffmpeg api推流,谷歌浏览器播放大华、海康威视网络摄像头rtsp视频流方案(hls、m3u8、flv、webrtc、srs、nginx、nginx-rtmp、rtmp)比较

    ffmpeg api推流,谷歌浏览器播放大华.海康威视网络摄像头rtsp视频流方案(hls.m3u8.flv.webrtc.srs.nginx.nginx-rtmp.rtmp)比较 将网络摄像头视频流 ...

  4. 海康大华天地伟业网络摄像头chrome浏览器web二次开发

    海康大华天地伟业网络摄像头chrome浏览器二次开发 海康大华天地伟业网络摄像头chrome浏览器web二次开发 由于工作的原因需要开发海康和大华,还有天地伟业的摄像头,而且必须是本地部署开发,每个厂 ...

  5. 摄像头网页服务器,网络摄像头实现直播的方法 在网页浏览器播放等于可以在网页传播...

    网络摄像头实现直播的方法,可以在网页浏览器播放,可以发送给你的朋友,可以放到你的官网去增加一条播放链接,可以在网页文章里增加一条播放链接.怎么实现呢? 需要的准备如下: 1.网络摄像头一个 2.电脑一 ...

  6. 安防摄像头互联网直播方案LiveGBS设计文档

    LiveGBS设计文档 一.介绍 28181协议全称为GB/T28181<安全防范视频监控联网系统信息传输.交换.控制技术要求>,是由公安部科技信息化局提出,由全国安全防范报警系统标准化技 ...

  7. 网络摄像头RTSP视频流WEB端实时播放实现方案

    IPC视频流怎么实时在WEB浏览器播放,视频流格式是RTSP. 下面我整理了自己实现的方案以及网上看到的一些方案 一.FFmpeg + nginx 将转 hls 通过 video.js 在支持h5浏览 ...

  8. 网络摄像头RTSP拉流协议网页无插件视频直播平台EasyNVR为什么无法获取通道接口数据?

    TSINGSEE青犀视频的技术支持最近给我反馈了一个问题,关于代理EasyNVR获取通道接口返回为空的问题.代理EasyNVR的过程也是将EasyNVR集成进其他平台的过程,这个问题在集成过程中还是比 ...

  9. 用ffmpeg+nginx+海康威视网络摄像头rtsp在手机端和电脑端实现直播

    原料:海康威视摄像头,nginx服务器,ffmpeg. 首先海康威视摄像头, 它的rtsp数据流的地址为:rtsp://[username]:[password]@[ip]:[port]/[codec ...

最新文章

  1. 论MOS管开关对电源的影响
  2. linux 拆分文件 多个,linux – 如何拆分文件并保留每个部分的第...
  3. C和汇编---sizeof运算符和strlen函数
  4. C#多线程之旅(七)——终止线程
  5. MySQL查询多表定义实体类_自己设计一个 JAVA + MyBatis 解析实体类多表通用查询
  6. Qt文档阅读笔记-QTcpServer官方解析与实例(使用QSocket创建简单的HTTP服务器)
  7. pytorch的4种边界Padding方法--ZeroPad2d、ConstantPad2d、ReflectionPad2d、ReplicationPad2d
  8. spring boot 整合 mybatis
  9. Android 学习 笔记_07. XML文件解析
  10. 华为鸿蒙智能家居套件价格,华为全屋智能家居方案价格
  11. SolidWorks用鼠标中键控制模型的旋转、缩放和平移
  12. 同步时钟之hwclock命令(硬件-系统,系统-硬件)
  13. 怎么生成a类型的对象 java_用一个 java 程序! 写一个类A, 该类创建的对象可以调用方法f输出英文字母表,然后再编写...
  14. 第四届vex机器人亚洲锦标赛_站在亚洲之巅丨上实剑桥国际高中吴霖哲同学斩获VEX机器人亚洲锦标赛金奖...
  15. win7关闭系统索引服务器,如何关闭Windows7系统中的索引功能
  16. 气象历史数据和空气质量历史数据资源汇总免费
  17. ETL和ELT的区别
  18. 元宇宙将如何影响我们的投资、就业和生活方式?
  19. 软件开发人员的作战手册 - 让程序员活的久一点
  20. JSfunction参数设计的初衷

热门文章

  1. WM_KILLFOCUS 和 WM_SETFOCUS
  2. 查找字符串fing()函数
  3. 温度补偿计算公式_热补偿计算实例
  4. TCGA差异表达分析|2022.5.1更新
  5. 简单删除隐藏文件夹System Volume Information
  6. 实现中英文对接翻译小程序—最终版
  7. torchtorchvision对应版本
  8. cpu低端计算机配置清单,i3 4160/GTX750Ti剑灵/英雄联盟中低端组装机配置清单
  9. 在Vue单文件组件的template标签上使用v-if不生效的原因
  10. iOS 判断是否安装了微信、QQ客户端