編輯:關於Android編程
本文也主要是一步步分析spydroid源碼。 首先spydroid的采用的協議是RTSP,目前我知道支持RTSP協議的服務器是Darwin,但是Darwin比較復雜,所以大家可以選擇EasyDarwin,大家可以去搜搜看看。還是繼續說spydroid吧,spydroid這個項目大家可以在github上搜到的,不過作者也是很久沒有更新了,如果大家只做推流的話可以看看原作者的另外一個項目Spydroid。
項目包結構
從這個包結構可以看出作者大概的設計,首先是rtsp這個包,這個包裡有一個RtspClient,這裡主要是和服務器建立RTSP會話連接使用的。接著是Session SessionBuilder MediaStream三個類。首先是Session,這個對象保存了本次推流連接所有的音視頻相關信息和資源,包括各種參數等等,SessionBuilder主要用於創建Session。MediaStream是一個父類,它下面有兩個子類VideoStream和AudioStream,如果大家想要擴展音視頻的編碼支持,可以繼承這兩個子類進行改造。具體參照可以查看H264Stream和AACStream兩個類。video和audio兩個包就是具體的音視頻編碼和采集相關的東西;rtp和rtcp則是音視頻打包發送相關的東西;gl包是作者封裝了SurfaceView,這樣可以不用通過攝像頭來直接采集數據,而是從SurfaceView的預覽裡面采集視頻數據;hw包則是處理硬編碼相關的;mp4包是提取視頻的sps和pps信息的。
現在已經對spydroid的項目有了大致的了解,接著我會分析一些重要的類。
首先是Session類,這個類主要有兩個重要成員:AudioStream和VideoStream,通過該類可以初始化音視頻流,停止音視頻推流,以及獲取相關流媒體信息等。在Spydroid的設計中,Session一般不是直接創建的,而是通過SessionBuilder進行創建的。SessionBuilder是一個單例模式的類,通過SessionBuilder我們創建Session對象,AudioStream和VideoStream對象,並且對AudioStream和VideoStream參數進行了初始化設置。代碼如下:
Session mSession = SessionBuilder.getInstance()
.setContext(getApplicationContext())
.setAudioEncoder(SessionBuilder.AUDIO_AAC)//音頻編碼格式
.setAudioQuality(new AudioQuality(8000,16000))//音頻參數 采樣率
.setVideoEncoder(SessionBuilder.VIDEO_H264)//視頻編碼格式
//視頻參數 分辨率1280*720 幀率15 碼率1000*1000
.setVideoQuality(new VideoQuality(1280, 720, 15, 1000*1000))
.setSurfaceView(mSurfaceView)//用於進行預覽展示的SurfaceView
.setPreviewOrientation(0)urfaceView//Camera方向
.setCallback(this)//一些監聽回調
.build();
接下來是RtspClient這個類,這個類主要是負責與流媒體服務器進行RTSP協議會話連接,還是首先來看看相關初始化設置吧,這裡我們首先設定我們推送的地址為:rtsp://192.168.1.115:554/live.sdp。代碼如下:
RtspClient mClient = new RtspClient();
mClient.setSession(mSession);//設置Session
mClient.setCallback(this); //回調監聽
mClient.setServerAddress("192.168.1.115", 554);//服務器的ip和端口號
//這裡算是一個標識符,服務器會在連接後創建一個名為live.sdp的文件,所以這裡的名字一定要唯一。
mClient.setStreamPath("/live.sdp");
mClient.startStream();//開始推流
暫時就這樣吧,下一節具體分析RTSP的會話過程。
前面提到了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是如將音視頻數據發送帶服務器的。
前面已經分析完客戶端和服務器的RTSP會話連接,下面就進入推流階段,也就是客戶端向服務器發送音視頻數據。這裡就暫時只分析視頻了,音頻也是差不多的。
首先是VideoStream類,這個類和AudioStream一樣繼承了MediaStream,然後MediaStream實現了Stream接口。VideoStream也有子類:H264Stream和H263Stream,當然我們如果有其他編碼方式也可以按照這個進行擴展。這裡主要講H264Stream的軟編碼。
發送數據的流程是,首先調用了H264Strem的start方法,在這個方法裡首先執行了config()方法,這個方法主要是獲取視頻的sps和pps信息,並且以分辨率,幀率和碼率為鍵值存儲在sharepreference中,如果下一次參數一樣則直接從sharepreference中取。
接著把sps和pps傳遞給了H264Packetizer對象,這個H264Packetizer是一個用來進行RTP打包的類,暫時就不分析了。接著調用了父類的start方法,然後根據判斷系統能否使用硬編碼來決定視頻的編碼器,這裡我們先分析軟編碼。
在VideoStream的encodeWithMediaRecorder方法中我們看到,首先是創建了Localsocket,這是一個本地的Socket,主要用於系統的MediaRecoder服務接收數據;然後打開了Camera,並設置了視頻采集編碼參數。最後通過H264Packetizer對象進行編碼。
注意:Spydroid的作者使用了很多子線程,很多地方的try catch並沒有做任何處理,所以如果推流失敗的時候,請檢查這些try catch。
本次分析就到此為止了,Spydroid的RTP打包完全可以照搬!
第一步:注冊開發者賬號,—->微信開放平台https://open.weixin.qq.com/第二步:創建一個應用,並通過審核(其中需要填寫項目中的D
偶然間發現了Android.inputmethodservice.Keyboard類,即android可以自定義鍵盤類,做了一個簡單例子供大家參考。效果如下:先看界面布局
以前看了很多人介紹的Android事件派發流程,但最近使用那些來寫代碼的時候出現了不少錯誤。所以回顧一下整個流程,簡單介紹從手觸摸屏幕開始到事件在View樹派發。從源碼上
之前在一篇文章中已經講過了菜單項的創建方法,但是那種方法效率較低,維護不易,現在實現另一種方法創建菜單。MenuInflater,通過此類我們可以輕松的創建菜單項,具體步