天天看點

如何快速實作Android平台前端裝置接入能力

技術背景

SIP(會話初始化協定)是在 IP網絡上進行多媒體通信的應用層控制協定,以幾種RFC的形式提供,其中最重要的是包含核心協定規範的RFC3261。該協定用于建立,修改和終止與一個或多個參與者的會話。通過會話,我們了解了一組進行通信的發送方和接收方,以及在通信過程中這些發送方和接收方保持的狀态。會話的示例可以包括Internet電話呼叫,多媒體分發,多媒體會議,分布式計算機遊戲等。

SIP不是通信裝置将需要的唯一協定,也不意味着是通用協定。SIP的目的僅僅是使通信成為可能,通信本身必須通過其他方式(可能還有其他協定)來實作。

與SIP一起最常使用的兩種協定是RTP和SDP。 RTP協定用于承載實時多媒體資料(包括音頻,視訊和文本),該協定可以将資料編碼和拆分為資料包,并通過Internet傳輸此類資料包。

另一個重要的協定是SDP,用于描述和編碼會話參與者的功能。 然後,将這種描述用于協商會話的特征,以便所有裝置都可以參與(例如,包括協商用于編碼媒體的編解碼器,以便所有參與者都可以對其進行解碼,協商使用的傳輸協定 等等)。

GB/T28181-2011 《安全防範視訊監控聯網系統資訊傳輸、交換、控制技術要求》是由公安部科技資訊化局提出,由全國安全防範報警系統标準化技術委員會(SAC/TC100)歸口,公安部一所等多家機關共同起草的一部國家标準。

該标準規定了城市監控報警聯網系統中資訊傳輸、交換、控制的互聯結構、通信協定結構,傳輸、交換、控制的基本要求和安全性要求,以及控制、傳輸流程和協定接口等技術要求。該标準适用于安全防範監控報警聯網系統的方案設計、系統檢測、驗收以及與之相關的裝置研發、生産,其他資訊系統可參考采用。

該标準于2012年6月1日正式釋出實施,在全國範圍内的平安城市項目建設中被普遍推廣應用。GB/T28181-2011标準自釋出以來,受到了各大視訊監控廠商的積極響應。截止2012年底,有近百家視訊監控企業通過公安部一所、公安部三所的認證,如深圳宙視達、浙江宇視、超視科技、東方網力、海康威視、高遠時代、浙江大華、先進視訊、波粒科技、華為技術、中興力維、中星電子、科達、天地偉業等。

相關接口

廢話不多說,直接上設計接口,好多開發者網上看到的大多是非常簡單的接口。極緻簡單,一直是我們追求的目标,但是更好的參數化配置和可擴充的設計,也是一個規範化産品的必經之路。

除了正常的音視訊采集編碼接口外,GB28181裝置接入子產品,主要分信令互動和媒體資料傳輸兩個部分,以大牛直播SDK(​​​官方​​​)分别介紹下相關接口設計。

媒體資料傳輸相關
/*+++++++++++++++RTP Sender相關接口+++++++++++++++*/

  /*
   * 建立RTP Sender執行個體
   *
   * @param reserve:保留參數傳0
   *
   * @return RTP Sender 句柄,0表示失敗
   */
  public native long CreateRTPSender(int reserve);

  /**
   *設定 RTP Sender傳輸協定
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   * @param transport_protocol, 0:UDP, 1:TCP, 預設是UDP
   *
   * @return {0} if successful
   */
  public native int SetRTPSenderTransportProtocol(long rtp_sender_handle, int transport_protocol);

  /**
   *設定 RTP Sender IP位址類型
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   * @param ip_address_type, 0:IPV4, 1:IPV6, 預設是IPV4, 目前僅支援IPV4
   *
   * @return {0} if successful
   */
  public native int SetRTPSenderIPAddressType(long rtp_sender_handle, int ip_address_type);

  /**
   *設定 RTP Sender RTP Socket本地端口
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   * @param port, 必須是偶數,設定0的話SDK會自動配置設定, 預設值是0
   *
   * @return {0} if successful
   */
  public native int SetRTPSenderLocalPort(long rtp_sender_handle, int port);

  /**
   *設定 RTP Sender SS-RC
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   * @param ssrc, 如果設定的話,這個字元串要能轉換成uint32類型, 否則設定失敗
   *
   * @return {0} if successful
   */
  public native int SetRTPSenderSS-RC(long rtp_sender_handle, String ssrc);

  /**
   *設定 RTP Sender RTP socket 發送Buffer大小
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   * @param buffer_size, 必須大于0, 預設是512*1024, 目前僅對UDP socket有效, 根據視訊碼率考慮設定合适的值
   *
   * @return {0} if successful
   */
  public native int SetRTPSenderSocketSendBuffer(long rtp_sender_handle, int buffer_size);

  /**
   *設定 RTP Sender RTP時間戳時鐘頻率
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   * @param clock_rate, 必須大于0, 對于GB28181 PS規定是90kHz, 也就是90000
   *
   * @return {0} if successful
   */
  public native int SetRTPSenderClockRate(long rtp_sender_handle, int clock_rate);

  /**
   *設定 RTP Sender 目的IP位址, 注意目前用在GB2818推送上,隻設定一個位址,将來擴充如果用在其他地方,可能要設定多個目的位址,到時候接口可能會調整
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   * @param address, IP位址
   * @param port, 端口
   *
   * @return {0} if successful
   */
  public native int SetRTPSenderDestination(long rtp_sender_handle, String address, int port);

  /**
   *初始化RTP Sender, 初始化之前先調用上面的接口配置相關參數
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   *
   * @return {0} if successful
   */
  public native int InitRTPSender(long rtp_sender_handle);

  /**
   *擷取RTP Sender RTP Socket本地端口
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   *
   * @return 失敗傳回0, 成功的話傳回響應的端口, 請在InitRTPSender傳回成功之後調用
   */
  public native int GetRTPSenderLocalPort(long rtp_sender_handle);

  /**
   * UnInit RTP Sender
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   *
   * @return {0} if successful
   */
  public native int UnInitRTPSender(long rtp_sender_handle);

  /**
   * 釋放RTP Sender, 釋放之後rtp_sender_handle就無效了,請不要再使用
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   *
   * @return {0} if successful
   */
  public native int DestoryRTPSender(long rtp_sender_handle);


  /*+++++++++++++++RTP Sender相關接口+++++++++++++++*/           

複制

信令相關操作
/*+++++++++++++++GB28181相關接口+++++++++++++++*/

  /**
   * 設定GB28181 RTP Sender
   *
   * @param rtp_sender_handle, CreateRTPSender傳回值
   * @param rtp_payload_type, 對于GB28181 PS, 協定定義是96, 具體以SDP為準
   *
   * @return {0} if successful
   */
  public native int SetGB28181RTPSender(long handle, long rtp_sender_handle, int rtp_payload_type);

  /**
   * 啟動 GB28181 媒體流
   *
   * @return {0} if successful
   */
  public native int StartGB28181MediaStream(long handle);

  /**
   * 停止 GB28181 媒體流
   *
   * @return {0} if successful
   */
  public native int StopGB28181MediaStream(long handle);


  /*---------------GB28181相關接口---------------*/           

複制

啟動、停止GB28181

完成視訊分辨率等參數配置後,點選“啟動GB28181”,即可開始走信令互動流程,裝置端主動發送Register,進入後續互動流程。

如需停止GB28181,點停止即可。

GB28181裝置端主要實作了按需推送資料到平台端。

如何快速實作Android平台前端裝置接入能力
class ButtonGB28181AgentListener implements OnClickListener {
        public void onClick(View v) {
            stopGB28181Stream();
            destoryRTPSender();

            if (null == gb28181_agent_ ) {
                if( !initGB28181Agent() )
                    return;
            }

            if (gb28181_agent_.isRunning()) {
                gb28181_agent_.terminateAllPlays(true);// 目前測試下來,發送BYE之後,有些伺服器會立即發送INVITE,是否發送BYE根據實際情況看
                gb28181_agent_.stop();
                btnGB28181Agent.setText("啟動GB28181");
            }
            else {
                if ( gb28181_agent_.start() ) {
                    btnGB28181Agent.setText("停止GB28181");
                }
            }
        }
    }           

複制

注冊

當Andriod裝置端第一次接入平台端時,裝置端将持續向平台端發送 REGISTER消息,直到 Server端回複"200 OK"代表注冊成功。

如果裝置或系統注冊不成功,宜延遲一定的随機時間後重新注冊。 每隔一定時間用戶端都會再次向伺服器重新整理注冊,防止注冊過期導緻連接配接斷開。

以基本注冊為例:基本注冊采用IETFRFC3261規定的基于數字摘要的挑戰應答式安全技術進行注冊。

注冊流程描述如下:

a) 1:SIP代理向SIP伺服器發送 Register請求;

b) 2:SIP伺服器向 SIP代理發送響應401,并在響應的消息頭 WWW_Authenticate字段中給出适合SIP代理的認證體制和參數;

c) 3:SIP代理重新向SIP伺服器發送 Register請求,在請求的 Authorization字段給出信任書, 包含認證資訊;

