spydroid-ipcamera源碼分析(七):Rtsp和RtspServer

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)上許多資料和博客,在此不一一列舉厢破,特此感謝荣瑟!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市摩泪,隨后出現(xiàn)的幾起案子笆焰,更是在濱河造成了極大的恐慌,老刑警劉巖见坑,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嚷掠,死亡現(xiàn)場離奇詭異,居然都是意外死亡荞驴,警方通過查閱死者的電腦和手機(jī)不皆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來熊楼,“玉大人霹娄,你說我怎么就攤上這事■昶” “怎么了犬耻?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長执泰。 經(jīng)常有香客問我香追,道長,這世上最難降的妖魔是什么坦胶? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任透典,我火速辦了婚禮晴楔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘峭咒。我一直安慰自己税弃,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布凑队。 她就那樣靜靜地躺著则果,像睡著了一般。 火紅的嫁衣襯著肌膚如雪漩氨。 梳的紋絲不亂的頭發(fā)上西壮,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天,我揣著相機(jī)與錄音叫惊,去河邊找鬼款青。 笑死,一個胖子當(dāng)著我的面吹牛霍狰,可吹牛的內(nèi)容都是我干的抡草。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蔗坯,長吁一口氣:“原來是場噩夢啊……” “哼康震!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起宾濒,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤腿短,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后绘梦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體答姥,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年谚咬,在試婚紗的時候發(fā)現(xiàn)自己被綠了鹦付。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡择卦,死狀恐怖敲长,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情秉继,我是刑警寧澤祈噪,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站尚辑,受9級特大地震影響辑鲤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜杠茬,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一月褥、第九天 我趴在偏房一處隱蔽的房頂上張望弛随。 院中可真熱鬧,春花似錦宁赤、人聲如沸舀透。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽愕够。三九已至,卻和暖如春佛猛,著一層夾襖步出監(jiān)牢的瞬間惑芭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工继找, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留遂跟,地道東北人。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓码荔,卻偏偏與公主長得像漩勤,于是被迫代替她去往敵國和親感挥。 傳聞我的和親對象是個殘疾皇子缩搅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容