系列索引:FFmpeg入门系列索引
上一篇:FFmpeg入门教程11.01:番外篇
本文介绍RTSP流数据解析,RTSP数据包为RTSP包头+RTP数据包,RTP数据包为RTP数据包头+H264数据包。
简单来说:
RTSP(Real Time Streaming Protocol),RFC2326,实时流传输协议,是TCP/IP协议体系中的一个应用层协议,由哥伦比亚大学、网景和RealNetworks公司提交的IETF RFC标准。该协议定义了一对多应用程序如何有效地通过IP网络传送多媒体数据。RTSP在体系结构上位于RTP和RTCP之上,它使用TCP或UDP完成数据传输。HTTP与RTSP相比,HTTP请求由客户机发出,服务器作出响应;使用RTSP时,客户机和服务器都可以发出请求,即RTSP可以是双向的。RTSP是用来控制声音或影像的多媒体串流协议,并允许同时多个串流需求控制,传输时所用的网络通讯协定并不在其定义的范围内,服务器端可以自行选择使用TCP或UDP来传送串流内容,它的语法和运作跟HTTP 1.1类似,但并不特别强调时间同步,所以比较能容忍网络延迟。而前面提到的允许同时多个串流需求控制(Multicast),除了可以降低服务器端的网络用量,更进而支持多方视讯会议(Video Conference)。因为与HTTP1.1的运作方式相似,所以代理服务器〈Proxy〉的快取功能〈Cache〉也同样适用于RTSP,并因RTSP具有重新导向功能,可视实际负载情况来转换提供服务的服务器,以避免过大的负载集中于同一服务器而造成延迟。
准备FFmpeg+wireshark+easydarwin
先按照FFmpeg入门教程10.24:搭建UDP/TCP/HTTP(S)/RTP/RTMP/RTSP推流服务器 搭建一个推流服务器,本文是解析RTSP流数据,自然是搭建RTSP推流服务器。
然后按照wireshark,我使用的版本为3.4.8
然后推流
1 ffmpeg -re -i /home/jackey/Videos/test.mkv -rtsp_transport tcp -vcodec h264 -f rtsp rtsp://localhost/test
然后收流看看有没有推流成功
1 ffplay rtsp://localhost/test
解析使用管理员/root打开wireshart。
不同的版本界面不一样,操作也可能不同。
双击any
那一行
在过滤规则栏填入rtsp
用于过滤rtsp相关的数据
然后在终端新开一个收流客户端,就可以看到
新收流客户端的前10个是类似TCP/IP三次握手的过程。
握手我们来看一下握手的过程
OPTIONS首先向推流服务器发送OPTIONS请求。
我们可以得到
请求类型为:OPTIONS 请求的地址为:rtsp://localhost:554/test 自动在地址后面加上了RTSP的端口 请求的版本为:RTSP/1.0 CSeq:1 User-Agent: Lavf58.7.100 所有的行结尾为:\r\n 我们用代码实现此流程时会用到。
同时,推流服务器会返回给我们请求结果
我们可以得到:
请求结果:RTSP/1.0 200 OK CSeq:1 Session:8h******** Public:DESCRIBE, … 所有行尾为:\r\n CSeq和请求的值一样 Public应该是表示服务器支持的参数 DESCRIBE
第二条信息,CSeq加1,Session是OPTIONS回应的值。
回应为:
CSeq、Session是我们发送的。
SETUP
CSeq加1,发送的参数变了,视频有音视频两条流,所以SETUP有两个。
回应为:
PLAY
CSeq在SETUP的基础上加一(SETUP有两个)
回应为:
代码实现如果我们是在纯代码中开发这个流程,就需要按照这个流程进行发送接收。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 int RTSPData::options (int to) { #define OPTIONS_CMD "OPTIONS %s " RTSP_VERSIION "\r\nCSeq: %d\r\n" RTSP_USERAGENT "\r\n" char cmd[1024 ]; int size = snprintf (cmd, sizeof (cmd), OPTIONS_CMD, _url, CSeq); _send_request(cmd, size); char resp[2048 ]; if (_wait_response(to, resp, sizeof (resp))) { return -1 ; } CSeq++; return 0 ; }int RTSPData::describe (int to) { #define DESCRIBE_CMD "DESCRIBE %s " RTSP_VERSIION "\r\n" "CSeq: %d\r\n" RTSP_USERAGENT "Accept: application/sdp\r\n\r\n" char cmd[1024 ]; int size = snprintf (cmd, sizeof (cmd), DESCRIBE_CMD, _url, CSeq); _send_request(cmd, size); char resp[2048 ]; if (_wait_response(to, resp, sizeof (resp))) { return -1 ; } _parse_sdp(resp); CSeq++; return 0 ; }int RTSPData::_setup_interleaved(int to) { #define SETUPI_CMD_I "SETUP %s " RTSP_VERSIION "\r\nCSeq: %d\r\n" RTSP_USERAGENT "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n" char cmd[1024 ]; int size = snprintf (cmd, sizeof (cmd), SETUPI_CMD_I, control, CSeq); _send_request(cmd, size); char resp[2048 ]; if (_wait_response(to, resp, sizeof (resp))) { return -1 ; } _parse_session(resp); CSeq++; return 0 ; }int RTSPData::setup (int to) { return _setup_interleaved(to); }int RTSPData::play (int to) { #define PLAY_CMD "PLAY %s " RTSP_VERSIION "\r\nCSeq: %d\r\n" RTSP_USERAGENT "Session: %s\r\nRange: npt=0-\r\n\r\n" char cmd[1024 ]; int size = snprintf (cmd, sizeof (cmd), PLAY_CMD, _url, CSeq, sessionId); _send_request(cmd, size); char resp[2048 ]; if (_wait_response(to, resp, sizeof (resp))) { return -1 ; } CSeq++; return 0 ; }int RTSPData::rtsp_init () { CSeq = 1 ; struct hostent *hp; struct sockaddr_in server; rtp_content = (uint8_t *)malloc (rtp_size); hp = gethostbyname (host); if (NULL == hp) { printf ("gethostbyname(%s) error.\n" , host); return -1 ; } rtspSocket = socket (AF_INET, SOCK_STREAM, 0 ); if (rtspSocket < 0 ) { printf ("Create socket failed.\n" ); return -1 ; } memset (&server, 0 , sizeof (struct sockaddr_in)); memcpy (&(server.sin_addr), hp->h_addr, hp->h_length); server.sin_family = AF_INET; server.sin_port = htons ((uint16_t )(port)); if (::connect (rtspSocket, (struct sockaddr*)&server, sizeof (struct sockaddr_in)) != 0 ) { printf ("Connect to server [%x:%d] error.\n" , server.sin_addr.s_addr, server.sin_port); close (rtspSocket); rtspSocket = -1 ; return -1 ; } printf ("Set non-blocking socket.\n" ); int on = 1 ; int rc = ioctl (rtspSocket, FIONBIO, (char *)&on); if (rc < 0 ) { close (rtspSocket); rtspSocket = -1 ; return -1 ; } if (options (rtspTimeout)) { close (rtspSocket); rtspSocket = -1 ; return -1 ; } if (describe (rtspTimeout)) { close (rtspSocket); rtspSocket = -1 ; return -1 ; } if (setup (rtspTimeout)) { close (rtspSocket); rtspSocket = -1 ; return -1 ; } if (play (rtspTimeout)) { close (rtspSocket); rtspSocket = -1 ; return -1 ; } return 0 ; }
以上代码为标准初始化流程,在各个初始化函数中还有一些函数调用
通过socket发送指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int RTSPData::_send_request(const char *req, int size) { qDebug ("send request. %.*s\n" , size, req); ssize_t snd = 0 ; if (rtspSocket < 0 ) { qDebug ("Connection to server has not been set up.\n" ); return -1 ; } ssize_t s; do { s = send (rtspSocket, req + snd, size - snd, 0 ); if (s <= 0 ) { qDebug ("Send request error. %s.\n" , strerror (errno)); return -1 ; } snd += s; } while (snd < size); return 0 ; }
等待接收服务器返回数据并解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 int RTSPData::_wait_response(int to, char resp[], size_t size) { int rc = 0 ; struct pollfd fds; if (rtspSocket < 0 ) { qDebug ("Connection to server has not been set up.\n" ); return -1 ; } memset (&fds, 0 , sizeof (fds)); fds.fd = rtspSocket; fds.events = POLLIN; rc = poll (&fds, 1 , to); if (rc < 0 ) { qDebug ("poll call error. %s.\n" , strerror (errno)); return -1 ; } else if (0 == rc) { qDebug ("Time out.\n" ); } int rcvs = recv (rtspSocket, resp, size, 0 ); qDebug ("Response[%.*s].\n" , rcvs, resp); #define RTSP_SUCESS RTSP_VERSIION" 200 OK\r\n" if (strncmp (resp, RTSP_SUCESS, sizeof (RTSP_SUCESS)-1 ) != 0 ) { qDebug ("Response error.\n" ); return -1 ; } return 0 ; }int RTSPData::_parse_session(const char *resp) { char const * pr = strstr (resp, RTSP_SESSION_NAME); if (NULL == pr) { qDebug ("Not " RTSP_SESSION_NAME " entry.\n" ); return -1 ; } pr += (sizeof (RTSP_SESSION_NAME) - 1 ); while (' ' == *pr || '\t' == *pr) pr++; int w = 0 ; while (*pr >= '0' && *pr <= '9' ) { sessionId[w++] = *pr++; } sessionId[w] = '\0' ; return 0 ; }
session字段就是使用的第一次从服务器接收的数据。有的命令只是将发送的指令部分数据返回表示运行正常,不解析也可以,但是第一次的OPTIONS必须要解析。
此流程是RTSP 的CS握手流程,但是我们不能每次读取数据就进行握手。
我们可以看到,CSeq这个变量会随着通信进行值变化。以此我们作为是否初始化完成、是否需要再次初始化的标志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int RTSPData::isStart () { if (rtspSocket >= 0 && CSeq > 4 ) { return 1 ; } if (rtspSocket >= 0 ) { close (rtspSocket); rtspSocket = -1 ; } rtsp_init (); if (rtspSocket < 0 || CSeq <= 4 ) return 0 ; return 1 ; }
先判断基本流程是否走完,如果走完了就直接返回。如果没有走完就再做一遍。
数据获取流程为:
1 2 3 4 5 6 7 8 9 10 11 while (1 ){ if (!isStart ()){ } read_data (); if (rtsp_packet ()){ }else { } }
RTSP数据包握手完毕后就是视频数据了
Payload是H264,H264在RTP后面,RTP在RTSP后面,RTSP在TCP(推流时使用的是TCP协议)后面。
TCP数据先把数据从socket中读出来,以便后续处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 int RTSPData::rtsp_read () { static uint8_t buff[2048 ]; int rcvs = recv (rtspSocket, buff, sizeof (buff), 0 ); if (rcvs < 0 ) { if (errno != EAGAIN) { close (rtspSocket); rtspSocket = -1 ; } } else if (rcvs > 0 ) { if (rtp_read != rtp_write) { if (rtp_read != 0 ) { memmove (rtp_content, rtp_content+rtp_read, rtp_write - rtp_read); rtp_write -= rtp_read; rtp_read = 0 ; } } else { rtp_read = rtp_write = 0 ; } memcpy (rtp_content+rtp_write, buff, rcvs); rtp_write += rcvs; } return rcvs; }
RTSP数据一个包结构为:
1 2 3 +-------+------+-------+ |RTSP包头|RTP包头|RTP负载| +-------+------+-------+
RTSP包结构为:
1 2 3 +-----+-------+------+ |Magic|Channel|Length| +-----+-------+------+
Magic魔数,一个字节,固定为0x24,如果不是就把此包丢掉 Channel,一个字节取值由RTSP协议中Setup阶段设置的interleaved来决定,默认0-1,0代表后面的是RTP包,1代表RTCP包,本文中所有手动的值都为0x02,所以不看这个值,具体请查看标准文档 Length,两个字节,表示RTP/RTCP数据包的长度 根据此结构,我们构造一个结构体
1 2 3 4 5 6 typedef struct RtspCntHeader { uint8_t magic; uint8_t channel; uint16_t length; uint8_t payload[0 ]; } RtspCntHeader_st;
如果获取的数据小于这个结构体大小,那就表示这个包连包头都不完整,直接丢掉
1 2 if (_rtsp_remaining <= sizeof (RtspCntHeader_st)) return 0 ;
将读取的数据指针强转为RTSP数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 RtspCntHeader_st* rtspH = (RtspCntHeader_st*)(rtp_content+rtp_read);if (0x24 != rtspH->magic) { qDebug ("Magic number error. %02x\n" , rtspH->magic); rtp_read = rtp_write = 0 ; break ; }size_t rtsplen = ntohs (rtspH->length);if (rtsplen > _rtsp_remaining - sizeof (RtspCntHeader_st)) { qDebug ("No enough data. %lu|%lu\n" , rtsplen, _rtsp_remaining); break ; }
然后验证魔数,忽略channel,获取RTP包长度。
RTP数据RTP头为:
1 2 3 4 5 6 7 8 9 10 11 12 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |V=2|P|X| CC |M| PT | sequence number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | timestamp | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | synchronization source (SSRC) identifier | +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ | contributing source (CSRC) identifiers | | .... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
V:2bit,版本号 P:1bit,Padding标记,取值0-1,0表示Payload后面没有填充,1代表Payload 后跟有1个或最多8个字节的填充,如果有填充,RTP包最后一个字节是填充计数器,表示包含自身在内的填充的字节数 X:1bit,扩展标记 CSRC:4bit,Contributing Source identifiers Count,CSRC计数器,特约信源计数器 M:标记,取值0-1,0代表不是一帧的结束,1代表一帧数据的结束,该值是由h264定义的NAL单元传输三个结构中的FU(Fragmentation unit 分片单元) 结构的Header中E结束位决定的 PT:7bit,有效载荷类型,Payload Type Seq:16bit,sequence number序列号,在前包的seq上自增1。如果没有扩展,RTP包除去前面12个字节的报头后,就是Payload,如果有Padding,还要减去后面的填充RTP Payload RTP Payload的结构,是由h264协议定义的,可能会有三种情况: 1.当NAL单元小于MTU时,可以传输一个完整的NAL单元,即Payload就是原始的h264 NAL单元 2.当NAL单元特别小时,可以同时传送两个或多个NAL单元,即组合封包模式 3.当NAL单元大于MTU是,就分两个或多个RTP包发送,即分片封包(Fragmentation Units)传输H.264 以此创建RTP数据包结构体
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 typedef struct RtpCntHeader {#if 0 uint8_t version:2 ; uint8_t padding:1 ; uint8_t externsion:1 ; uint8_t CSrcId:4 ; uint8_t marker:1 ; uint8_t pt:7 ; #else uint8_t exts; uint8_t type;#endif uint16_t seqNo; uint32_t ts; uint32_t SyncSrcId; uint8_t payload[0 ]; } RtpCntHeader_st;
将上面RtspCntHeader_st的payload部分强转为RtpCntHeader_st。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 RtpCntHeader_st* rtpH = (RtpCntHeader_st*)rtspH->payload;if (0x60 != (rtpH->type & 0x7f )) { qDebug ("No video stream %02x.\n" , rtpH->type); rtp_read += (sizeof (RtspCntHeader_st) + rtsplen); continue ; }uint8_t h1 = rtpH->payload[0 ];uint8_t h2 = rtpH->payload[1 ];uint8_t nal = h1 & 0x1f ;uint8_t flag = h2 & 0xe0 ;int paylen = rtsplen - sizeof (RtpCntHeader_st);if (0x1c == nal) { if (0x80 == flag) { packet_buffer[packet_wpos++] = 0 ; packet_buffer[packet_wpos++] = 0 ; packet_buffer[packet_wpos++] = 0 ; packet_buffer[packet_wpos++] = 1 ; packet_buffer[packet_wpos++] = ((h1 & 0xe0 ) | (h2 & 0x1f )); } memcpy (packet_buffer + packet_wpos, &(rtpH->payload[2 ]), paylen - 2 ); packet_wpos += (paylen - 2 ); }else { packet_buffer[packet_wpos++] = 0 ; packet_buffer[packet_wpos++] = 0 ; packet_buffer[packet_wpos++] = 0 ; packet_buffer[packet_wpos++] = 1 ; memcpy (packet_buffer + packet_wpos, rtpH->payload, paylen); packet_wpos += paylen; } rtp_read += (paylen + sizeof (struct RtpCntHeader) + sizeof (struct RtspCntHeader));
至此为止,从RTSP流中解析出了H264数据了。本代码的原来目的是在国产瑞芯微rockchip平台上开发视频硬件解码并显示的程序。代码第一版开发时是由公司提供的硬件平台(现在以已作他用),只需要把H264数据安装rockchip硬件解码的流程填充就可以了,代码没有什么问题,但是Qt/QML在显示多路硬件解码的视频时会出现问题。
完整代码是属于公司客户的项目,只提供RTSP解析部分的内容,硬解部分忽略。
可公开代码在ffmpeg_beginner 中的RTSParser
中。
本文到此结束。
下一篇:FFmpeg入门教程11.03:jetson nano搭建EasyDarwin+FFmpeg+x264+RTSP推流服务器