d) 4:SIP 伺服器對請求進行驗證,如果檢查出 SIP 代理身份合法,向 SIP 代理發送成功響應 200OK,如果身份不合法則發送拒絕服務應答。

相關注冊回報:

@Override
    public void ntsRegisterOK(String dateString) {
        Log.i(TAG, "ntsRegisterOK Date: " + (dateString!= null? dateString : ""));
    }

    @Override
    public void ntsRegisterTimeout() {
        Log.e(TAG, "ntsRegisterTimeout");
    }

    @Override
    public void ntsRegisterTransportError(String errorInfo) {
        Log.e(TAG, "ntsRegisterTransportError error:" + (errorInfo != null?errorInfo :""));
    }           

複制

資訊查詢(Catalog)

注冊成功後,用戶端與伺服器之間資訊查詢操作,如目錄查詢、曆史錄像檔案檢視等,GB28181使用 SIP擴充協定規定的 Message方法實作。目前使用了兩種類型的查詢指令:Catalog 裝置目錄查詢消息、RecordInfo 曆史錄像檔案查詢消息。

本文以Catalog裝置目錄查詢消息為例:平台端向裝置端發送Catalog請求,裝置端回複200 OK後,發送裝置資訊,平台端回複200 OK,如遇多個裝置資訊,切記分次發送。

private boolean initGB28181Agent()
    {
        if ( gb28181_agent_ != null )
            return  true;

        String local_ip_addr = IPAddrUtils.getIpAddress(myContext);
        Log.i(TAG, "initGB28181Agent local ip addr: " + local_ip_addr);

        if ( local_ip_addr == null || local_ip_addr.isEmpty() ) {
            Log.e(TAG, "initGB28181Agent local ip is empty");
            return  false;
        }

        gb28181_agent_ = GBSIPAgentFactory.getInstance().create();
        if ( gb28181_agent_ == null ) {
            Log.e(TAG, "initGB28181Agent create agent failed");
            return false;
        }

        gb28181_agent_.addListener(this);

        // 必填資訊
        gb28181_agent_.setLocalAddressInfo(local_ip_addr, gb28181_sip_local_port_);
        gb28181_agent_.setServerParameter(gb28181_sip_server_addr_, gb28181_sip_server_port_, gb28181_sip_server_id_, gb28181_sip_server_domain_);
        gb28181_agent_.setUserInfo(gb28181_sip_username_, gb28181_sip_password_);

        // 可選參數
        gb28181_agent_.setUserAgent(gb28181_sip_user_agent_filed_);
        gb28181_agent_.setTransportProtocol(gb28181_sip_trans_protocol_==0?"UDP":"TCP");

        // GB28181配置
        gb28181_agent_.config(gb28181_reg_expired_, gb28181_heartbeat_interval_, gb28181_heartbeat_count_);

        com.gb28181.ntsignalling.Device gb_device = new com.gb28181.ntsignalling.Device("34020000001380000001", "安卓測試裝置", Build.MANUFACTURER, Build.MODEL,
                    "宇宙","火星1","火星", true);

        getLocation(this);
        gb_device.setLongitude(mLongitude);
        gb_device.setLatitude(mLatitude);
        gb28181_agent_.addDevice(gb_device);

        if (!gb28181_agent_.initialize()) {
            gb28181_agent_ = null;
            Log.e(TAG, "initGB28181Agent gb28181_agent_.initialize failed.");
            return  false;
        }

        return true;
    }           

複制

心跳(KeepAlive)

KeepAlive以MESSAGE的形式傳遞,異常處理如下:

@Override
    public void ntsOnHeartBeatException(int exceptionCount,  String lastExceptionInfo) {
        Log.e(TAG, "ntsOnHeartBeatException heart beat timeout count reached, count:" + exceptionCount+
                ", exception info:" + (lastExceptionInfo!=null?lastExceptionInfo:""));

        // 10毫秒後,停止信令, 然後重新開機
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.i(TAG, "gb28281_heart_beart_timeout");
                stopGB28181Stream();
                destoryRTPSender();

                if (gb28181_agent_ != null) {
                    Log.i(TAG, "gb28281_heart_beart_timeout sip stop");
                    gb28181_agent_.stop();

                    Log.i(TAG, "gb28281_heart_beart_timeout sip start");
                    gb28181_agent_.start();
                }
            }

        },10);
    }           

複制

Invite處理

假定整個信令互動流程順利,Android裝置端完成Register、Catalog、KeepAlive消息處理後,平台端發過來Invite請求并攜帶SDP消息體。

Android裝置端可擷取到比如deviceid, tcp/udp傳輸模式、rtp端口,address類型等,并建構200 OK,攜帶相關的音視訊資訊。

