GB/T 28181聯網系統通訊協定結構和技術實現

2022-09-05 12:01:13

技術回顧

在本文開頭,我們先一起回顧下GB/T28181聯網系統通訊協定結構:

聯網系統在進行視音訊傳輸及控制時應建立兩個傳輸通道:對談通道媒體流通道

  • 對談通道用於在裝置之間建立對談並傳輸系統控制命令;
  • 媒體流通道用於傳輸視音訊資料,經過壓縮編碼的視音訊流採用串流媒體協定 RTP/RTCP傳輸。

具體如下圖:

我們先來看看對談初始協定

  • 安全註冊、實時視音訊點播、歷史視音訊的回放等應用的對談控制採用IETF RFC 3261規定的 Register、Invite等請求和響應方法實現;
  • 歷史視音訊回放控制採用SIP擴充套件協定IETF RFC 2976規定的INFO方法實現;
  • 前端裝置控制、資訊查詢、報警事件通知和分發等應用的對談控制採用 SIP擴充套件協定IETF RFC 3428規定的Message方法實現;
  • SIP訊息應支援基於UDP和 TCP的傳輸;
  • 互聯的系統平臺及裝置不應向對方的SIP埠傳送應用無關訊息,避免應用無關訊息佔用系統平臺及裝置的SIP訊息處理資源。

接下來是對談描述協定

聯網系統有關裝置之間對談建立過程的對談協商和媒體協商應採用IETF RFC 4566協定描述,主要內容包括對談描述、媒體資訊描述、時間資訊描述。

對談協商和媒體協商資訊應採用SIP訊息的訊息體攜帶傳輸。

控制描述協定

聯網系統有關前端裝置控制、報警資訊、裝置目錄資訊等控制命令應採用監控報警聯網系統控制描述協定(MANSCDP)描述。

聯網系統控制命令應採用SIP訊息Message的訊息體攜帶傳輸。

媒體回放控制協定

歷史視音訊的回放控制命令應採用監控報警聯網系統實時流協定(MANSRTSP),實現裝置在端到端之間對視音訊流的正常播放、快速、暫停、停止、隨機拖動播放等遠端控制。

歷史媒體的回放控制命令採用SIP訊息Info的訊息體攜帶傳輸。

由於我們主要側重於GB/T 28181音視訊實時資料接入,這塊未做實現,有相關需求的開發者,參考對應的spec章節即可。

媒體傳輸和媒體編解碼協定

  • 媒體流在聯網系統IP網路上傳輸時應支援 RTP傳輸,媒體流傳送源端應支援控制媒體流傳送峰值功能;
  • RTP的負載應採用如下兩種格式之一:基於 PS封裝的視音訊資料或視音訊基本流資料;
  • 媒體流的傳輸應採用IETF RFC 3550規定的 RTP協定,提供實時資料傳輸中的時間戳資訊及各資料流的同步;應採用IETFRFC3550規定的RTCP協定,為按序傳輸封包提供可靠保證,提供流量控制和擁塞控制。

技術實現

下面以Android平臺GB/T 28181裝置接入實現為例,大概介紹下相關的引數設定和設計細節:

先說引數設定,除了常規的連結sip server基礎設定外,我們根據規範要求,新增了註冊有效期、心跳間隔、心跳間隔失敗次數、信令傳輸協定等設定:

/*** GB28181 相關引數,可以修改相關引數後測試 ***/
    GBSIPAgent     gb28181_agent_             = null;
    private int    gb28181_sip_local_port_    = 12070;
    private String gb28181_sip_server_id_     = "34020000002000000001";
    private String gb28181_sip_domain_        = "3402000000";
    private String gb28181_sip_server_addr_   = "192.168.0.105";
    private int    gb28181_sip_server_port_   = 15060;

    private String gb28181_sip_user_agent_filed_  = "NT GB28181 User Agent V1.2";
    private String gb28181_sip_username_   = "31011500991320000069";
    private String gb28181_sip_password_   = "12345678";

    private int gb28181_reg_expired_           = 3600; // 註冊有效期時間最小3600秒
    private int gb28181_heartbeat_interval_    = 20; // 心跳間隔GB28181預設是60, 目前調整到20秒
    private int gb28181_heartbeat_count_       = 3; // 心跳間隔3次失敗,表示和伺服器斷開了
    private int gb28181_sip_trans_protocol_    = 0; // 0表示信令用UDP傳輸, 1表示信令用TCP傳輸

    private long gb28181_rtp_sender_handle_ = 0;
    private int  gb28181_rtp_payload_type_  = 96;

    private long player_handle_ = 0;
    private long rtp_receiver_handle_ = 0;
    private AtomicLong last_received_audio_data_time_ = new AtomicLong(0);

    /*** GB28181 相關引數,可以修改相關引數後測試 ***/

 

