技术背景
我们在做Android平台GB28181设备接入侧模块的时候,特别是执法记录仪或类似场景,系统除了对常规的录像有要求,还需要能和GB28181平台侧交互,比如实现设备侧视音频文件检索、下载或回放。本文假定记录仪或相关设备已经完成录像,主要来探讨下设备视音频文件检索相关。
规范解读
先回顾下GB/T28181-2016视音频文件检索基本要求:
文件检索主要用区域、设备、录像时间段、录像地点、录像内容为条件进行查询,用 Message 消息发送检索请求和返回查询结果,传送结果的 Message 消息可以发送多条,应支持附录 N 多响应消息传输的要求。文件检索请求和应答命令采用 MANSCDP 协议格式定义。
命令流程:
信令流程描述如下:
- 目录检索方向目录拥有方发送目录查询请求 Message 消息,消息体中包含视音频文件检索条件;
- 目录拥有方向目录检索方发送 200 OK,无消息体;
- 目录拥有方向目录检索方发送查询结果,消息体中含文件目录,当一条 Message 消息无法传送完所有查询结果时,采用多条消息传送;
- 目录检索方向目录拥有方发送 200 OK,无消息体。
无查询结果的示例如下:
<?xml version="1.0"encoding="GB2312"?> <Query> <CmdType>RecordInfo</CmdType> <SN>405331641</SN> <DeviceID>34020000001380000001</DeviceID> <StartTime>2023-09-04T00:00:00</StartTime> <EndTime>2023-09-04T06:00:00</EndTime> <Type>all</Type> </Query>
没查到录像,那么设备侧回复如下,没有查询到文件的话,<SumNum>元素内容填充"0", 且不携带<RecordList>元素:
<?xml version="1.0"encoding="GB2312"?> <Response> <CmdType>RecordInfo</CmdType> <SN>405331641</SN> <DeviceID>34020000001380000001</DeviceID> <Name>DaniuSDK</Name> <SumNum>0</SumNum> </Response>
有查询结果:
<Query> <CmdType>RecordInfo</CmdType> <SN>68331900</SN> <DeviceID>34020000001380000001</DeviceID> <StartTime>2023-09-04T06:00:00</StartTime> <EndTime>2023-09-04T12:00:00</EndTime> <Type>all</Type> </Query>
设备侧回复如下:
<Response> <CmdType>RecordInfo</CmdType> <SN>68331900</SN> <DeviceID>34020000001380000001</DeviceID> <Name>DaniuSDK</Name> <SumNum>6</SumNum> <RecordList Num="3"> <Item> <DeviceID>34020000001380000001</DeviceID> <Name>DaniuSDK</Name> <StartTime>2023-09-04T10:11:56</StartTime> <EndTime>2023-09-04T10:12:58</EndTime> <Secrecy>0</Secrecy> </Item> <Item> <DeviceID>34020000001380000001</DeviceID> <Name>DaniuSDK</Name> <StartTime>2023-09-04T10:13:07</StartTime> <EndTime>2023-09-04T10:15:33</EndTime> <Secrecy>0</Secrecy> </Item> <Item> <DeviceID>34020000001380000001</DeviceID> <Name>DaniuSDK</Name> <StartTime>2023-09-04T10:15:37</StartTime> <EndTime>2023-09-04T10:16:32</EndTime> <Secrecy>0</Secrecy> </Item> </RecordList> </Response>
需要注意的是,会话外的SIP MESSAGE请求大小不能超过1300个字节。
技术实现
以大牛直播SDK的Android平台GB28181设备接入侧为例,设计接口逻辑如下:
packagecom.gb.ntsignalling; publicinterfaceGBSIPAgent { voidaddListener(GBSIPAgentListenerlistener); voidaddPlayListener(GBSIPAgentPlayListenerplayListener); voidremovePlayListener(GBSIPAgentPlayListenerplayListener); voidaddDownloadListener(GBSIPAgentDownloadListenerdownloadListener); voidremoveDownloadListener(GBSIPAgentDownloadListenerremoveListener); voidaddTalkListener(GBSIPAgentTalkListenertalkListener); voidremoveTalkListener(GBSIPAgentTalkListenertalkListener); voidaddAudioBroadcastListener(GBSIPAgentAudioBroadcastListeneraudioBroadcastListener); voidaddDeviceControlListener(GBSIPAgentDeviceControlListenerdeviceControlListener); voidaddQueryCommandListener(GBSIPAgentQueryCommandListenerqueryCommandListener); voidaddQueryRecordInfoListener(GBSIPAgentQueryRecordInfoListenerqueryRecordInfoListener); /*历史视音频文件检索应答*/booleanrespondRecordInfoQueryCommand(StringfromUserName, StringfromUserNameAtDomain, StringtoUserName,StringdeviceName, RecordQueryInfoqueryInfo, java.util.List<RecordFileInfo>recordList); }
RecordQueryInfo设计如下:
//GBSIPAgentQueryRecordInfoListener//Author: daniusdk.compackagecom.gb.ntsignalling; publicinterfaceGBSIPAgentQueryRecordInfoListener { voidntsOnQueryRecordInfoCommand(StringfromUserName, StringfromUserNameAtDomain, StringtoUserName, RecordQueryInforecordQueryInfo); } packagecom.gb.ntsignalling; publicinterfaceRecordQueryInfo { /**命令序列号(必选)*/StringgetSN(); /** 目录设备/视频监控联网系统/区域编码(必选)*/StringgetDeviceID(); /** 录像起始时间(必选)*/StringgetStartTime(); /** 录像终止时间(必选)*/StringgetEndTime(); /** 文件路径名 (可选)*/StringgetFilePath(); /** 录像地址(可选 支持不完全查询)*/StringgetAddress(); /** 保密属性(可选)缺省为0;0:不涉密,1:涉密*/StringgetSecrecy(); /** 录像产生类型(可选)time或alarm 或 manual或all*/StringgetType(); /** 录像触发者ID(可选)*/StringgetRecorderID(); /**录像模糊查询属性(可选)缺省为0;0:不进行模糊查询,此时根据 SIP 消息中 To头域*URI中的ID值确定查询录像位置,若ID值为本域系统ID 则进行中心历史记录检索,若为前*端设备ID则进行前端设备历史记录检索;1:进行模糊查询,此时设备所在域应同时进行中心*检索和前端检索并将结果统一返回.*/StringgetIndistinctQuery(); }
RecordFileInfo设计如下:
//RecordFileInfo.java//Author: daniusdk.compackagecom.gb.ntsignalling; publicclassRecordFileInfo { /* 设备/区域编码(必选) */privateStringmDeviceID; /* 设备/区域名称(必选) */privateStringmName; /*文件路径名 (可选)*/privateStringmFilePath; /*录像地址(可选)*/privateStringmAddress; /*录像开始时间(可选)*/privateStringmStartTime; /*录像结束时间(可选)*/privateStringmEndTime; /*保密属性(必选)缺省为0;0:不涉密,1:涉密*/privateStringmSecrecy="0"; /*录像产生类型(可选)time或alarm 或 manual*/privateStringmType; /*录像触发者ID(可选)*/privateStringmRecorderID; /*录像文件大小,单位:Byte(可选)*/privateStringmFileSize; publicRecordFileInfo() { } publicRecordFileInfo(StringdeviceID) { this.setDeviceID(deviceID); } publicRecordFileInfo(StringdeviceID, Stringname) { this.setDeviceID(deviceID); this.setName(name); } publicStringgetDeviceID() { returnmDeviceID; } publicvoidsetDeviceID(StringdeviceID) { this.mDeviceID=deviceID; } publicStringgetName() { returnmName; } publicvoidsetName(Stringname) { this.mName=name; } publicStringgetFilePath() { returnmFilePath; } publicvoidsetFilePath(StringfilePath) { this.mFilePath=filePath; } publicStringgetAddress() { returnmAddress; } publicvoidsetAddress(Stringaddress) { this.mAddress=address; } publicStringgetStartTime() { returnmStartTime; } publicvoidsetStartTime(StringstartTime) { this.mStartTime=startTime; } publicStringgetEndTime() { returnmEndTime; } publicvoidsetEndTime(StringendTime) { this.mEndTime=endTime; } publicStringgetSecrecy() { returnmSecrecy; } publicvoidsetSecrecy(Stringsecrecy) { this.mSecrecy=secrecy; } publicStringgetType() { returnmType; } publicvoidsetType(Stringtype) { this.mType=type; } publicStringgetRecorderID() { returnmRecorderID; } publicvoidsetRecorderID(StringrecorderID) { this.mRecorderID=recorderID; } publicStringgetFileSize() { returnmFileSize; } publicvoidsetFileSize(StringfileSize) { this.mFileSize=fileSize; } }
调用逻辑如下:
packagecom.mydemo; importcom.gb.ntsignalling.GBSIPAgentQueryRecordInfoListener; publicclassAndroidG8181DemoImplimplementsGBSIPAgentQueryRecordInfoListener { privatestaticclassQueryRecordInfoTaskextendsRecordExecutorService.CancelableTask { publicvoidrun() { RecordBaseQuerybase_query=newRecordBaseQuery(get_canceler(), rec_dir_); java.util.Datestart_time_lower=base_query.parser_xml_date_time(record_query_info_.getStartTime()); java.util.Datestart_time_upper=base_query.parser_xml_date_time(record_query_info_.getEndTime()); if (null==start_time_lower||null==start_time_upper) { Log.e(TAG, "start_time_lower:"+start_time_lower+" or start_time_upper:"+start_time_upper+" is null"); return; } base_query.set_start_time_lower(start_time_lower); base_query.set_start_time_upper(start_time_upper); List<RecordFileDescription>file_list=base_query.execute(); if (is_cancel()) return; file_list=base_query.sort_by_start_time_asc(file_list); if (is_cancel()) return; List<com.gb.ntsignalling.RecordFileInfo>list=base_query.to_record_file_info_list(file_list, record_query_info_.getDeviceID(), null); if (is_cancel()) return; if (file_list!=null) { for (RecordFileDescriptioni : file_list) Log.i(TAG, i.toString(base_query.get_print_begin_date_time_format(), base_query.get_print_end_date_time_format())); } if (is_cancel() ||null==handler_||null==sip_agent_) return; Handlerhandler=handler_.get(); GBSIPAgentsip_agent=sip_agent_.get(); if (null==handler||null==sip_agent) return; handler.post(newRunnable() { publicvoidrun() { if (null==this.sip_agent_) return; GBSIPAgentsip_agent=this.sip_agent_.get(); if (null==sip_agent) return; if (this.canceler_!=null&&this.canceler_.get()) return; Stringdevice_name=null; sip_agent.respondRecordInfoQueryCommand(from_user_name_, from_user_name_at_domain_, to_user_name_, device_name, this.record_query_info_, this.record_list_); } privateWeakReference<GBSIPAgent>sip_agent_; privateAtomicBooleancanceler_; privateStringfrom_user_name_; privateStringfrom_user_name_at_domain_; privateStringto_user_name_; privateRecordQueryInforecord_query_info_; privateList<RecordFileInfo>record_list_; publicRunnableset(GBSIPAgentsip_agent, AtomicBooleancanceler, Stringfrom_user_name, Stringfrom_user_name_at_domain, Stringto_user_name, RecordQueryInforecord_query_info, List<RecordFileInfo>record_list) { this.sip_agent_=newWeakReference<>(sip_agent); this.canceler_=canceler; this.from_user_name_=from_user_name; this.from_user_name_at_domain_=from_user_name_at_domain; this.to_user_name_=to_user_name; this.record_query_info_=record_query_info; this.record_list_=record_list; returnthis; } }.set(sip_agent, get_canceler(), this.from_user_name_, this.from_user_name_at_domain_, this.to_user_name_, this.record_query_info_, list)); } publicQueryRecordInfoTaskset(Handlerhandler, GBSIPAgentsip_agent, Stringrec_dir, Stringfrom_user_name, Stringfrom_user_name_at_domain, Stringto_user_name, RecordQueryInfoquery_info) { this.handler_=newWeakReference<>(handler); this.sip_agent_=newWeakReference<>(sip_agent); this.rec_dir_=rec_dir; this.from_user_name_=from_user_name; this.from_user_name_at_domain_=from_user_name_at_domain; this.to_user_name_=to_user_name; this.record_query_info_=query_info; returnthis; } privateWeakReference<Handler>handler_; privateWeakReference<GBSIPAgent>sip_agent_; privateStringrec_dir_; privateStringfrom_user_name_; privateStringfrom_user_name_at_domain_; privateStringto_user_name_; privateRecordQueryInforecord_query_info_; } publicvoidntsOnQueryRecordInfoCommand(StringfromUserName, StringfromUserNameAtDomain, finalStringtoUserName, RecordQueryInforecordQueryInfo) { handler_.post(newRunnable() { publicvoidrun() { Log.i(TAG, "ntsOnQueryRecordInfoCommand from_user_name:"+from_user_name_+", to_user_name:"+to_user_name_+", sn:"+record_query_info_.getSN() +", device_id:"+record_query_info_.getDeviceID() +", start_time:"+record_query_info_.getStartTime() +", end_time:"+record_query_info_.getEndTime()); QueryRecordInfoTaskquery_task=newQueryRecordInfoTask(); query_task.set(handler_, gb28181_agent_, recDir, from_user_name_, from_user_name_at_domain_, to_user_name_, record_query_info_); if (!record_executor_.submit(query_task)) Log.e(TAG, "ntsOnQueryRecordInfoCommand call record_executor_.submit failed"); } privateStringfrom_user_name_; privateStringfrom_user_name_at_domain_; privateStringto_user_name_; privateRecordQueryInforecord_query_info_; publicRunnableset(Stringfrom_user_name, Stringfrom_user_name_at_domain, Stringto_user_name, RecordQueryInforecord_query_info) { this.from_user_name_=from_user_name; this.from_user_name_at_domain_=from_user_name_at_domain; this.to_user_name_=to_user_name; this.record_query_info_=record_query_info; returnthis; } }.set(fromUserName, fromUserNameAtDomain, toUserName, recordQueryInfo)); } }
总结
GB28181设备接入侧视音频历史文件查询,看似不难,实际上需要处理的逻辑还很多,感兴趣的开发者,可以通过平台,和我私信探讨。