Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android直播推流學習

Android直播推流學習

編輯:關於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打包完全可以照搬!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved