Android GB28181历史视音频文件检索 安卓GB28181历史视音频文件检索

基于安卓系统的执法记录仪、智能头盔等设备,设备端录像、录像查询以及录像文件下载是必不可少的功能, 使用GB28181协议下载安卓设备上的录像文件, 检索录像文件是第一步, 先查询再下载,这里记录下我实现视音频文件检索的一些细节问题.

检索请求和查询结果都使用SIP MESSAGE+MANSCDP协议.

信令流程:

  1. 向安卓设备发送目录查询请求 MESSAGE消息,消息体中包含文件检索条件, 消息体类型为:Application/MANSCDP+xml

  2. 安卓设备向检索方回复200OK, 无消息体.

  3. 安卓根据查询条件执行查询,查询结果用MESSAGE消息发给检索方,消息体类型为:Application/MANSCDP+xml

4.检索方回复200OK, 无消息体.

查询条件的详细定义请参考GB28181标准,这里给出一个例子:

XML 复制代码
 <?xml version="1.0" encoding="GB2312"?>
 <Query>
 <CmdType>RecordInfo</CmdType>
 <SN>73</SN>
 <DeviceID>64010000001310000003</DeviceID>
 <StartTime>2023-08-23T06:07:29</StartTime>
 <EndTime>2023-08-23T22:10:31</EndTime>
 </Query>

查询开始时间是:2023-08-23 06:07:29, 结束时间: 2023-08-23 10:10:31, StartTime和EndTime类型是xs:dateTime , xs:dateTime 详细定义请参考W3C XML Schema Definition Language文档.

xs:dateTime非形式化定义:"YYYY-MM-DDThh:mm:ss", 在国内使用场景中可能不太在意时区问题, 如果遇到时区问题, xs:dateTime也支持时区信息, 例如: "2023-08-23T06:07:29Z" 表示UTC时间, "2023-08-23T06:07:29+08:00" 换算成北京时间是:"2023-08-23 14:07:29".

查询条件中的"StartTime"和"EndTime" 如何使用没找到详细的说明,我的代码实现是这样规定的,一:把这个时间范围定义为半闭半开区间,也就是: [StartTime, EndTime), 二. 查找文件时只判断录像文件的开始时间是否在[StartTime, EndTime)范围内,录像文件的结束时间不考虑.

查询结果的详细定义参考GB28181标准就好,下面给出一个有查询结果的例子和一个无查询结果的例子:

XML 复制代码
<!----查询结果分两次发送, 总共5条----->
<!---第一次发送3条--->
<?xml version="1.0" encoding="GB2312"?>
    <Response>
    <CmdType>RecordInfo</CmdType>
    <SN>73</SN>
    <DeviceID>64010000001310000003</DeviceID>
    <Name>anrdoid-dev-test</Name>
    <SumNum>5</SumNum>
    <RecordList Num="3">
    <Item>
    <DeviceID>64010000001310000003</DeviceID>
    <Name>anrdoid-dev-test</Name>
    <StartTime>2023-08-23T12:27:18</StartTime>
    <EndTime>2023-08-23T12:27:21</EndTime>
    <Secrecy>0</Secrecy>
    </Item>
    <Item>
    <DeviceID>64010000001310000003</DeviceID>
    <Name>anrdoid-dev-test</Name>
    <StartTime>2023-08-23T12:27:23</StartTime>
    <EndTime>2023-08-23T12:27:26</EndTime>
    <Secrecy>0</Secrecy>
    </Item>
    <Item>
    <DeviceID>64010000001310000003</DeviceID>
    <Name>anrdoid-dev-test</Name>
    <StartTime>2023-08-23T16:52:10</StartTime>
    <EndTime>2023-08-23T16:53:01</EndTime>
    <Secrecy>0</Secrecy>
    </Item>
    </RecordList>
    </Response>

<!---第二次发送2条--->
<?xml version="1.0" encoding="GB2312"?>
    <Response>
    <CmdType>RecordInfo</CmdType>
    <SN>73</SN>
    <DeviceID>64010000001310000003</DeviceID>
    <Name>anrdoid-dev-test</Name>
    <SumNum>5</SumNum>
    <RecordList Num="2">
    <Item>
    <DeviceID>64010000001310000003</DeviceID>
    <Name>anrdoid-dev-test</Name>
    <StartTime>2023-08-23T17:21:18</StartTime>
    <EndTime>2023-08-23T17:37:21</EndTime>
    <Secrecy>0</Secrecy>
    </Item>
    <Item>
    <DeviceID>64010000001310000003</DeviceID>
    <Name>anrdoid-dev-test</Name>
    <StartTime>2023-08-23T18:15:22</StartTime>
    <EndTime>2023-08-23T18:33:58</EndTime>
    <Secrecy>0</Secrecy>
    </Item>
    </RecordList>
    </Response>

<!----没有查询到文件的例子--->
	<?xml version="1.0" encoding="GB2312"?>
    <Response>
    <CmdType>RecordInfo</CmdType>
    <SN>73</SN>
    <DeviceID>64010000001310000003</DeviceID>
    <Name>anrdoid-dev-test</Name>
    <SumNum>0</SumNum>
    </Response>

查询结果的一些实现细节:

  1. 如果没有查询到文件,响应中<SumNum>元素内容填充"0", 且不携带<RecordList>元素.

  2. 根据RFC3428

复制代码
The size of MESSAGE requests outside of a media session MUST NOT exceed 1300 bytes,

会话外的SIP MESSAGE请求大小不能超过1300个字节**(1300个字节限制的是整体消息大小,不止是Body Length).**

针对这个消息大小限制问题, GB28181附录给出两种解决方案:

方案一: 对多条查询结果进行拆分,确保每个MESSAGE消息大小不超过1300个字节,每个响应消息的SN要与查询请求的SN相同, 串行发送, 也就是上一次发送的MESSAGE收到200 OK响应后,再发下一批拆分的查询结果, 代码实现上要计算每个SIP消息大小,记录好状态,具体实现起来比较麻烦;

方案二: SIP消息使用TCP传输, 这个需要服务端和安卓设备都支持TCP传输,GB28181要求每条响应消息携带的文件记录数上限为10000条,实际代码实现中建议不要一次发送太多条记录, 要考虑XML解析性能问题,大XML解析挺慢的.

另外我建议文件记录中的可选元素场景中不需要的就不加, 尽可能减少XML大小,降低信令传输带宽.

我的接口定义和Demo代码:

java 复制代码
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);
}


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();
}


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 MyAndroidG8181DemoImpl 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));
    }
}

整个历史视音频文件检索的实现代码较多,这里为了方便说明信令流程和关键细节,只给出接口定义和基本Demo, 如有不清楚的地方请联系qq: 1130758427, 从协议到一个可靠稳定的代码实现,需要花不少精力和时间.

相关推荐
音视频牛哥2 个月前
Android终端GB28181音视频实时回传设计探讨
大牛直播sdk·android gb28181·gb28181安卓端·gb28181平台·gb28181客户端·gb28181-2022·gb28181实时回传
音视频牛哥1 年前
Android平台GB28181执法记录仪技术方案
大牛直播sdk·android gb28181·gb28181平台·gb28181推送·smartgbd·gb28181执法记录仪
音视频牛哥1 年前
如何实现Android视音频数据对接到GB28181平台(SmartGBD)
大牛直播sdk·android gb28181·gb28181平台·gb28181推送·smartgbd
音视频牛哥1 年前
Android平台GB28181设备接入端如何实现多视频通道接入?
大牛直播sdk·android gb28181·gb28181智能安全帽·gb28181多通道·gb28181安卓端