Android視訊推流直播學習【三】
前面提到了Spydroid兩個關鍵的類:Session和RtspClient。Session是負責維護流媒體資源的,而RtspClient則是建立RTSP連結的。接下來我們就詳細的分析RtspClient類。
首先RtspClient有一個Parameter的內部類,這個內部類儲存了伺服器ip、埠號、Session物件等資訊。在RtspClient物件建立的時候,首先是建立了一個HandlerThread和Handler物件,Spydroid整個專案用到了很多HandlerThread。大家可以把這個理解成一個執行緒就好了,Handler可以和HandlerThread物件繫結到一起,然後就可以像平時用Handler給主執行緒傳送訊息一樣給這個HandlerThread物件發訊息。實際上,Android應用的主執行緒就是一個HandlerThread。這樣做的好處是方便執行緒之間進行通訊,也方便管理。
建立好RtspClient並且設定好相關引數之後,就開始呼叫startStream()方法進行推流了。我們看到Spydroid是在一個子執行緒中進行的推流的。
第一步是獲取流媒體的sdp資訊,這裡呼叫了syncConfigure()方法。繼續跟蹤下去會發現其實是分別呼叫了AudioStream和VideoStream的configure()方法。這裡就暫時不深入分析,這些方法具體做了什麼。這裡呼叫這個的主要目的是提取編碼器的相關資訊,並組成sdp資訊,用於後面RTSP會話階段使用。
第二步是開始和伺服器進行互動。這裡分為了Announce、Setup、Record三個階段。Announce階段主要是向伺服器傳送客戶端的。
//Announce階段
private void sendRequestAnnounce() throws IllegalStateException, SocketException, IOException {
//body就是sdp資訊
String body = mParameters.session.getSessionDescription();
String request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" +
"CSeq: " + (++mCSeq) + "\r\n" +
"Content-Length: " + body.length() + "\r\n" +
"Content-Type: application/sdp\r\n\r\n" +
body;
Log.i(TAG,request.substring(0, request.indexOf("\r\n")));
mOutputStream.write(request.getBytes ("UTF-8"));
mOutputStream.flush();
//解析伺服器返回的資訊
Response response = Response.parseResponse(mBufferedReader);
if (response.headers.containsKey("server")) {
Log.v(TAG,"RTSP server name:" + response.headers.get("server"));
} else {
Log.v(TAG,"RTSP server name unknown");
}
//獲取伺服器返回的SessionID
if (response.headers.containsKey("session")) {
try {
Matcher m = Response.rexegSession.matcher(response.headers.get("session"));
m.find();
mSessionID = m.group(1);
} catch (Exception e) {
throw new IOException("Invalid response from server. Session id: "+mSessionID);
}
}
//如果伺服器的返回碼是401 說明伺服器需要進行帳號登入授權才可以進行使用
if (response.status == 401) {
String nonce, realm;
Matcher m;
if (mParameters.username == null || mParameters.password == null) throw new IllegalStateException("Authentication is enabled and setCredentials(String,String) was not called !");
try {
m = Response.rexegAuthenticate.matcher(response.headers.get("www-authenticate")); m.find();
nonce = m.group(2);
realm = m.group(1);
} catch (Exception e) {
throw new IOException("Invalid response from server");
}
String uri = "rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path;
String hash1 = computeMd5Hash(mParameters.username+":"+m.group(1)+":"+mParameters.password);
String hash2 = computeMd5Hash("ANNOUNCE"+":"+uri);
String hash3 = computeMd5Hash(hash1+":"+m.group(2)+":"+hash2);
mAuthorization = "Digest username=\""+mParameters.username+"\",realm=\""+realm+"\",nonce=\""+nonce+"\",uri=\""+uri+"\",response=\""+hash3+"\"";
request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" +
"CSeq: " + (++mCSeq) + "\r\n" +
"Content-Length: " + body.length() + "\r\n" +
"Authorization: " + mAuthorization + "\r\n" +
"Session: " + mSessionID + "\r\n" +
"Content-Type: application/sdp\r\n\r\n" +
body;
Log.i(TAG,request.substring(0, request.indexOf("\r\n")));
mOutputStream.write(request.getBytes("UTF-8"));
mOutputStream.flush();
response = Response.parseResponse(mBufferedReader);
if (response.status == 401) throw new RuntimeException("Bad credentials !");
} else if (response.status == 403) {
throw new RuntimeException("Access forbidden !");
}
}
Setup階段,主要就是告訴伺服器音視訊資料是通過udp還是tcp方式進行傳送,如果是udp方式,伺服器會返回udp接收的埠號,tcp的話則是直接使用當前的socket進行資料傳送。這裡需要注意的是,某些RTSP伺服器在Announce階段並不會返回SessionID,可能會在Setup階段返回。所以兩個地方我們都要嘗試獲取伺服器的SessionID,並且下一次向伺服器傳送訊息的時候帶上SessionID。
//Setup階段
private void sendRequestSetup() throws IllegalStateException, SocketException, IOException {
//通過迴圈 分別為音視訊進行setup操作
for (int i=0;i<2;i++) {
Stream stream = mParameters.session.getTrack(i);
if (stream != null) {
String params = mParameters.transport==TRANSPORT_TCP ?
("TCP;interleaved="+2*i+"-"+(2*i+1)) : ("UDP;unicast;client_port="+(5000+2*i)+"-"+(5000+2*i+1)+";mode=receive");
String request = "SETUP rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+"/trackID="+i+" RTSP/1.0\r\n" +
"Transport: RTP/AVP/"+params+"\r\n" +
addHeaders();
//addHeaders()方法主要是在會話裡新增SessionID
Log.i(TAG,request.substring(0, request.indexOf("\r\n")));
mOutputStream.write(request.getBytes("UTF-8"));
mOutputStream.flush();
Response response = Response.parseResponse(mBufferedReader);
Matcher m;
if (response.headers.containsKey("session")) {
try {
m = Response.rexegSession.matcher(response.headers.get("session"));
m.find();
mSessionID = m.group(1);
} catch (Exception e) {
throw new IOException("Invalid response from server. Session id: "+mSessionID);
}
}
//如果是UDP方式傳送音視訊資料包,那麼則要獲取伺服器返回的UDP埠號
if (mParameters.transport == TRANSPORT_UDP) {
try {
m = Response.rexegTransport.matcher(response.headers.get("transport")); m.find();
stream.setDestinationPorts(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)));
Log.d(TAG, "Setting destination ports: "+Integer.parseInt(m.group(3))+", "+Integer.parseInt(m.group(4)));
} catch (Exception e) {
e.printStackTrace();
int[] ports = stream.getDestinationPorts();
Log.d(TAG,"Server did not specify ports, using default ports: "+ports[0]+"-"+ports[1]);
}
} else {
//如果是TCP方式傳送音視訊資料包,那麼則直接使用當前的socket。
stream.setOutputStream(mOutputStream, (byte)(2*i));
}
}
}
}
Record階段沒什麼需要分析的,這個階段我個人理解是通知伺服器準備接收音視訊資料了。
Record階段結束後,客戶端和伺服器的rtsp會話已經建立,接下來就是開始傳送音視訊資料了,後面主要分析視訊資料,音訊資料就暫時不分析了,基本上也是大同小異。
這裡我們注意到在RTSP連線完成後,還有一些程式碼:
if (mParameters.transport == TRANSPORT_UDP) {
mHandler.post(mConnectionMonitor);
}
private Runnable mConnectionMonitor = new Runnable() {
@Override
public void run() {
if (mState == STATE_STARTED) {
try {
// We poll the RTSP server with OPTION requests
sendRequestOption();
mHandler.postDelayed(mConnectionMonitor, 6000);
} catch (IOException e) {
// Happens if the OPTION request fails
postMessage(ERROR_CONNECTION_LOST);
Log.e(TAG, "Connection lost with the server...");
mParameters.session.stop();
mHandler.post(mRetryConnection);
}
}
}
};
這裡,如果音視訊資料包是以UDP方式進行傳送的話,那麼為了維護和伺服器的RTSP會話連結,那麼客戶端必須要隔一段時間向伺服器傳送Option資訊。上面的程式碼主要工作就是這個。
後面,我們會通過ViedeoStream來分析,spydroid是如將音視訊資料傳送帶伺服器的。