在本文開頭,我們先一起回顧下GB/T28181聯網系統通訊協定結構:
聯網系統在進行視音訊傳輸及控制時應建立兩個傳輸通道:對談通道和媒體流通道。
具體如下圖:
我們先來看看對談初始協定:
接下來是對談描述協定
聯網系統有關裝置之間對談建立過程的對談協商和媒體協商應採用IETF RFC 4566協定描述,主要內容包括對談描述、媒體資訊描述、時間資訊描述。
對談協商和媒體協商資訊應採用SIP訊息的訊息體攜帶傳輸。
控制描述協定
聯網系統有關前端裝置控制、報警資訊、裝置目錄資訊等控制命令應採用監控報警聯網系統控制描述協定(MANSCDP)描述。
聯網系統控制命令應採用SIP訊息Message的訊息體攜帶傳輸。
媒體回放控制協定
歷史視音訊的回放控制命令應採用監控報警聯網系統實時流協定(MANSRTSP),實現裝置在端到端之間對視音訊流的正常播放、快速、暫停、停止、隨機拖動播放等遠端控制。
歷史媒體的回放控制命令採用SIP訊息Info的訊息體攜帶傳輸。
由於我們主要側重於GB/T 28181音視訊實時資料接入,這塊未做實現,有相關需求的開發者,參考對應的spec章節即可。
媒體傳輸和媒體編解碼協定
下面以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的驗證,經不住推敲,如果要產品化,開發者還需要很長的路要走。