技术背景
我们在做GB28181设备接入端的时候,其中有个功能,不难但非常重要:那就是GB28181实时位置的订阅(mobileposition subscribe)和上报(notify)。
特别是执法记录仪、智能安全帽、车载终端等场景下,现场人员的实时位置是国标平台侧非常关注的。国标平台侧通过周期性的获取GB28181设备接入端的经纬度信息,并在电子地图显示,需要看现场的情况,点开图标,进行音视频回传和语音广播语音对讲等操作,现场人员的总体概况一目了然。
规范解读
其他不表,我们先看看GB/T28181-2016规范中,关于订阅通知流程:
基本流程和注解如下:
1.国标服务平台向Android平台GB28181设备接入终端发送SUBSCRIBE消息体,并携带Expire头域指定订阅过期时间;
2.Android平台GB28181设备接入终端收到SUBSCRIBE后,200 OK响应;
3.Android平台GB28181设备接入终端发送 NOTIFY 消息相关的位置信息,并使用Event头域描述订阅事件,国标GB28181的移动设备位置订阅这个值是"presence";
4.国标服务平台收到 Android平台GB28181设备接入终端NOTIFY消息后,200 OK响应;
5.NOTIFY...200 OK...NOTIFY...200 OK...etc..
6.国标服务平台在订阅过期之前,向Android国标接入终端发送刷新订阅 SUBSCRIBE 消息,消息头域中使用 Event头域描述订阅事件,消息体中携带订阅的详细参数,使用 Expire头域指定订阅过期时间;
7.Android平台GB28181设备接入终端收到订阅消息后,向国标服务平台发送200 OK响应;
8.NOTIFY...200 OK...NOTIFY...200 OK........
9.如国标服务平台需要取消订阅,可以向Android平台GB28181设备接入终端发送取消订阅SUBSCRIBE消息,消息头域中使用Event头域描述订阅事件,消息体中携带订阅的详细参数,Expire头域值为0;
10.Android国标接入终端收到订阅消息后,向国标服务平台发送200 OK响应,取消向国标服务平台发送实时位置通知消息,取消订阅成功的话,也会发一个最终的NOTIFY给国标服务端;
11.这里需要注意的是:Android平台GB28181设备接入终端收到SUBSCRIBE请求后,会检查SUBSCRIBE请求中"Expires"值的大小,当且仅当这个值大于0且小于1小时,并且小于Notifier配置的最小值时,Notifier可能会返回一个"423 Interval too small"错误,并包含一个""Min-Expires" 头域;
12.Android国标接入端发送的NOTIFY请求超时的话,应该移除这个订阅;
13.NOTIFY request必须包含"Subscription-State"头,有三个可选的值:"active", "pending", "terminated". 当值是"active"或"pending"时,应该也包含一个”expires“参数,显示订阅剩余时间。
GB/T28181-2016针对MobilePosition描述
<elementname="TargetID"type="tg:deviceIDType"/>移动设备位置数据通知 <! -- 命令类型:移动设备位置数据通知(必选)--> <elementname="CmdType"fixed="MobilePosition"/> <! -- 命令序列号(必选)--> <elementname="SN" type="integer"minInclusivevalue= "1"/> <! -- 产生通知时间(必选)--> <elementname="Time" type="dateTime"/> <! --经度(必选)--> <elementname="Longitude"type="double"/> <! -- 纬度(必选)--> <elementname="Latitude"type="double"/> <! --速度,单位:km/h(可选)--> <elementname="Speed"type="double"/> <!--方向,取值为当前摄像头方向与正北方的顺时针夹角,取值范围0°~360°,单位:(°)(可选)--> <elementname="Direction"type="double"/> <! --海拔高度,单位:m(可选)--> <elementname="Altitude"type="tg:deviceIDType"/>
技术实现
Android平台GB28181设备接入端,启动GB28181后,调用InitGB28181Agent()的时候,添加设备:
相关代码如下:
/* * Camera2Activity.java * Author: daniusdk.com */ 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_.addPlayListener(this); gb28181_agent_.addTalkListener(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_.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.Device gb_device = new com.gb.ntsignalling.Device("34020000001310000001", "安卓测试设备", Build.MANUFACTURER, Build.MODEL, "宇宙","火星1","火星", true); if (mLongitude != null && mLatitude != null) { com.gb.ntsignalling.DevicePosition device_pos = new com.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, "initGB28181Agent gb28181_agent_.createSipStack failed."); return false; } boolean is_bind_local_port_ok = false; // 最多尝试5000个端口 int try_end_port = gb28181_sip_local_port_base_ + 5000; try_end_port = try_end_port > 65536 ?65536: try_end_port; for (int i = 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, "initGB28181Agent gb28181_agent_.bindLocalPort failed."); return false; } if (!gb28181_agent_.initialize()) { gb28181_agent_.unBindLocalPort(); gb28181_agent_.releaseSipStack(); gb28181_agent_ = null; Log.e(TAG, "initGB28181Agent gb28181_agent_.initialize failed."); return false; } return true; }
Android平台GB28181设备接入端DevicePosition设计如下:
/* * DevicePosition.java * Author: daniusdk.com */ public class DevicePosition { private String mTime; // 产生位置信息的时间,格式如:2022-03-16T10:37:21, yyyy-MM-dd'T'HH:mm:ss private String mLongitude; // 经度 private String mLatitude; //纬度 private String mSpeed; // 速度,单位:km/h private String mDirection; // 方向,取值为当前摄像头方向与正北方的顺时针夹角,取值范围0°~360°,单位:(°) private String mAltitude; // 海拔高度,单位:m public String getTime() { return mTime; } public void setTime(String time) { this.mTime = time; } public String getLongitude() { return mLongitude; } public void setLongitude(double longitude) { this.mLongitude = String.valueOf(longitude); } public void setLongitude(String longitude) { this.mLongitude =longitude; } public String getLatitude() { return mLatitude; } public void setLatitude(double latitude) { this.mLatitude = String.valueOf(latitude); } public void setLatitude(String latitude) { this.mLatitude = latitude;} public String getSpeed() { return mSpeed; } public void setSpeed(double speed) { this.mSpeed = String.valueOf(speed); } public String getDirection() { return mDirection; } public void setDirection(double direction) { this.mDirection = String.valueOf(direction); } public String getAltitude() { return mAltitude; } public void setAltitude(double altitude) { this.mAltitude = String.valueOf(altitude); } }
当有SUBSCRIBE request请求位置更新,上层处理如下:
@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.gb.ntsignalling.DevicePosition device_pos = new com.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); } } } 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); }
总结
国标平台侧获取到Android平台GB28181设备接入端的实时位置信息后,可以非常方便的根据实时经纬度信息,把前端设备位置标注到地图服务上。Android平台获取实时经纬度并无难度,这里不再赘述。