技术背景
我们在做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设备接入侧为例,设计接口逻辑如下:
package com.gb.ntsignalling;
public interface GBSIPAgent {
void addListener(GBSIPAgentListener listener);
void addPlayListener(GBSIPAgentPlayListener playListener);
void removePlayListener(GBSIPAgentPlayListener playListener);
void addDownloadListener(GBSIPAgentDownloadListener downloadListener);
void removeDownloadListener(GBSIPAgentDownloadListener removeListener);
void addTalkListener(GBSIPAgentTalkListener talkListener);
void removeTalkListener(GBSIPAgentTalkListener talkListener);
void addAudioBroadcastListener(GBSIPAgentAudioBroadcastListener audioBroadcastListener);
void addDeviceControlListener(GBSIPAgentDeviceControlListener deviceControlListener);
void addQueryCommandListener(GBSIPAgentQueryCommandListener queryCommandListener);
void addQueryRecordInfoListener(GBSIPAgentQueryRecordInfoListener queryRecordInfoListener);
/*
历史视音频文件检索应答
*/
boolean respondRecordInfoQueryCommand(String fromUserName, String fromUserNameAtDomain, String toUserName,String deviceName, RecordQueryInfo queryInfo,
java.util.List<RecordFileInfo> recordList);
}
RecordQueryInfo设计如下:
//GBSIPAgentQueryRecordInfoListener
//Author: daniusdk.com
package com.gb.ntsignalling;
public interface GBSIPAgentQueryRecordInfoListener {
void ntsOnQueryRecordInfoCommand(String fromUserName, String fromUserNameAtDomain,
String toUserName,
RecordQueryInfo recordQueryInfo);
}
package com.gb.ntsignalling;
public interface RecordQueryInfo {
/*
*命令序列号(必选)
*/
String getSN();
/*
* 目录设备/视频监控联网系统/区域编码(必选)
*/
String getDeviceID();
/*
* 录像起始时间(必选)
*/
String getStartTime();
/*
* 录像终止时间(必选)
*/
String getEndTime();
/*
* 文件路径名 (可选)
*/
String getFilePath();
/*
* 录像地址(可选 支持不完全查询)
*/
String getAddress();
/*
* 保密属性(可选)缺省为0;0:不涉密,1:涉密
*/
String getSecrecy();
/*
* 录像产生类型(可选)time或alarm 或 manual或all
*/
String getType();
/*
* 录像触发者ID(可选)
*/
String getRecorderID();
/*
*录像模糊查询属性(可选)缺省为0;0:不进行模糊查询,此时根据 SIP 消息中 To头域
*URI中的ID值确定查询录像位置,若ID值为本域系统ID 则进行中心历史记录检索,若为前
*端设备ID则进行前端设备历史记录检索;1:进行模糊查询,此时设备所在域应同时进行中心
*检索和前端检索并将结果统一返回.
*/
String getIndistinctQuery();
}
RecordFileInfo设计如下:
//RecordFileInfo.java
//Author: daniusdk.com
package com.gb.ntsignalling;
public class RecordFileInfo {
/* 设备/区域编码(必选) */
private String mDeviceID;
/* 设备/区域名称(必选) */
private String mName;
/*文件路径名 (可选)*/
private String mFilePath;
/*录像地址(可选)*/
private String mAddress;
/*录像开始时间(可选)*/
private String mStartTime;
/*录像结束时间(可选)*/
private String mEndTime;
/*保密属性(必选)缺省为0;0:不涉密,1:涉密*/
private String mSecrecy = "0";
/*录像产生类型(可选)time或alarm 或 manual*/
private String mType;
/*录像触发者ID(可选)*/
private String mRecorderID;
/*录像文件大小,单位:Byte(可选)*/
private String mFileSize;
public RecordFileInfo() { }
public RecordFileInfo(String deviceID) {
this.setDeviceID(deviceID);
}
public RecordFileInfo(String deviceID, String name) {
this.setDeviceID(deviceID);
this.setName(name);
}
public String getDeviceID() {
return mDeviceID;
}
public void setDeviceID(String deviceID) {
this.mDeviceID = deviceID;
}
public String getName() {
return mName;
}
public void setName(String name) {
this.mName = name;
}
public String getFilePath() {
return mFilePath;
}
public void setFilePath(String filePath) {
this.mFilePath = filePath;
}
public String getAddress() {
return mAddress;
}
public void setAddress(String address) {
this.mAddress = address;
}
public String getStartTime() {
return mStartTime;
}
public void setStartTime(String startTime) {
this.mStartTime = startTime;
}
public String getEndTime() {
return mEndTime;
}
public void setEndTime(String endTime) {
this.mEndTime = endTime;
}
public String getSecrecy() {
return mSecrecy;
}
public void setSecrecy(String secrecy) {
this.mSecrecy = secrecy;
}
public String getType() {
return mType;
}
public void setType(String type) {
this.mType = type;
}
public String getRecorderID() {
return mRecorderID;
}
public void setRecorderID(String recorderID) {
this.mRecorderID = recorderID;
}
public String getFileSize() {
return mFileSize;
}
public void setFileSize(String fileSize) {
this.mFileSize = fileSize;
}
}
调用逻辑如下:
package com.mydemo;
import com.gb.ntsignalling.GBSIPAgentQueryRecordInfoListener;
public class AndroidG8181DemoImpl implements GBSIPAgentQueryRecordInfoListener {
private static class QueryRecordInfoTask extends RecordExecutorService.CancelableTask {
@Override
public void run() {
RecordBaseQuery base_query = new RecordBaseQuery(get_canceler(), rec_dir_);
java.util.Date start_time_lower = base_query.parser_xml_date_time(record_query_info_.getStartTime());
java.util.Date start_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 (RecordFileDescription i : 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;
Handler handler = handler_.get();
GBSIPAgent sip_agent = sip_agent_.get();
if (null == handler || null == sip_agent)
return;
handler.post(new Runnable() {
@Override
public void run() {
if (null == this.sip_agent_)
return;
GBSIPAgent sip_agent = this.sip_agent_.get();
if (null == sip_agent)
return;
if (this.canceler_ != null && this.canceler_.get())
return;
String device_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_);
}
private WeakReference<GBSIPAgent> sip_agent_;
private AtomicBoolean canceler_;
private String from_user_name_;
private String from_user_name_at_domain_;
private String to_user_name_;
private RecordQueryInfo record_query_info_;
private List<RecordFileInfo> record_list_;
public Runnable set(GBSIPAgent sip_agent, AtomicBoolean canceler, String from_user_name, String from_user_name_at_domain, String to_user_name,
RecordQueryInfo record_query_info, List<RecordFileInfo> record_list) {
this.sip_agent_ = new WeakReference<>(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;
return this;
}
}.set(sip_agent, get_canceler(), this.from_user_name_, this.from_user_name_at_domain_, this.to_user_name_,
this.record_query_info_, list));
}
public QueryRecordInfoTask set(Handler handler, GBSIPAgent sip_agent, String rec_dir,
String from_user_name, String from_user_name_at_domain,
String to_user_name, RecordQueryInfo query_info) {
this.handler_ = new WeakReference<>(handler);
this.sip_agent_ = new WeakReference<>(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;
return this;
}
private WeakReference<Handler> handler_;
private WeakReference<GBSIPAgent> sip_agent_;
private String rec_dir_;
private String from_user_name_;
private String from_user_name_at_domain_;
private String to_user_name_;
private RecordQueryInfo record_query_info_;
}
@Override
public void ntsOnQueryRecordInfoCommand(String fromUserName, String fromUserNameAtDomain, final String toUserName,
RecordQueryInfo recordQueryInfo) {
handler_.post(new Runnable() {
@Override
public void run() {
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());
QueryRecordInfoTask query_task = new QueryRecordInfoTask();
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");
}
private String from_user_name_;
private String from_user_name_at_domain_;
private String to_user_name_;
private RecordQueryInfo record_query_info_;
public Runnable set(String from_user_name, String from_user_name_at_domain, String to_user_name, RecordQueryInfo record_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;
return this;
}
}.set(fromUserName, fromUserNameAtDomain, toUserName, recordQueryInfo));
}
}
总结
GB28181设备接入侧视音频历史文件查询,看似不难,实际上需要处理的逻辑还很多,感兴趣的开发者,可以通过平台,和我私信探讨。