為了測試方便,我們在介面加了個啟動/停止GB28181的按鈕:

/*
 * Github: https://github.com/daniulive/SmarterStreaming
 */
class ButtonGB28181AgentListener implements OnClickListener {
  public void onClick(View v) {
    stopAudioPlayer();
    destoryRTPReceiver();

    gb_broadcast_source_id_ = null;
    gb_broadcast_target_id_ = null;
    btnGB28181AudioBroadcast.setText("GB28181語音廣播");
    btnGB28181AudioBroadcast.setEnabled(false);

    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");
      }
    }
  }
}

 

其中,initGB2818Agent()主要是基礎引數設定,對應的實現如下:

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

  getLocation(context_);

  String local_ip_addr = IPAddrUtils.getIpAddress(context_);
  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_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);

  if (mLongitude != null && mLatitude != null) {
    com.gb28181.ntsignalling.DevicePosition device_pos = new com.gb28181.ntsignalling.DevicePosition();

    device_pos.setTime(mLocationTime);
    device_pos.setLongitude(mLongitude);
    device_pos.setLatitude(mLatitude);
    gb_device.setPosition(device_pos);

    gb_device.setSupportMobilePosition(true); // 設定支援移動位置上報
  }

  gb28181_agent_.addDevice(gb_device);

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

  return true;
}

 

引數設定後,開始傳送Regiter到平臺端,Android裝置端,針對Register的處理如下:

@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不再贅述,我們看看Inite處理:

@Override
public void ntsOnInvitePlay(String deviceId, PlaySessionDescription session_des) {
  handler_.postDelayed(new Runnable() {
    @Override
    public void run() {
      MediaSessionDescription video_des = session_des_.getVideoDescription();
      SDPRtpMapAttribute ps_rtpmap_attr = video_des.getPSRtpMapAttribute();

      Log.i(TAG,"ntsInviteReceived, device_id:" +device_id_+", is_tcp:" + video_des.isRTPOverTCP()
            + " rtp_port:" + video_des.getPort() + " ssrc:" + video_des.getSSRC()
            + " address_type:" + video_des.getAddressType() + " address:" + video_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_ = ps_rtpmap_attr.getPayloadType();

      libPublisher.SetRTPSenderTransportProtocol(rtp_sender_handle, video_des.isRTPOverUDP()?0:1);
      libPublisher.SetRTPSenderIPAddressType(rtp_sender_handle, video_des.isIPv4()?0:1);
      libPublisher.SetRTPSenderLocalPort(rtp_sender_handle, 0);
      libPublisher.SetRTPSenderSSRC(rtp_sender_handle, video_des.getSSRC());
      libPublisher.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 設定到2M
      libPublisher.SetRTPSenderClockRate(rtp_sender_handle, ps_rtpmap_attr.getClockRate());
      libPublisher.SetRTPSenderDestination(rtp_sender_handle, video_des.getAddress(), video_des.getPort());

      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(context_);
      gb28181_agent_.respondPlayInviteOK(device_id_,local_ip_addr, local_port);

      gb28181_rtp_sender_handle_ = rtp_sender_handle;
    }

    private String device_id_;
    private PlaySessionDescription session_des_;

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

Ack後,開始傳送打包後的ps資料:

@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 && !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 ) {
          if (publisherHandle != 0) {
            libPublisher.SmartPublisherClose(publisherHandle);
            publisherHandle = 0;
          }
        }

        destoryRTPSender();

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

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

      startLayerPostThread();

      isGB28181StreamRunning = true;
    }

    private String device_id_;

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

  }.set(deviceId),0);
}

 

再看看位置訂閱處理:

@Override
    public void ntsOnDevicePositionRequest(String deviceId, int interval) {
        handler_.postDelayed(new Runnable() {
            @Override
            public void run() {
                getLocation(context_);

                Log.v(TAG, "ntsOnDevicePositionRequest, deviceId:" + this.device_id_ + ", Longitude:" + mLongitude
                        + ", Latitude:" + mLatitude + ", Time:" + mLocationTime);


                if (mLongitude != null && mLatitude != null) {
                    com.gb28181.ntsignalling.DevicePosition device_pos = new com.gb28181.ntsignalling.DevicePosition();

                    device_pos.setTime(mLocationTime);
                    device_pos.setLongitude(mLongitude);
                    device_pos.setLatitude(mLatitude);

                    if (gb28181_agent_ != null ) {
                        gb28181_agent_.updateDevicePosition(device_id_, device_pos);
                    }
                }
            }

            private String device_id_;
            private int interval_;

            public Runnable set(String device_id, int interval) {
                this.device_id_ = device_id;
                this.interval_ = interval;
                return this;
            }

        }.set(deviceId, interval),0);
    }

 

