1. 程式人生 > >Android視訊推流直播學習【三】

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是如將音視訊資料傳送帶伺服器的。