背景
我们在做GB28181设备接入模块的时候,考虑到好多设备性能一般,我们一般的设计思路是,先注册设备到平台侧,平台侧发calalog过来,获取设备信息,然后,设备侧和国标平台侧维持心跳,如果有位置订阅信息,按照订阅时间间隔,实时上报设备位置信息。
如果本地没有录像诉求,或者,国标平台侧不发起invite请求,Android平台GB28181设备接入端,不做视频编码,甚至可以连摄像头都不打开,等有实时录像或国标平台侧视频预览播放请求的时候,再打开摄像头,毕竟摄像头单纯的打开,设备都有性能损耗,甚至一些中低端记录仪,还没编码就开始发热。
技术实现
本文以大牛直播SDK的Android平台GB28181设备接入侧为例,先启动GB28181,启动后,直接注册到国标平台侧,整体设计架构图如下:
classButtonGB28181AgentListenerimplementsView.OnClickListener { publicvoidonClick(Viewv) { 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"); } } } }
其中,initGB28181Agent()做的工作如下:
privatebooleaninitGB28181Agent() { if ( gb28181_agent_!=null ) returntrue; getLocation(context_); Stringlocal_ip_addr=IPAddrUtils.getIpAddress(context_); Log.i(TAG, "[daniusdk]initGB28181Agent local ip addr: "+local_ip_addr); if ( local_ip_addr==null||local_ip_addr.isEmpty() ) { Log.e(TAG, "[daniusdk]initGB28181Agent local ip is empty"); returnfalse; } gb28181_agent_=GBSIPAgentFactory.getInstance().create(); if ( gb28181_agent_==null ) { Log.e(TAG, "[daniusdk]initGB28181Agent create agent failed"); returnfalse; } gb28181_agent_.addListener(this); gb28181_agent_.addPlayListener(this); gb28181_agent_.addAudioBroadcastListener(this); gb28181_agent_.addDeviceControlListener(this); gb28181_agent_.addQueryCommandListener(this); // 必填信息gb28181_agent_.setLocalAddress(local_ip_addr); 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_.setUserInfo(gb28181_sip_username_, 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.gb.ntsignalling.Devicegb_device=newcom.gb.ntsignalling.Device("34020000001380000001", "安卓测试设备", Build.MANUFACTURER, Build.MODEL, "宇宙","火星1","火星", true); if (mLongitude!=null&&mLatitude!=null) { com.gb.ntsignalling.DevicePositiondevice_pos=newcom.gb.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_.createSipStack()) { gb28181_agent_=null; Log.e(TAG, "[daniusdk]initGB28181Agent gb28181_agent_.createSipStack failed."); returnfalse; } booleanis_bind_local_port_ok=false; // 最多尝试5000个端口inttry_end_port=gb28181_sip_local_port_base_+5000; try_end_port=try_end_port>65536?65536: try_end_port; for (inti=gb28181_sip_local_port_base_; i<try_end_port; ++i) { if (gb28181_agent_.bindLocalPort(i)) { is_bind_local_port_ok=true; break; } } if (!is_bind_local_port_ok) { gb28181_agent_.releaseSipStack(); gb28181_agent_=null; Log.e(TAG, "[daniusdk]initGB28181Agent gb28181_agent_.bindLocalPort failed."); returnfalse; } if (!gb28181_agent_.initialize()) { gb28181_agent_.unBindLocalPort(); gb28181_agent_.releaseSipStack(); gb28181_agent_=null; Log.e(TAG, "[daniusdk]initGB28181Agent gb28181_agent_.initialize failed."); returnfalse; } returntrue; }
注册成功后,会把国标平台侧返回200 OK时带的时间返回上来,便于Android平台GB28181设备侧做校时,如有注册异常,也会返回:
publicvoidntsRegisterOK(StringdateString) { Log.i(TAG, "ntsRegisterOK Date: "+ (dateString!=null?dateString : "")); } publicvoidntsRegisterTimeout() { Log.e(TAG, "ntsRegisterTimeout"); } publicvoidntsRegisterTransportError(StringerrorInfo) { Log.e(TAG, "ntsRegisterTransportError error:"+ (errorInfo!=null?errorInfo :"")); }
周期性的心跳,如有异常,我们也回调到上层:
publicvoidntsOnHeartBeatException(intexceptionCount, StringlastExceptionInfo) { Log.e(TAG, "ntsOnHeartBeatException heart beat timeout count reached, count:"+exceptionCount+", exception info:"+ (lastExceptionInfo!=null?lastExceptionInfo:"")); // 停止信令, 然后重启handler_.postDelayed(newRunnable() { publicvoidrun() { Log.i(TAG, "gb28281_heart_beart_timeout"); stopAudioPlayer(); destoryRTPReceiver(); if (gb_broadcast_source_id_!=null&&gb_broadcast_target_id_!=null&&gb28181_agent_!=null) gb28181_agent_.byeAudioBroadcast(gb_broadcast_source_id_, gb_broadcast_target_id_); gb_broadcast_source_id_=null; gb_broadcast_target_id_=null; btnGB28181AudioBroadcast.setText("GB28181语音广播"); btnGB28181AudioBroadcast.setEnabled(false); stopGB28181Stream(); destoryRTPSender(); if (gb28181_agent_!=null) { gb28181_agent_.terminateAllPlays(true); Log.i(TAG, "gb28281_heart_beart_timeout sip stop"); gb28181_agent_.stop(); Stringlocal_ip_addr=IPAddrUtils.getIpAddress(context_); if (local_ip_addr!=null&&!local_ip_addr.isEmpty() ) { Log.i(TAG, "gb28281_heart_beart_timeout get local ip addr: "+local_ip_addr); gb28181_agent_.setLocalAddress(local_ip_addr); } Log.i(TAG, "gb28281_heart_beart_timeout sip start"); gb28181_agent_.start(); } } },0); }
如果国标平台侧订阅了实时位置信息,我们的处理如下:
publicvoidntsOnDevicePositionRequest(StringdeviceId, intinterval) { handler_.postDelayed(newRunnable() { publicvoidrun() { getLocation(context_); if (mLongitude!=null&&mLatitude!=null) { com.gb.ntsignalling.DevicePositiondevice_pos=newcom.gb.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); } } } privateStringdevice_id_; privateintinterval_; publicRunnableset(Stringdevice_id, intinterval) { this.device_id_=device_id; this.interval_=interval; returnthis; } }.set(deviceId, interval),0); }
如果平台侧发起预览请求,我们的处理如下:
publicvoidntsOnInvitePlay(StringdeviceId, SessionDescriptionsession_des) { handler_.postDelayed(newRunnable() { publicvoidrun() { // 先振铃响应下gb28181_agent_.respondPlayInvite(180, device_id_); MediaSessionDescriptionvideo_des=null; SDPRtpMapAttributeps_rtpmap_attr=null; // 28181 视频使用PS打包Vector<MediaSessionDescription>video_des_list=session_des_.getVideoPSDescriptions(); if (video_des_list!=null&&!video_des_list.isEmpty()) { for(MediaSessionDescriptionm : video_des_list) { if (m!=null&&m.isValidAddressType() &&m.isHasAddress() ) { video_des=m; ps_rtpmap_attr=video_des.getPSRtpMapAttribute(); break; } } } if (null==video_des) { gb28181_agent_.respondPlayInvite(488, device_id_); Log.i(TAG, "ntsOnInvitePlay get video description is null, response 488, device_id:"+device_id_); return; } if (null==ps_rtpmap_attr) { gb28181_agent_.respondPlayInvite(488, device_id_); Log.i(TAG, "ntsOnInvitePlay get ps rtp map attribute is null, response 488, device_id:"+device_id_); return; } longrtp_sender_handle=libPublisher.CreateRTPSender(0); if ( rtp_sender_handle==0 ) { gb28181_agent_.respondPlayInvite(488, device_id_); Log.i(TAG, "ntsOnInvitePlay CreateRTPSender failed, response 488, device_id:"+device_id_); return; } gb28181_rtp_payload_type_=ps_rtpmap_attr.getPayloadType(); gb28181_rtp_encoding_name_=ps_rtpmap_attr.getEncodingName(); 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.SetRTPSenderSocketSendBuffer(rtp_sender_handle, 2*1024*1024); // 设置到2MlibPublisher.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; } intlocal_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); Stringlocal_ip_addr=IPAddrUtils.getIpAddress(context_); MediaSessionDescriptionlocal_video_des=newMediaSessionDescription(video_des.getType()); local_video_des.addFormat(String.valueOf(ps_rtpmap_attr.getPayloadType())); local_video_des.addRtpMapAttribute(ps_rtpmap_attr); local_video_des.setAddressType(video_des.getAddressType()); local_video_des.setAddress(local_ip_addr); local_video_des.setPort(local_port); local_video_des.setTransportProtocol(video_des.getTransportProtocol()); if (!gb28181_agent_.respondPlayInviteOK(device_id_,local_video_des) ) { libPublisher.DestoryRTPSender(rtp_sender_handle); Log.e(TAG, "ntsOnInvitePlay call respondPlayInviteOK failed."); return; } gb28181_rtp_sender_handle_=rtp_sender_handle; } privateStringdevice_id_; privateSessionDescriptionsession_des_; publicRunnableset(Stringdevice_id, SessionDescriptionsession_des) { this.device_id_=device_id; this.session_des_=session_des; returnthis; } }.set(deviceId, session_des),0); }
收到Ack后,才开始真正发送数据:
publicvoidntsOnAckPlay(StringdeviceId) { handler_.postDelayed(newRunnable() { publicvoidrun() { Log.i(TAG,"ntsOnACKPlay, device_id:"+device_id_); if (!isRTSPPublisherRunning&&!isPushingRtmp&&!isRecording) { InitAndSetConfig(); } libPublisher.SetGB28181RTPSender(publisherHandle, gb28181_rtp_sender_handle_, gb28181_rtp_payload_type_, gb28181_rtp_encoding_name_); //libPublisher.SetGBTCPConnectTimeout(publisherHandle, 10*60*1000);//libPublisher.SetGBInitialTCPReconnectInterval(publisherHandle, 1000);//libPublisher.SetGBInitialTCPMaxReconnectAttempts(publisherHandle, 3);intstartRet=libPublisher.StartGB28181MediaStream(publisherHandle); if (startRet!=0) { if (!isRTSPPublisherRunning&&!isPushingRtmp&&!isRecording) { if (publisherHandle!=0) { longhandle=publisherHandle; publisherHandle=0; libPublisher.SmartPublisherClose(handle); } } destoryRTPSender(); Log.e(TAG, "Failed to start GB28181 service.."); return; } if (!isRTSPPublisherRunning&&!isPushingRtmp&&!isRecording) { CheckInitAudioRecorder(); } startLayerPostThread(); isGB28181StreamRunning=true; } privateStringdevice_id_; publicRunnableset(Stringdevice_id) { this.device_id_=device_id; returnthis; } }.set(deviceId),0); }
总结
除此之外,还有语音广播和语音对讲,这里不再赘述,GB28181规范普及之前,要想从外网远程访问局域网内的监控设备非常麻烦,一般要么RTSP转RTMP推到RTMP服务器,此外还要单独构建信令。GB28181规范,让远程、跨网访问监控设备更方便,把GB28181平台部署到外网后,前端设备只要注册到国标服务器,就可以被远程访问、管理和调取视频。但由于设备侧性能并不是非常好,如果要有好的稳定性和性能要求,需尽可能的减少性能消耗,按需打开摄像头、按需编码等。