自述
有的时候在嵌入式环境,不能用库去拉取rtsp流,或者编译库比较麻烦,这个时候,就可以研究下rtsp协议和流传,自己拉取流。本文为记录我直接用c++(或c)拉取rtsp over udp 流。
抓包分析
首先需要区别需要拉取的rtsp流是基于udp还是tcp的,基于udp的rtsp就是rtsp的协议部分用tcp沟通,协商好后会定义一个端口,用udp接收rtp包;而tcp的就是全程用一个套接字,我没有研究tcp的。本文只讨论udp的。
安装wireshark抓包软件,过滤器为:ip.src==192.168.85.119 直接抓网络摄像头的ip数据,然后打开网络摄像头的上位机软件,先用软件收流(也可以用ffplay rtsp://192.168.85.119:554/554),这个时候就能在wireshark中抓到数据包,如下如所示:
抓到的上面一部分就为tcp沟通rtsp协议的,下面就是udp接收数据,我们追踪tcp流,结果如下:
OPTIONS rtsp://192.168.85.119:554 RTSP/1.0
CSeq: 1
User-Agent: Lavf58.29.100
RTSP/1.0 200 OK
CSeq: 1
Date: Thu, 01 Jan 1970 01:42:20 GMT
Public: OPTIONS,DESCRIBE,SETUP,PLAY,PAUSE,TEARDOWN
DESCRIBE rtsp://192.168.85.119:554 RTSP/1.0
Accept: application/sdp
CSeq: 2
User-Agent: Lavf58.29.100
RTSP/1.0 200 OK
CSeq: 2
Server: sunshine/1.11
Date: Thu, 01 Jan 1970 01:42:20 GMT
Content-Type: application/sdp
Content-Base: rtsp://192.168.85.119/554/
Content-Length: 337
v=0
o=OnewaveUServerNG 2208994816 2208994816 IN IP4 192.168.85.126
s=Unnamed
i=N/A
c=IN IP4 192.168.85.126
t=0 0
a=recvonly
m=video 5004 RTP/AVP 96
b=RR:0
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42001f;sprop-parameter-sets=Z0IAH5Y1QKALdNwEBAQIAAAAAWjOPIAAAAABBuU=,aM48gAAA;
a=control:trackID=0
SETUP rtsp://192.168.85.119/554/trackID=0 RTSP/1.0
Transport: RTP/AVP/UDP;unicast;client_port=34802-34803
CSeq: 3
User-Agent: Lavf58.29.100
RTSP/1.0 200 OK
CSeq: 3
Server: sunshine/1.11
Date: Thu, 01 Jan 1970 01:42:20 GMT
Session: 141565
Transport: RTP/AVP/UDP;unicast;client_port=34802-34803;destination=192.168.85.126;source=192.168.85.119;server_port=-5004--5003
PLAY rtsp://192.168.85.119/554/ RTSP/1.0
Range: npt=0.000-
CSeq: 4
User-Agent: Lavf58.29.100
Session: 141565
RTSP/1.0 200 OK
CSeq: 4
Server: sunshine/1.11
Date: Thu, 01 Jan 1970 01:42:20 GMT
Session: 141565
这就是tcp沟通的rtsp协议内容,而我们要做的就是把对应的内容,用tcp发给相机。此处不详细介绍,可参考最后代码一起分析。
协议分析
rtsp的协议部分,不同相机可能不同,需要根据各自的情况调整,基本模仿着wireshark抓取客户端的沟通过程数据内容,基本没有问题,接收udp数据流时,端口就在rtsp协议里面Transport: RTP/AVP/UDP;unicast;client_port=34802-34803这是我们发过去的,请求把udp数据发到我们的34802端口,后面那个端口不用管他,应该是给音频的;而相机就会给我们回复Transport: RTP/AVP/UDP;unicast;client_port=34802-34803;destination=192.168.85.126;source=192.168.85.119;server_port=-5004--5003udp数据从相机的5004端口发出,发送到我们的34802端口,这样我们用udp去收取这个端口的数据即可。当然,这个端口我们可以修改的,前提是不能被占用。
收到数据还不算完成,因为这是rtp包,获取完整的一个264包才能送去解码,这时需要研究下rtp包的格式,提取出264码流。我就不详细介绍rtp包格式了,就说下我如何处理。
264码流最重要的就是sps pps I帧,有的相机他会把这些一起打包到rtp包里面,我们只要提取出来数据部分就可以了,但是有的严格按照rtp协议,我们就需要特殊处理了。
截图中data长度为27为sps帧,16的为pps帧,17的为 SEI 补充增强信息,后面的就是I帧。
- sps帧
//sps帧
80 e0 00 19 20 f1 82 02 77 55 a8 c0 67 42 00 1f 96 35 40 a0 0b 74 dc 04 04 04 08
//解析:
//rtp头:80 e0 00 19 20 f1 82 02 77 55 a8 c0 67 42
//data:00 1f 96 35 40 a0 0b 74 dc 04 04 04 08
rtp头最后的67 42就是sps帧的标志。提取data后,在前面补充00 00 00 01 67 42
00 00 00 01 67 42 00 1f 96 35 40 a0 0b 74 dc 04 04 04 08
- pps帧
//pps帧
80 e0 00 1a 20 f1 84 78 77 55 a8 c0 68 ce 3c 80
//解析:
//rtp头:80 e0 00 1a 20 f1 84 78 77 55 a8 c0 68 ce
//data:3c 80
rtp头最后的68 ce就是sps帧的标志。提取data后,在前面补充00 00 00 01 68 ce
00 00 00 01 68 ce 3c 80
- SEI 补充增强信息
//SEI 补充增强信息
80 e0 00 1b 20 f1 87 fc 77 55 a8 c0 06 e5 01 47 80
//rtp头:80 e0 00 1b 20 f1 87 fc 77 55 a8 c0 06 e5
//data:01 47 80
//补全后的SEI 补充增强信息
00 00 00 01 06 e5 01 47 80
- I帧和P帧
而p和I帧的提取就简单很多,只需要判断rtp头的第14位即可:
// 1 是S,Start,说明是分片的第一包
// 0 是E,End,如果是分片的最后一包,设置为1,这里不是
// 0 是R,Remain,保留位,总是0
// 00101 是NAL Type,这里是5,说明是关键帧(不知道为什么是关键帧请自行谷歌)
//I帧
8060001c20f18e507755a8c07c85//最后的0x85,代表I帧的第一个rtp包
80e0004520f18e507755a8c07c45//最后的0x45,代表I帧的最后一个rtp包
//P帧
8060001c20f18e507755a8c07c81//最后的0x81,代表P帧的第一个rtp包
80e0004520f18e507755a8c07c41//最后的0x41,代表P帧的最后一个rtp包
代码实现
此代码仅供参考,我也是从网上摘抄。
void camera_rtsp::open_rtsp(const char *rtsp)
{
char buf[CMD_LEN];
char rbuf[RECV_LEN];
//tcp 用于连接rtsp协议
int sock_fd=socket(AF_INET,SOCK_STREAM,0);
if(sock_fd==-1)
{
printf("create the socket failed.\n");
return;
}
//定义sockaddr_in
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("192.168.85.119");
servaddr.sin_port = htons(554); //服务器端口
if(connect(sock_fd,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)
{
printf("connect the server failed.\n");
close(sock_fd);
return;
}
/***************** OPTIONS *************************/
memset(buf,0,CMD_LEN);
printf("3\n");
sprintf(buf,GetRTSPCmd("OPTIONS"),RTSP_URL,1,"User-Agent: myrtsp");
SendRTSPCmd(sock_fd, "OPTIONS",buf);
ReadSocket(sock_fd,buf,CMD_LEN,10);
/***************** DESCRIBE *************************/
memset(buf,0,CMD_LEN);
sprintf(buf,GetRTSPCmd("DESCRIBE"),RTSP_URL,"Accept: application/sdp", 2, "User-Agent: myrtsp");
SendRTSPCmd(sock_fd, "DESCRIBE", buf);
memset(buf,0,CMD_LEN);
ReadSocket(sock_fd,buf,CMD_LEN,10);
/***************** SETUP *************************/
memset(buf,0,CMD_LEN);
sprintf(buf,
"SETUP rtsp://%s:%d/554\r\n"
"CSeq: 1\r\n"
"Transport: RTP/AVP/UDP;unicast;client_port=57104-57105\r\n\r\n",SERVER_IP,SERVER_PORT);
SendRTSPCmd(sock_fd, "SETUP", buf);
ReadSocket(sock_fd,buf,CMD_LEN,10);
//get the session and server port
char psession[50],pserverport[10];
char* ptr;
memset(psession,0,50);
memset(pserverport,0,10);
ptr=strstr(buf,"Session:");
if(ptr==0)
{
printf("cannot find the session.\n");
return 0;
}
memcpy(psession,ptr+strlen("Session:"),strstr(ptr,"\r\n")-ptr-strlen("Session:"));
printf("Session:%s\n",psession);
ptr=strstr(buf,"server_port=");
memcpy(pserverport,ptr+strlen("server_port="),strstr(ptr,"-")-ptr-strlen("server_port="));
printf("Server port: %s\n",pserverport);
printf("\n");
/***************** PLAY *************************/
memset(buf,0,CMD_LEN);
sprintf(buf,GetRTSPCmd("PLAY"),RTSP_URL,1,psession,"Range: npt=0.000-\r\n","","","");
SendRTSPCmd(sock_fd, "PLAY", buf);
ReadSocket(sock_fd,buf,CMD_LEN,10);
/* 接收数据用udp */
struct sockaddr_in addr_stream;
struct sockaddr_in addr_local;
int recv_fd=socket(AF_INET,SOCK_DGRAM,0);
if(recv_fd==-1)
{
printf("create the stream socket failed.\n");
return 0;
}
addr_local.sin_family=AF_INET;
// addr_local.sin_addr.s_addr=inet_addr("192.168.1.126");
addr_local.sin_addr.s_addr=INADDR_ANY;
//监听本地端口号
addr_local.sin_port=htons(57104);
int len_addr_local=sizeof(addr_local);
if(bind(recv_fd,(struct sockaddr*)&addr_local,len_addr_local)==-1)
{
printf("bind the local stream socket failed.\n");
return 0;
}
addr_stream.sin_family=AF_INET;
addr_stream.sin_addr.s_addr=inet_addr(SERVER_IP);
addr_stream.sin_port=htons(atoi(pserverport));
int len_recv;
int len_addr=sizeof(addr_stream);
int count = 0;
// FILE *save_fd = fopen("/tmp/test.264", "wb");
while(1)
{
memset(rbuf, 0, RECV_LEN);
len_recv = recvfrom(recv_fd, rbuf, RECV_LEN, 0, (struct sockaddr*)&addr_stream, &len_addr);
// printf("len_recv:%d\n", len_recv);
if(len_recv == -1)
{
printf("recvfrom the stream failed.\n");
return 0;
}
char *p = rbuf;
char *data = h264_data;
unsigned char frame_end = rbuf[1] >> 7;
unsigned char frame_start_flag = rbuf[13] & 0x80;
unsigned char frame_end_flag = rbuf[13] & 0x40;
#if 0
memcpy(data + h264_data_len, p + 14, len_recv - 14);
h264_data_len += len_recv - 14;
// printf("rbuf[1]:%x\n", rbuf[1]);
// printf("rbuf[13]:%x\n", rbuf[13]);
// printf("frame_end_flag:%x\n", frame_end_flag);
// 0 是F
// 11 是NRI
// 11100 是FU Type,这里是28,即FU-A
// 1 是S,Start,说明是分片的第一包
// 0 是E,End,如果是分片的最后一包,设置为1,这里不是
// 0 是R,Remain,保留位,总是0
// 00101 是NAL Type,这里是5,说明是关键帧(不知道为什么是关键帧请自行谷歌)
// int w_ret = fwrite(p + 14, len_recv - 14, 1, save_fd);
if(frame_end_flag == 0x40)
{
// printf("h264_data_len:%d\n", h264_data_len);
_data_callback->push_data((uint8_t *)(h264_data), (int16_t)1920, (int16_t)1080, (int32_t)h264_data_len);
int w_ret = fwrite(h264_data, h264_data_len, 1, save_fd);
h264_data_len = 0;
memset(h264_data, 0, 102400);
}
#else
// printf("rbuf[12]:%x rbuf[13]:%x\n", rbuf[12], rbuf[13]);
if((rbuf[12] == 0x7c) && (frame_end_flag == 0x40))//如果是尾就发送
{
memcpy(data + h264_data_len, p + 14, len_recv - 14);
h264_data_len += len_recv - 14;
// printf("pack h264_data_len:%d\n", h264_data_len);
_data_callback->push_data((uint8_t *)(h264_data), (int16_t)1280, (int16_t)720, (int32_t)h264_data_len);
// int w_ret = fwrite(h264_data, h264_data_len, 1, save_fd);
h264_data_len = 0;
memset(h264_data, 0, 102400);
continue;
}
if(rbuf[13] == 0x81) //P帧
{
h264_data[h264_data_len] = 0x00;
h264_data[h264_data_len + 1] = 0x00;
h264_data[h264_data_len + 2] = 0x00;
h264_data[h264_data_len + 3] = 0x01;
h264_data[h264_data_len + 4] = 0x61;
h264_data_len = h264_data_len + 5;
}
if(rbuf[13] == 0x85) //I帧
{
h264_data[h264_data_len] = 0x00;
h264_data[h264_data_len + 1] = 0x00;
h264_data[h264_data_len + 2] = 0x00;
h264_data[h264_data_len + 3] = 0x01;
h264_data[h264_data_len + 4] = 0x65;
h264_data_len = h264_data_len + 5;
}
if((rbuf[12] == 0x67) && (rbuf[13] == 0x42)) //SPS帧
{
h264_data[h264_data_len] = 0x00;
h264_data[h264_data_len + 1] = 0x00;
h264_data[h264_data_len + 2] = 0x00;
h264_data[h264_data_len + 3] = 0x01;
h264_data[h264_data_len + 4] = 0x67;
h264_data[h264_data_len + 5] = 0x42;
h264_data_len = h264_data_len + 6;
}
if((rbuf[12] == 0x68) && (rbuf[13] == 0xce)) //PPS帧
{
h264_data[h264_data_len] = 0x00;
h264_data[h264_data_len + 1] = 0x00;
h264_data[h264_data_len + 2] = 0x00;
h264_data[h264_data_len + 3] = 0x01;
h264_data[h264_data_len + 4] = 0x68;
h264_data[h264_data_len + 5] = 0xce;
h264_data_len = h264_data_len + 6;
}
if((rbuf[12] == 0x06)) //补全后的SEI 补充增强信息
{
h264_data[h264_data_len] = 0x00;
h264_data[h264_data_len + 1] = 0x00;
h264_data[h264_data_len + 2] = 0x00;
h264_data[h264_data_len + 3] = 0x01;
h264_data[h264_data_len + 4] = rbuf[12];
h264_data[h264_data_len + 5] = rbuf[13];
h264_data_len = h264_data_len + 6;
}
memcpy(data + h264_data_len, p + 14, len_recv - 14);
h264_data_len += len_recv - 14;
#endif
}
return;
}
char * camera_rtsp::GetRTSPCmd( const char * szName)
{
char *str = NULL;
char const* cmdFmt = NULL;
if(!strcmp(szName, "OPTIONS"))
{
cmdFmt =
"OPTIONS %s554 RTSP/1.0\r\n"
"CSeq: %d\r\n"
"%s"
"\r\n";
}
else if(!strcmp(szName, "ANNOUNCE"))
{
cmdFmt =
"ANNOUNCE %s RTSP/1.0\r\n"
"CSeq: %d/r/n"
"Content-Type: application/sdp\r\n"
"%s"
"Content-length: %d\r\n\r\n"
"%s";
}
else if(!strcmp(szName, "PLAY"))
{
cmdFmt =
"PLAY %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Session: %s\r\n"
"%s"
"%s"
"%s"
"%s"
"\r\n";
}
else if(!strcmp(szName, "PAUSE"))
{
cmdFmt =
"PAUSE %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Session: %s\r\n"
"%s"
"%s"
"/r/n";
}
else if(!strcmp(szName, "RECORD"))
{
cmdFmt =
"RECORD %s%s%s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Session: %s\r\n"
"Range: npt=0-\r\n"
"%s"
"%s"
"\r\n";
}
else if(!strcmp(szName, "SET_PARAMETER"))
{
cmdFmt =
"SET_PARAMETER %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Session: %s\r\n"
"%s"
"%s"
"Content-length: %d\r\n\r\n"
"%s: %s\r\n";
}
else if(!strcmp(szName, "GET_PARAMETER"))
{
cmdFmt =
"GET_PARAMETER %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Session: %s\r\n"
"%s"
"%s"
"Content-type: text/parameters\r\n"
"Content-length: %d\r\n\r\n"
"%s\r\n";
}
else if(!strcmp(szName, "TEARDOWN"))
{
cmdFmt =
"TEARDOWN %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Session: %s\r\n"
"%s"
"%s"
"\r\n";
}
else if(!strcmp(szName, "DESCRIBE"))
{
cmdFmt =
"DESCRIBE %s554 RTSP/1.0\r\n"
"%s\r\n"
"CSeq: %d\r\n"
"%s\r\n"
"\r\n";
}
else if(!strcmp(szName, "ANNOUNCE"))
{
cmdFmt =
"ANNOUNCE %s RTSP/1.0\r\n"
"CSeq: %d\r\n"
"Content-Type: application/sdp\r\n"
"%s"
"Content-length: %d\r\n\r\n"
"%s";
}
else if(!strcmp(szName, "SETUP"))
{
cmdFmt =
"%s"
"CSeq: %d\r\n"
"%s"
"%s"
"%s"
"%s"
"\r\n";
}
str = (char*)cmdFmt;
return str;
}
long camera_rtsp::ReadSocket(int sock, char *buf, int len, int timeout )
{
long lret ;
lret = recv(sock, buf, len, 0);
if(lret == -1)
{
printf("recv failed.\n");
}
else if(lret > 0)
{
printf("recv:%s\n",buf);
}
else
{
printf("recv 0.\n");
}
return lret;
}
long camera_rtsp::SendRTSPCmd( int sock, const char *cmd, const char *szparam )
{
long lret;
int ilen;
ilen = strlen(szparam);
lret = send(sock, szparam, ilen,0);
// lret = sendto(sock, szparam, ilen,0, (struct sockaddr* )&addr_server, sizeof(addr_server));
if(lret == -1)
{
printf("send %s failed.\n",cmd);
}
else
{
printf("send:%d\n", lret);
printf("send:\n%s\n",szparam);
}
return lret;
}
效果展示
最后附上效果图,在rv1126上收取了网络摄像头的rtsp流,硬件解码、编码为265、发送给上位机显示