一、项目需求:
1.从指定的RTSP服务器(可使用live555)下载多媒体文件。
2.需将RTSP服务器返回的RTP数据包解析出来存成文件。
3.存成的文件可以使用VLC的播放器正常播放。
二、所需知识:
1.网络编程异步通讯。
2.RTSP协议交互。
3.RTP协议包的格式(RTCP暂不考虑)。
三、要求:
1.使用C语言实现,不得使用第三方库。
2.源码不可以使用单一文件尽可能按功能模块下函数,存为多个文件。
四、解题思路
仔细分析了题目的要求后,得知,这就是一个socket 网络编程。而最复杂的服务器已经给我们了。我们要做的工作就是编一个客户端程序。顿时心里的压力小了许多。简单的了解到RTSP为应用层协议,用来控制实时数据的传送。然后从协议入手,渐渐的,感觉就上手了,知道该干嘛了。
RTSP协议是一种流媒体控制协议,可以多流媒体进行暂停、快进、快退等操作。本次项目中,RTSP协议编程用来从指定的RTSP服务器(可使用live555)下载多媒体文件,并将其保存为.ts文件。这次的项目目的就是实现下载的功能。简单的说就是给服务器发送信息,并得到服务器的响应。
1、RTSP协议解析
本协议旨在于控制多个数据发送会话,提供了一种选择传送途径(如UDP、组播UDP与TCP)的方法,并提供了一种选择基于RTP (RFC1889)的传送机制的方法。
(1)RTSP(Real Time Streaming Protocol)
实时流协议:一种流媒体控制协议,可对流媒体进行暂停、快进、快倒等操作。而它本身并不传输数据,RTSP的作用相当于流媒体服务器的远程控制。传输数据可以通过传输层的TCP/UDP协议,RTSP也提供了基于RTP传输机制的一些有效的方法。
(2)RTSP消息格式:RTSP的消息有两大类,一是请求消息(request),一是回应消息(response),两种消息的格式不同。一个消息一般由头和内容组成,不过也有很多的消息是只有消息头(message head or header)而没有消息体(message body)的。1)请求消息: 方法 URI RTSP版本 CR LF 消息头 CR LF CR LF 消息体 CR LF 其中方法包括OPTIONS回应中所有的命令,URI是接收方(服务端)的地址,例如:rtsp://192.168.22.136:5000/v0
RTSP版本一般都是RTSP/1.0。每行后面的CR LF表示回车换行,需要接收端有相应的解析,最后一个消息头需要有两个CR LF
一个请求消息(a request message)即可以由客户端向服务端发起也可以由服务端向客户端发起。请求消息的语法结构如下:
Request = Request-Line
*( general-header | request-header | entity-header)
CRLF
[message-body]
①请求消息的第一行的语法结构如下:
Request-Line = Method 空格 URL空格 RTSP-Version CRLF
其中在消息行中出现的第一个单词即是所使用的信令标志。目前已经有的信息标志如下:
Method = “DESCRIBE”
| “ANNOUNCE”
| “GET_PARAMETER”
| “OPTIONS”
| “PAUSE”
| “PLAY”
| “RECORD”
| “REDIRECT”
| “SETUP”
| “SET_PARAMETER”
| “TEARDOWN”
| extension-method
extension-method = 标志
我们可以使用自己定义的信令标示符
Request-URI = “*” | absolute_URI
请使用请求媒体存放的绝对路径 rtsp://192.168.2.102/zhen.ts
RTSP-Version = “RTSP” “/” 1*DIGIT “.” 1*DIGIT
RTSP的版本号
例子:
OPTION rtsp://192.168.2.102/zhen.ts RTSP/1.0
② Request Header Fields
在消息头中除了第一行的内容外,还有一些需求提供附加信息。其中有些是一定要的,后续我们会详细介绍经常用到的几个域的含义。
Request-header = Accept
| Accept-Encoding
| Accept-Language
| Authorization
| From
| If-Modified-Since
| Range
| Referer
| User-Agent
2)响应消息:
客户端或是服务端在接收并解释一个请求消息后,会回复一个消息(response message)给请求方
RTSP版本 状态码 解释 CR LF 消息头 CR LF CR LF 消息体 CR LF 其中RTSP版本一般都是RTSP/1.0,状态码是一个数值,200表示成功,解释是与状态码对应的文本解释
消息格式如下:
Response = Status-Line
*( general-header | response-header|entity-header)
CRLF
[message-body]
[message-body]
① Status-Line
响应消息的第一行是状态行(status-line),每个元素之间用空格分开。除了最后的CRLF之外,在此行的中间不得有CR或是LF的出现。它的语法格式如下,
Status-Line = RTSP-Version 空格 Status-Code 空格 Reason-Phrase CRLF
RTSP-Version = “RTSP” “/” 1*DIGIT “.” 1*DIGIT RTSP的版本号
Status-Code 是一个三位数的整数,用于描述接收方对所收到请求消息的执行结果,而Reason-Phrase是对Status-Code给出一个简短的文字描述,便于我们在收到一个消息后,不用每次都去查看code的解释,而只需要看Reason-Phrase就可以大概了解当前请求的执行状态。
Status-Code的第一位数字指定了这个回复消息的种类,一共有5类:
1XX: Informational – 请求被接收到,继续处理
2XX: Success – 请求被成功的接收,解析并接受
3XX: Redirection – 为完成请求需要更多的操作
4XX: Client Error – 请求消息中包含语法错误或是不能够被有效执行
5XX: Server Error – 服务器响应失败,无法处理正确的有效的请求消息
我们在RTSP编程中,可能遇到的状态码如下
Status-Code = “200” :OK
| “400” :Bad Request
| “404” :Not Found
| “500” Internal Server Error
PS: RTSP状态码:
状态码(Status-Code)由3位数字组成,表示请求是否被理解或被满足。这些状态码的完整定义在第十一章。原因解释(Reason-Phrase)是用简短的文字来描述状态码产生的原因。状态码用来支持自动操作,原因解释用来方便人的查看。客户端不需要检查或显示原因解释。
状态码 = "100" ; 继续
| "200" ; OK
| "201" ; 已创建 录制
| "250" ; 存储空间不足 录制
| "300" ; 有多个选项
| "301" ; 被永久移除
| "302" ; 被临时移除
| "303" ; 见其他
| "305" ; 使用代理
| "400" ; 错误的请求
| "401" ; 未通过认证
| "402" ; 需要付费
| "403" ; 禁止
| "404" ; 没有找到
| "405" ; 不允许该方法
| "406" ; 不接受
| "407" ; 代理需要认证
| "408" ; 请求超时
| "410" ; 不在服务器
| "411" ; 需要长度
| "412" ; 预处理失败 DESCRIBE, SETUP
| "413" ; 请求实体过长
| "414" ; 请求-URI过长
| "415" ; 媒体类型不支持
| "451" ; 不理解此参数 SETUP
| "452" ; 找不到会议 SETUP
| "453" ; 带宽不足 SETUP
| "454" ; 找不到会话
| "455" ; 此状态下此方法无效
| "456" ; 此头部域对该资源无效
| "457" ; 无效范围 PLAY
| "458" ; 参数是只读的 SET_PARAMETER
| "459" ; 不允许合控制
| "460" ; 只允许合控制
| "461" ; 传输方式不支持
| "462" ; 无法到达目的地址
| "500" ; 服务器内部错误
| "501" ; 未实现
| "502" ; 网关错误
| "503" ; 无法得到服务
| "504" ; 网关超时
| "505" ; 不支持此RTSP版本
| "551" ; 不支持选项
| 扩展码
扩展码 = 3位数字
未标识的表示所有适用
原因解释 = *<</span>文本, 包括 CR, LF>
②Response Header Fields
在响应消息的域中存放的是无法放在Status-Line中,而又需要传送给请求者的一些附加信息。
Response-header = Location
| Proxy-Authenticate
| Public
| Retry-After
| Server
| Vary
| WWW-Authenticate
(2)简单的rtsp交互过程:
C表示rtsp客户端,S表示rtsp服务端
第一步:查询服务器端有哪些方法可用
1.C->S:OPTION request //询问S有哪些方法可用
1.S->C:OPTION response //S回应信息中包括提供的所有可用方法
第二步:得到媒体的描述信息
2.C->S:DESCRIBE request //要求得到S提供的媒体初始化描述信息
2.S->C:DESCRIBE response //S回应媒体初始化描述信息,主要是sdp
第三步:建立RTSP会话
3.C->S:SETUP request //设置会话的属性,以及传输模式,提醒S建立会话
3.S->C:SETUP response //S建立会话,返回会话标识符,以及会话相关信息
第四步:请求开始传送数据
4.C->S:PLAY request //C请求播放
4.S->C:PLAY response //S回应该请求的信息
第五步:服务器发送数据,客户端开始接收数据
5.S->C:发送流媒体数据 //服务器开始不断的向客户端发送数据,RTP
第六步:关闭会话退出
6.C->S:TEARDOWN request //C请求关闭会话
6.S->C:TEARDOWN response //S回应该请求
RTSP协议最基本的了解的差不多了,接下去我就在考虑如何拼接要发送给服务器的信息内容。经过一番考虑后。发现用strcpy与sprintf就能够完成这个工作。第一次用到sprintf,发现和fprintf,printf的用法相类似,只不过fprintf是打印在文件中,而printf是打印在屏幕上。考虑到这里,心里便明了了许多。知道该怎么拼接头部信息。
下面从程序上讲讲思路:
1) 创建TCP socket,用来与服务器连接。这个项目中,我是用TCP来连接,用UDP进行数据传输。端口号为554,ip为服务器所在主机的ip
if((sock_fd = socket(AF_INET,SOCK_STREAM,0)) == -1)
{
perror("creat fail\n");
exit(1);
}
bzero(&ser_add,sizeof(ser_add));
ser_add.sin_family = AF_INET;
ser_add.sin_port = htons(PORT);
inet_pton(AF_INET,url_str,&ser_add.sin_addr.s_addr); //初始化TCP的socket
connect_fd = connect(sock_fd,(struct sockaddr *)&ser_add,sizeof(struct sockaddr));
这样连接服务器的socket就建好了。
(2)下面开始给服务器发送消息:
①OPTION方法
strcpy(szcmd,GetOptionCmd(url));//获取发送信息
SendRTSPCmd(sock_fd,"OPTIONS",szcmd);//发送option请求消息
memset(szcmd,0,100);
memset(recv_buf,0,sizeof(recv_buf));
RecvRTSPCmd(sock_fd,recv_buf, nlen,2);//receive option from servier
printf("recv_buf:\n"); //show recv_buf
printf("%s\n",recv_buf);
这里调用的是函数,函数体请见源程序
打印的OPTION请求消息如下
OPTIONS rtsp://192.168.1.102/zhen.ts RTSP/1.0
CSeq:1
User-Agent:rtsp client(v1.0)
OPTION:方法
rtsp://192.168.1.102/zhen.ts:URL
URL:RTSP通过与HTTP相似的方式来定义URL,RTSP完整的URL定义如下: rtsp URL = ( ”rtsp:” | ”rtspu:” | ”rtsps:” ) ”//” host [ ”:” port ] [ abs path ] rtsp = 使用可信的底层传输协议,例如TCP rtspu = 使用不可信的底层传输协议,例如UDP rtsps = 使用可信加密传输协议,例如TCP + TLS
RTSP/1.0:版本号
CSep: Cseq域指定一对RTSP请求-响应消息的序列号。在请求消息及响应消息中一定要指定这个域。对于请求消息,会有一个具有相同Cseq域内容的响应消息与之对应。请求消息与相应消息的CSeq是对应的。
User-Agent;这个域用于用户标识,不同公司或是型号的手机发出的消息中的这个域的内容都不大相同。有时会指出手机的版本号,播放器的型号等等。这里用rtsp client(v1.0)作为用户标识。
服务器响应消息如下:
RTSP/1.0 200 OK
CSeq: 1
Date: Sun, Mar 04 2012 06:16:39 GMT
Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER
RTSP/1.0 版本信息
200 状态码,表示成功,OK
OK: Reason-Phrase 状态码的描述信息,简略的描述状态码的返回原因
CSep: 1,与请求消息中的CSep相对应
Data: 系统时间 Sun, Mar 04 2012 06:16:39 GMT
Public: 返回消息给出的能够使用的方法 这里可以使用的方法有:
OPTIONS , DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, GET_PARAMETER, SET_PARAMETER
②DESCRIBE方法
strcpy(szcmd,GetDescribeCmd(url)); //get send message
SendRTSPCmd(sock_fd, "DESCRIBE", szcmd); //send message of describe
memset(szcmd,0,100);
memset(recv_buf,0,sizeof(recv_buf));
RecvRTSPCmd(sock_fd,recv_buf,nlen,2); //receive from server
printf("recv_buf:\n"); //show recv buf
printf("%s\n",recv_buf);
DESCRIBE请求消息如下
DESCRIBE rtsp://192.168.1.102/zhen.ts RTSP/1.0
CSeq: 2
User-Agent:rtsp client(v1.0)
具体参数参照OPTION
DESCRIBE响应消息中,最主要的就是回应媒体初始化描述信息,主要是sdp
Sdp:信息是文本信息,UTF-8 编码采用 ISO 10646 字符设置。SDP 会话描述如下(标注*符号的表示可选字段):
v= (协议版本)
o= (所有者/创建者和会话标识符)
s= (会话名称)
i=* (会话信息)
u=* (URI 描述)
e=* (Email 地址)
p=* (电话号码)
c=* (连接信息 ― 如果包含在所有媒体中,则不需要该字段)
b=* (带宽信息)
一个或更多时间描述(如下所示):
\ z=* (时间区域调整)
k=* (加密密钥)
a=* (0个或多个会话属性线路)
0个或多个媒体描述(如下所示)
时间描述
t= (会话活动时间)
r=* (0或多次重复次数)
媒体描述
m= (媒体名称和传输地址)
i=* (媒体标题)
c=* (连接信息 — 如果包含在会话层则该字段可选)
b=* (带宽信息)
k=* (加密密钥)
a=* (0个或多个会话属性线路)
DESCRIBE 响应消息如下:
v=0
o=- 1330841352577768 1 IN IP4 192.168.121.1
s=MPEG Transport Stream, streamed by the LIVE555 Media Server //会话名称
i=zhen.ts //媒体标题
t=0 0 //会话活动时间
a=tool:LIVE555 Streaming Media v2011.11.20
a=type:broadcast
a=control:*
a=range:npt=0- //range npt= 0- 表示从头开始播放
a=x-qt-text-nam:MPEG Transport Stream, streamed by the LIVE555 Media Server
a=x-qt-text-inf:zhen.ts
m=video 0 RTP/AVP 33 //媒体名称vedio RTP/AVP 表明传输方式为UDP(缺省)
c=IN IP4 0.0.0.0
b=AS:5000 //带宽信息
a=control:track1
③.SETUP方法
sock_udp_fd1 = InitUDPSocket(port1); //新建两个接收端口
sock_udp_fd2 = InitUDPSocket(port2);
strcpy(szcmd,GetSetupCmd(url,port1,port2)); //get send message
SendRTSPCmd(sock_fd, "SETUP", szcmd); //send message of SETUP
memset(szcmd,0,100);
memset(recv_buf,0,sizeof(recv_buf));
RecvRTSPCmd(sock_fd,recv_buf,nlen,2); //receive from server
memset(session,0,8);
prass_setup_recv(recv_buf,session);//解析响应消息得到session
printf("recv_buf:\n"); //show recv buf
printf("%s\n",recv_buf);
客户端提醒服务器建立会话,并确定传输模式
我所写代码的SETUP方法请求消息如下:
SETUP rtsp://192.168.1.102/zhen.ts RTSP/1.0
CSeq: 3
Transport: RTP/AVP;unicast;client_port=6544-6545
User-Agent:rtsp client(v1.0)
6544:接收RTP包的端口
5545:接收RTCP包的端口
Transport:这个域在请求消息中是标识哪一种传输协议将被使用,并指定一些在描述说明中没有指定的参数的值。使用的传输协议之间用逗号分隔,参数之间用分号分隔
Transport参数设置了传输模式。RTP/AVP/TCP表示通过TCP传输RTP包,RTP/AVP表示使用UDP传输RTP包。unicast表示单播。interleaved值有两个:0和1,0表示RTP包,1表示RTCP包,接收端根据interleaved的值来区别是哪种数据包。client_port值有3008和3009,3008表示客户端接收RTP包的端口,3009表示客户端接收RTCP包的端口,服务端要分别将RTP包和RTCP包发送到这两个端口。如下:
(1)TCP模式SETUP rtsp://192.168.20.136:5000/v0/streamid=0 RTSP/1.0 CSeq: 3Authorization: Basic YWRtaW46YWRtaW4=Transport: RTP/AVP/TCP;unicast;interleaved=0-1 User-Agent: bestilyq
(2)UDP模式SETUP rtsp://192.168.20.136:5000/v0/streamid=0 RTSP/1.0CSeq: 3Transport: RTP/AVP;unicast;client_port=3008-3009Authorization: Basic YWRtaW46YWRtaW4=User-Agent: bestilyq
本项目中,服务器的响应消息如下:
RTSP/1.0 200 OK
CSeq: 1
Date: Sun, Mar 04 2012 08:13:33 GMT
Transport:RTP/AVP;unicast;destination=192.168.1.113;source=192.168.1.102;client_port=6544-6545;server_port=6970-6971
Session: 0CCD8072
Transport解析:
RTP/AVP:此处缺省了UDP,表示是用UDP进行数据传输
destination=192.168.1.113:目标IP,虚拟机中系统IP
source=192.168.1.102:源IP,服务器所在的主机的IP
client_port=6544-6545:客户端的接收端口,6544,接收RTP包,6545接收RTCP包
server_port=6990-6971:服务器的发送端口。
Session:头字段标识了一个RTSP会话。Session ID 是由服务器在SETUP的回应中选择的,客户端一当得到Session ID后,在以后的对Session 的操作请求消息中都要包含Session ID.
解析SETUP的消息得到session,我用了最土的办法。稍后尝试下用strstr函数。
int prass_setup_recv(char *recv_setup_buf,char *pstr)
{
int i = 0;
if(recv_setup_buf == NULL)
{
perror("recv buf is NULL \n");
return -1;
}
while(*recv_setup_buf != '\0')
{
if(*recv_setup_buf == 'o'&& *(recv_setup_buf+1) =='n'&&*(recv_setup_buf+2) == ':'&&*(recv_setup_buf+3) ==' ')
{
memset(pstr,0,8);
recv_setup_buf = recv_setup_buf+4;
for( i = 0 ; i < 8 ;i++)
{
pstr[i] = recv_setup_buf[i];
}
break;
}
else
recv_setup_buf++;
}
return 0;
}
TCP接收数据时与UDP接收数据时,服务器的响应消息对比如下:
(1) TCP模式
RTSP/1.0 200 OK
CSeq: 3
Date: Sat Feb 5 22:35:27 2009 GMT
Session: a522bbb4335617db
Transport: RTP/AVP/TCP;interleaved=0-1
(2)UDP模式
RTSP/1.0 200 OK
CSeq: 3
Date: Sat Feb 5 22:49:39 2009 GMT
Session: 01fa4ca2566a6301 //服务器回应的会话标识符
Transport: RTP/AVP/UDP;unicast;client_port=3008-3009;server_port=1024-1025
④ PLAY方法解析
strcpy(play_buf,"Range: npt=0.000\r\n");
strcpy(szcmd,GetPlayCmd(url,session,play_buf)); //get send message
SendRTSPCmd(sock_fd, "PLAY", szcmd); //send message of PLAY
memset(szcmd,0,100);
memset(recv_buf,0,sizeof(recv_buf));
RecvRTSPCmd(sock_fd,recv_buf, nlen, 2); //recvive from server
printf("recv_buf:\n"); //show recv buf
printf("%s\n",recv_buf);
session:是从SETUP的响应消息中解析得到的。
PLAY请求消息如下:
PLAY rtsp://192.168.1.102/zhen.ts RTSP/1.0
CSeq: 4
Session: 0CCD8072
Range: npt=0.000
User-Agent:rtsp client(v1.0)
Range: Range用于在请求消息和响应消息中指定播放的时间段。Range头可能包含一个时间参数。该参数以UTC格式指定了播放开始的时间。如果在这个指定时间后收到消息,那么播放立即开始。时间参数可能用来帮助同步从不同数据源获取的数据流。
不含Range头的PLAY请求也是合法的。它从媒体流开头开始播放,直到媒体流被暂停。如果媒体流通过PAUSE暂停,媒体流传输将在暂停点(the pause point)重新开始
此处表示从头开始播放。
PLAY 服务器响应消息:
RTSP/1.0 200 OK
CSeq: 4
Date: Sun, Mar 04 2012 09:32:53 GMT
Range: npt=0.000-
Session: 0CCD8072
RTP-Info: url=rtsp://192.168.1.102/zhen.ts/track1;seq=1787;rtptime=4008936136
服务器回应的是一个RTP包
RTP-Info:这个域用于在回复PLAY消息中指定RTP特殊的参数
url: 与设置的RTP参数对应的流媒体链接
seq: 流媒体第一个包的序列号
rtptime: 用于回复range域对应的RTP时间戳
RTP-Info语法结构:
RTP-Info = "RTP-Info" ":" 1#stream-url 1*parameter
stream-url = "url" "=" url
parameter = ";" "seq" "=" 1*DIGIT
| ";" "rtptime" "=" 1*DIGIT
PLAY方法接收到服务器的响应消息后,服务器就开始不停的发送数据给客户端,客户端不断的接收数据。
客户端接收数据的函数如下
int recv_data_from_serv(int sock_udp_fd1,int sock_udp_fd2,char *filename)
{
struct sockaddr_in udp_addr;
struct timeval tm;
int maxfd =0;
int filelen = 0;
int fd = 0;
int i = 0;
int udp_addr_len = 0;
tm.tv_sec = 5;
tm.tv_usec = 0;
fd_set read_fd;
FD_SET(sock_udp_fd1,&read_fd);
FD_SET(sock_udp_fd2,&read_fd);
int select_ret = 0;
if( sock_udp_fd1 >= sock_udp_fd2)//find max fd
{
maxfd = sock_udp_fd1;
}
else
maxfd = sock_udp_fd2;
if((fd = open(filename, O_CREAT|O_WRONLY)) < 0)
{
perror("open zhen.ts error!\n");
return -1;
}
udp_addr_len = sizeof(udp_addr);
while(1)
{
select_ret = select(maxfd+1,&read_fd,NULL,NULL,&tm);
if(select_ret < 0)
{
perror("no such fd\n");
continue;
}
if(select_ret == 0)
{
printf("time out\n");
continue;
}
if( FD_ISSET(sock_udp_fd1,&read_fd))
{
memset(zhen_buf,0,sizeof(zhen_buf));
memset(recv_buf,0,sizeof(recv_buf));
filelen = recvfrom(sock_udp_fd1,recv_buf,sizeof(recv_buf),0,(struct sockaddr*)&udp_addr, &udp_addr_len);
printf("sizoe of recvbuf is %d\n",sizeof(recv_buf));
printf("filelen1:%d\n",filelen);
if(filelen < 12)
{
perror("recv error\n");
continue;
}
filelen = filelen - 12;
write(fd,(zhen_buf+12),filelen);
printf("new filelen:%d\n",filelen);
}
if( FD_ISSET(sock_udp_fd2,&read_fd))
{
memset(zhen_buf,0,sizeof(zhen_buf));
memset(recv_buf,0,sizeof(recv_buf));
filelen = recvfrom(sock_udp_fd2,recv_buf,sizeof(recv_buf),0, (struct sockaddr*)&udp_addr, &udp_addr_len);
printf("sizeof recvbuf is :%d\n",sizeof(recv_buf));
// recv_buf[filelen] = '\0';
if(filelen < 12)
{
perror("recv error\n");
continue;
}
filelen = filelen - 12;
write(fd,(zhen_buf+12),filelen);
}
tm.tv_sec = 5;
tm.tv_usec = 0;
printf("recv num is NO i:%d\n",i++);
}
}