語音廣播和語音對講處理:

@Override
public void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID) {
  handler_.postDelayed(new Runnable() {
    @Override
    public void run() {
      Log.i(TAG, "ntsOnNotifyBroadcastCommand, fromUserName:"+ from_user_name_ + ", fromUserNameAtDomain:"+ from_user_name_at_domain_
            + ", SN:" + sn_ + ", sourceID:" + source_id_ + ", targetID:" + target_id_);

      if (gb28181_agent_ != null ) {
        gb28181_agent_.respondBroadcastCommand(from_user_name_, from_user_name_at_domain_,sn_,source_id_, target_id_, true);
        btnGB28181AudioBroadcast.setText("收到GB28181語音廣播通知");
      }
    }

    private String from_user_name_;
    private String from_user_name_at_domain_;
    private String sn_;
    private String source_id_;
    private String target_id_;

    public Runnable set(String from_user_name, String from_user_name_at_domain, String sn, String source_id, String target_id) {
      this.from_user_name_ = from_user_name;
      this.from_user_name_at_domain_ = from_user_name_at_domain;
      this.sn_ = sn;
      this.source_id_ = source_id;
      this.target_id_ = target_id;
      return this;
    }

  }.set(fromUserName, fromUserNameAtDomain, sn, sourceID, targetID),0);
}

@Override
public void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID) {
  handler_.postDelayed(new Runnable() {
    @Override
    public void run() {
      Log.i(TAG, "ntsOnAudioBroadcastPlay, fromFromUserName:" + command_from_user_name_
            + " FromUserNameAtDomain:" + command_from_user_name_at_domain_
            + " sourceID:" + source_id_ + ", targetID:" + target_id_);

      stopAudioPlayer();
      destoryRTPReceiver();

      if (gb28181_agent_ != null ) {
        String local_ip_addr = IPAddrUtils.getIpAddress(context_);

        boolean is_tcp = true; // 考慮到跨網段, 預設用TCP傳輸rtp包
        rtp_receiver_handle_ = lib_player_.CreateRTPReceiver(0);
        if (rtp_receiver_handle_ != 0 ) {
          lib_player_.SetRTPReceiverTransportProtocol(rtp_receiver_handle_, is_tcp?1:0);
          lib_player_.SetRTPReceiverIPAddressType(rtp_receiver_handle_, 0);

          if (0 == lib_player_.CreateRTPReceiverSession(rtp_receiver_handle_, 0) ) {
            int local_port = lib_player_.GetRTPReceiverLocalPort(rtp_receiver_handle_);
            boolean ret = gb28181_agent_.inviteAudioBroadcast(command_from_user_name_,command_from_user_name_at_domain_,
                                                              source_id_, target_id_, "IP4", local_ip_addr, local_port, is_tcp?"TCP/RTP/AVP":"RTP/AVP");

            if (!ret ) {
              destoryRTPReceiver();
              btnGB28181AudioBroadcast.setText("GB28181語音廣播");
            }
            else {
              btnGB28181AudioBroadcast.setText("GB28181語音廣播呼叫中");
            }
          } else {
            destoryRTPReceiver();
            btnGB28181AudioBroadcast.setText("GB28181語音廣播");
          }
        }
      }
    }

    private String command_from_user_name_;
    private String command_from_user_name_at_domain_;
    private String source_id_;
    private String target_id_;

    public Runnable set(String command_from_user_name, String command_from_user_name_at_domain, String source_id, String target_id) {
      this.command_from_user_name_ = command_from_user_name;
      this.command_from_user_name_at_domain_ = command_from_user_name_at_domain;
      this.source_id_ = source_id;
      this.target_id_ = target_id;
      return this;
    }

  }.set(commandFromUserName, commandFromUserNameAtDomain, sourceID, targetID),0);
}

 

Bye處理如下:

@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);
}

考慮到篇幅有限,上面僅展示基礎的處理,總的來說,GB/T 28181接入,資料相對全面,但是好多都是基於demo的驗證,經不住推敲,如果要產品化,開發者還需要很長的路要走。