Rtsp协议
实时流协议(RTSP)是应用层协议,控制实时数据的传送 。RTSP提供了一个可扩展框架,使受控、按需传输实时数据(如音频与视频)成为可能。数据源包括现场数据与存储在剪辑中的数据。本协议旨在于控制多个数据发送会话,提供了一种选择传送途径(如UDP、组播UDP与TCP)的方法,并提供了一种选择基于RTP (RFC1889)的传送机制的方法。
RTSP协议的服务端与客户端建立连接流程:
(1)客户端发起RTSP OPTION请求,目的是得到服务器提供什么方法。RTSP提供的方法一般包括OPTIONS、DESCRIBE、SETUP、TEARDOWN、PLAY、PAUSE、SCALE、GET_PARAMETER。
(2)服务器对RTSP OPTION回应,服务器实现什么方法就回应哪些方法。在此系统中,我们只对DESCRIBE、SETUP、TEARDOWN、PLAY、PAUSE方法做了实现。
(3)客户端发起RTSP DESCRIBE请求,服务器收到的信息主要有媒体的名字,解码类型,视频分辨率等描述,目的是为了从服务器那里得到会话描述信息(SDP)。
(4)服务器对RTSP DESCRIBE响应,发送必要的媒体参数,在传输H.264文件时,主要包括SPS/PPS、媒体名、传输协议等信息。
(5)客户端发起RTSP SETUP请求,目的是请求会话建立并准备传输。请求信息主要包括传输协议和客户端端口号。
(6)服务器对RTSP SETUP响应,发出相应服务器端的端口号和会话标识符。
(7)客户端发出了RTSP PLAY的请求,目的是请求播放视频流。
(8)服务器对RTSP PLAY响应,响应的消息包括会话标识符,RTP包的序列号,时间戳。此时服务器对H264视频流封装打包进行传输。
(9)客户端发出RTSP TEARDOWN请求,目的是关闭连接,终止传输。
(10)服务器关闭连接,停止传输。
RtspServer
RtspServer继承Android的Server,主要作为后台服务提供为传输Rtsp协议数据的服务端Socket,监听并处理Rtsp传输的连接逻辑。
/**
* Starts (or restart if needed, if for example the configuration
* of the server has been modified) the RTSP server.
*/
public void start() {
if (!mEnabled || mRestart) stop();
if (mEnabled && mListenerThread == null) {
try {
mListenerThread = new RequestListener();
} catch (Exception e) {
mListenerThread = null;
}
}
mRestart = false;
}
/**
* Stops the RTSP server but not the Android Service.
* To stop the Android Service you need to call {@link android.content.Context#stopService(Intent)};
*/
public void stop() {
if (mListenerThread != null) {
try {
mListenerThread.kill();
for ( Session session : mSessions.keySet() ) {
if ( session != null ) {
if (session.isStreaming()) session.stop();
}
}
} catch (Exception e) {
} finally {
mListenerThread = null;
}
}
}
start()方法启动一个线程阻塞监听Socket的连接,stop()方法则循环遍历停止和释放所有客户端连接(Rtsp协议支持多个客户端同时连接)。
while (!Thread.interrupted()) {
try {
new WorkerThread(mServer.accept()).start();
} catch (SocketException e) {
break;
} catch (IOException e) {
Log.e(TAG,e.getMessage());
continue;
}
}
上面是RequestListener内部类中截取run()方法中的一段代码,可以看到这里使用了无限循环和线程阻塞的方式监听Socket的连接,只要有客户端连接就会启动一个WorkerThread线程来对应该客户端,同时又可以监听到多个客户端的连接。
while (!Thread.interrupted()) {
request = null;
response = null;
// Parse the request
try {
//读取和过滤客户端发来的数据包。
request = Request.parseRequest(mInput);
} catch (SocketException e) {
// Client has left
break;
} catch (Exception e) {
// We don't understand the request :/
response = new Response();
response.status = Response.STATUS_BAD_REQUEST;
}
// Do something accordingly like starting the streams, sending a session description
if (request != null) {
try {
//解析请求内容
response = processRequest(request);
}
catch (Exception e) {
// This alerts the main thread that something has gone wrong in this thread
postError(e, ERROR_START_FAILED);
Log.e(TAG,e.getMessage()!=null?e.getMessage():"Aerror occurred");
e.printStackTrace();
response = new Response(request);
}
}
// We always send a response
// The client will receive an "INTERNAL SERVER ERROR" if an exception has been thrown at some point
try {
//发送返回数据给客户端
response.send(mOutput);
} catch (IOException e) {
Log.e(TAG,"Response was not sent properly");
break;
}
}
上面是WorkerThread内部类中截取run()方法中的一段代码,主要流程就是读取客户端发来的请求数据包,解析并处理数据包内容,发送返回数据给客户端。
public Response processRequest(Request request) throws IllegalStateException, IOException {
Response response = new Response(request);
/* ********************************************************************************** */
/* ********************************* Method DESCRIBE ******************************** */
/* ********************************************************************************** */
if (request.method.equalsIgnoreCase("DESCRIBE")) {
// Parse the requested URI and configure the session
mSession = handleRequest(request.uri, mClient);
mSessions.put(mSession, null);
mSession.syncConfigure();
String requestContent = mSession.getSessionDescription();
String requestAttributes =
"Content-Base: "+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/\r\n" +
"Content-Type: application/sdp\r\n";
response.attributes = requestAttributes;
response.content = requestContent;
// If no exception has been thrown, we reply with OK
response.status = Response.STATUS_OK;
}
/* ********************************************************************************** */
/* ********************************* Method OPTIONS ********************************* */
/* ********************************************************************************** */
else if (request.method.equalsIgnoreCase("OPTIONS")) {
response.status = Response.STATUS_OK;
response.attributes = "Public: DESCRIBE,SETUP,TEARDOWN,PLAY,PAUSE\r\n";
response.status = Response.STATUS_OK;
}
/* ********************************************************************************** */
/* ********************************** Method SETUP ********************************** */
/* ********************************************************************************** */
else if (request.method.equalsIgnoreCase("SETUP")) {
Pattern p; Matcher m;
int p2, p1, ssrc, trackId, src[];
String destination;
p = Pattern.compile("trackID=(\\w+)",Pattern.CASE_INSENSITIVE);
m = p.matcher(request.uri);
if (!m.find()) {
response.status = Response.STATUS_BAD_REQUEST;
return response;
}
trackId = Integer.parseInt(m.group(1));
if (!mSession.trackExists(trackId)) {
response.status = Response.STATUS_NOT_FOUND;
return response;
}
p = Pattern.compile("client_port=(\\d+)-(\\d+)",Pattern.CASE_INSENSITIVE);
m = p.matcher(request.headers.get("transport"));
if (!m.find()) {
int[] ports = mSession.getTrack(trackId).getDestinationPorts();
p1 = ports[0];
p2 = ports[1];
}
else {
p1 = Integer.parseInt(m.group(1));
p2 = Integer.parseInt(m.group(2));
}
ssrc = mSession.getTrack(trackId).getSSRC();
src = mSession.getTrack(trackId).getLocalPorts();
destination = mSession.getDestination();
mSession.getTrack(trackId).setDestinationPorts(p1, p2);
boolean streaming = isStreaming();
mSession.syncStart(trackId);
if (!streaming && isStreaming()) {
postMessage(MESSAGE_STREAMING_STARTED);
}
response.attributes = "Transport: RTP/AVP/UDP;"+(InetAddress.getByName(destination).isMulticastAddress()?"multicast":"unicast")+
";destination="+mSession.getDestination()+
";client_port="+p1+"-"+p2+
";server_port="+src[0]+"-"+src[1]+
";ssrc="+Integer.toHexString(ssrc)+
";mode=play\r\n" +
"Session: "+ "1185d20035702ca" + "\r\n" +
"Cache-Control: no-cache\r\n";
response.status = Response.STATUS_OK;
// If no exception has been thrown, we reply with OK
response.status = Response.STATUS_OK;
}
/* ********************************************************************************** */
/* ********************************** Method PLAY *********************************** */
/* ********************************************************************************** */
else if (request.method.equalsIgnoreCase("PLAY")) {
String requestAttributes = "RTP-Info: ";
if (mSession.trackExists(0)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+0+";seq=0,";
if (mSession.trackExists(1)) requestAttributes += "url=rtsp://"+mClient.getLocalAddress().getHostAddress()+":"+mClient.getLocalPort()+"/trackID="+1+";seq=0,";
requestAttributes = requestAttributes.substring(0, requestAttributes.length()-1) + "\r\nSession: 1185d20035702ca\r\n";
response.attributes = requestAttributes;
// If no exception has been thrown, we reply with OK
response.status = Response.STATUS_OK;
}
/* ********************************************************************************** */
/* ********************************** Method PAUSE ********************************** */
/* ********************************************************************************** */
else if (request.method.equalsIgnoreCase("PAUSE")) {
response.status = Response.STATUS_OK;
}
/* ********************************************************************************** */
/* ********************************* Method TEARDOWN ******************************** */
/* ********************************************************************************** */
else if (request.method.equalsIgnoreCase("TEARDOWN")) {
response.status = Response.STATUS_OK;
}
/* ********************************************************************************** */
/* ********************************* Unknown method ? ******************************* */
/* ********************************************************************************** */
else {
Log.e(TAG,"Command unknown: "+request);
response.status = Response.STATUS_BAD_REQUEST;
}
return response;
}
processRequest(Request request)方法就是对Rtsp协议请求做处理,spydroid-ipcamera项目只对DESCRIBE、OPTIONS、SETUP、PLAY、PAUSE、TEARDOWN这几个请求做回应。在回应客户端的同时,在处理DESCRIBE请求中,会建立和配置一个Session对象来对应这个客户端。而在处理SETUP请求时,会对Session对象进行配置并启动Session的内部运行流程(流媒体的采集、编码、传输)。
这篇文章我们大致了解了RTSP协议和它在spydroid-ipcamera项目中的运用,包括Socket连接的创建流程和Rtsp请求的具体处理。
至此,spydroid-ipcamera源码分析系列文章也完结了,水平有限,难免会有错误的地方,欢迎指出和评论。这一系列文章参考了网上许多资料和博客,在此不一一列举,特此感谢!