Rtsp協(xié)議
實時流協(xié)議(RTSP)是應(yīng)用層協(xié)議,控制實時數(shù)據(jù)的傳送 蝙寨。RTSP提供了一個可擴(kuò)展框架冈爹,使受控、按需傳輸實時數(shù)據(jù)(如音頻與視頻)成為可能逛绵。數(shù)據(jù)源包括現(xiàn)場數(shù)據(jù)與存儲在剪輯中的數(shù)據(jù)怀各。本協(xié)議旨在于控制多個數(shù)據(jù)發(fā)送會話,提供了一種選擇傳送途徑(如UDP术浪、組播UDP與TCP)的方法瓢对,并提供了一種選擇基于RTP (RFC1889)的傳送機(jī)制的方法。
RTSP協(xié)議的服務(wù)端與客戶端建立連接流程:
(1)客戶端發(fā)起RTSP OPTION請求胰苏,目的是得到服務(wù)器提供什么方法硕蛹。RTSP提供的方法一般包括OPTIONS、DESCRIBE硕并、SETUP法焰、TEARDOWN、PLAY倔毙、PAUSE埃仪、SCALE、GET_PARAMETER陕赃。
(2)服務(wù)器對RTSP OPTION回應(yīng)卵蛉,服務(wù)器實現(xiàn)什么方法就回應(yīng)哪些方法。在此系統(tǒng)中么库,我們只對DESCRIBE傻丝、SETUP、TEARDOWN诉儒、PLAY桑滩、PAUSE方法做了實現(xiàn)。
(3)客戶端發(fā)起RTSP DESCRIBE請求允睹,服務(wù)器收到的信息主要有媒體的名字运准,解碼類型,視頻分辨率等描述缭受,目的是為了從服務(wù)器那里得到會話描述信息(SDP)胁澳。
(4)服務(wù)器對RTSP DESCRIBE響應(yīng),發(fā)送必要的媒體參數(shù)米者,在傳輸H.264文件時韭畸,主要包括SPS/PPS宇智、媒體名、傳輸協(xié)議等信息胰丁。
(5)客戶端發(fā)起RTSP SETUP請求随橘,目的是請求會話建立并準(zhǔn)備傳輸。請求信息主要包括傳輸協(xié)議和客戶端端口號锦庸。
(6)服務(wù)器對RTSP SETUP響應(yīng)机蔗,發(fā)出相應(yīng)服務(wù)器端的端口號和會話標(biāo)識符。
(7)客戶端發(fā)出了RTSP PLAY的請求甘萧,目的是請求播放視頻流萝嘁。
(8)服務(wù)器對RTSP PLAY響應(yīng),響應(yīng)的消息包括會話標(biāo)識符扬卷,RTP包的序列號牙言,時間戳。此時服務(wù)器對H264視頻流封裝打包進(jìn)行傳輸怪得。
(9)客戶端發(fā)出RTSP TEARDOWN請求咱枉,目的是關(guān)閉連接,終止傳輸徒恋。
(10)服務(wù)器關(guān)閉連接庞钢,停止傳輸。
RtspServer
RtspServer繼承Android的Server因谎,主要作為后臺服務(wù)提供為傳輸Rtsp協(xié)議數(shù)據(jù)的服務(wù)端Socket基括,監(jiān)聽并處理Rtsp傳輸?shù)倪B接邏輯。
/**
* 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()方法啟動一個線程阻塞監(jiān)聽Socket的連接财岔,stop()方法則循環(huán)遍歷停止和釋放所有客戶端連接(Rtsp協(xié)議支持多個客戶端同時連接)风皿。
while (!Thread.interrupted()) {
try {
new WorkerThread(mServer.accept()).start();
} catch (SocketException e) {
break;
} catch (IOException e) {
Log.e(TAG,e.getMessage());
continue;
}
}
上面是RequestListener內(nèi)部類中截取run()方法中的一段代碼,可以看到這里使用了無限循環(huán)和線程阻塞的方式監(jiān)聽Socket的連接匠璧,只要有客戶端連接就會啟動一個WorkerThread線程來對應(yīng)該客戶端桐款,同時又可以監(jiān)聽到多個客戶端的連接。
while (!Thread.interrupted()) {
request = null;
response = null;
// Parse the request
try {
//讀取和過濾客戶端發(fā)來的數(shù)據(jù)包夷恍。
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 {
//解析請求內(nèi)容
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 {
//發(fā)送返回數(shù)據(jù)給客戶端
response.send(mOutput);
} catch (IOException e) {
Log.e(TAG,"Response was not sent properly");
break;
}
}
上面是WorkerThread內(nèi)部類中截取run()方法中的一段代碼魔眨,主要流程就是讀取客戶端發(fā)來的請求數(shù)據(jù)包,解析并處理數(shù)據(jù)包內(nèi)容酿雪,發(fā)送返回數(shù)據(jù)給客戶端遏暴。
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協(xié)議請求做處理,spydroid-ipcamera項目只對DESCRIBE指黎、OPTIONS朋凉、SETUP、PLAY醋安、PAUSE杂彭、TEARDOWN這幾個請求做回應(yīng)墓毒。在回應(yīng)客戶端的同時,在處理DESCRIBE請求中亲怠,會建立和配置一個Session對象來對應(yīng)這個客戶端所计。而在處理SETUP請求時,會對Session對象進(jìn)行配置并啟動Session的內(nèi)部運(yùn)行流程(流媒體的采集团秽、編碼主胧、傳輸)。
這篇文章我們大致了解了RTSP協(xié)議和它在spydroid-ipcamera項目中的運(yùn)用徙垫,包括Socket連接的創(chuàng)建流程和Rtsp請求的具體處理。
至此放棒,spydroid-ipcamera源碼分析系列文章也完結(jié)了姻报,水平有限,難免會有錯誤的地方间螟,歡迎指出和評論吴旋。這一系列文章參考了網(wǎng)上許多資料和博客,在此不一一列舉厢破,特此感謝荣瑟!