@Override
    public void ntsOnInvitePlay(String deviceId, InvitePlaySessionDescription session_des) {
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.i(TAG,"ntsInviteReceived, device_id:" +device_id_+", is_tcp:" + session_des_.isRTPOverTCP()
                        + " rtp_port:" + session_des_.getMediaPort() + " ss-rc:" + session_des_.getSS-RC()
                        + " address_type:" + session_des_.getAddressType() + " address:" + session_des_.getAddress());

                // 可以先給信令伺服器發送臨時振鈴響應
                //sip_stack_android.respondPlayInvite(180, device_id_);

                long rtp_sender_handle = libPublisher.CreateRTPSender(0);
                if ( rtp_sender_handle == 0 ) {
                    gb28181_agent_.respondPlayInvite(488, device_id_);
                    Log.i(TAG, "ntsInviteReceived CreateRTPSender failed, response 488, device_id:" + device_id_);
                    return;
                }

                gb28181_rtp_payload_type_ = session_des_.getPSRtpMapAttribute().getPayloadType();

                libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle, session_des_.isRTPOverUDP()?0:1);
                libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle, session_des_.isIPv4()?0:1);
                libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0);
                libPublisher.SetRTPSenderSS-RC(rtp_sender_handle, session_des_.getSS-RC());
                libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 設定到2M
                libPublisher.SetRTPSenderClockRate(rtp_sender_handle, session_des_.getPSRtpMapAttribute().getClockRate());
                libPublisher.SetRTPSenderDestination(rtp_sender_handle, session_des_.getAddress(), session_des_.getMediaPort());

                if ( libPublisher.InitRTPSender(rtp_sender_handle) != 0 ) {
                    gb28181_agent_.respondPlayInvite(488, device_id_);
                    libPublisher.DestoryRTPSender(rtp_sender_handle);
                    return;
                }

                int local_port = libPublisher.GetRTPSenderLocalPort(rtp_sender_handle);
                if (local_port == 0) {
                    gb28181_agent_.respondPlayInvite(488, device_id_);
                    libPublisher.DestoryRTPSender(rtp_sender_handle);
                    return;
                }

                Log.i(TAG,"get local_port:" + local_port);

                String local_ip_addr = IPAddrUtils.getIpAddress(myContext);
                gb28181_agent_.respondPlayInviteOK(device_id_,local_ip_addr, local_port);

                gb28181_rtp_sender_handle_ = rtp_sender_handle;
            }

            private String device_id_;
            private InvitePlaySessionDescription session_des_;

            public Runnable set(String device_id, InvitePlaySessionDescription session_des) {
                this.device_id_ = device_id;
                this.session_des_ = session_des;
                return this;
            }
        }.set(deviceId, session_des),0);
    }           

複制

Ack确認

平台端發送Ack确認,裝置端收到Ack後,開始發送音視訊資料。

@Override
    public void ntsOnAckPlay(String deviceId) {
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.i(TAG,"ntsOnACKPlay, device_id:" +device_id_);

                if (!isRecording && !isRTSPPublisherRunning && !isPushingRtsp && !isPushingRtmp) {
                    InitAndSetConfig();
                }

                libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_);
                int startRet = libPublisher.StartGB28181MediaStream(publisherHandle);
                if (startRet != 0) {

                    if (!isRecording && !isRTSPPublisherRunning && !isPushingRtmp && !isPushingRtsp) {
                        if (publisherHandle != 0) {
                            libPublisher.SmartPublisherClose(publisherHandle);
                            publisherHandle = 0;
                        }
                    }

                    destoryRTPSender();

                    Log.e(TAG, "Failed to start GB28181 service..");
                    return;
                }

                if (!isRecording && !isRTSPPublisherRunning && !isPushingRtsp && !isPushingRtmp) {
                    if (pushType == 0 || pushType == 1) {
                        CheckInitAudioRecorder();    //enable pure video publisher..
                    }
                }

                isGB28181StreamRunning = true;
            }

            private String device_id_;

            public Runnable set(String device_id) {
                this.device_id_ = device_id;
                return this;
            }

        }.set(deviceId),0);
    }           

複制

BYE

如果用戶端需要斷開Invite會話,則會發送BYE,用戶端傳回200 OK。

@Override
    public void ntsOnByePlay(String deviceId)
    {
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Log.i(TAG, "ntsOnByePlay, stop GB28181 media stream, deviceId=" + device_id_);

                stopGB28181Stream();
                destoryRTPSender();
            }

            private String device_id_;

            public Runnable set(String device_id) {
                this.device_id_ = device_id;
                return this;
            }

        }.set(deviceId),0);
    }           

複制

登出

用戶端向伺服器發送 Register指令消息,消息中的 Expire字段設定為0即是登出。