Android平台GB28181历史视音频文件检索规范探讨及技术实现

​技术背景

我们在做Android平台GB28181设备接入侧模块的时候,特别是执法记录仪或类似场景,系统除了对常规的录像有要求,还需要能和GB28181平台侧交互,比如实现设备侧视音频文件检索、下载或回放。本文假定记录仪或相关设备已经完成录像,主要来探讨下设备视音频文件检索相关。

规范解读

先回顾下GB/T28181-2016视音频文件检索基本要求:

文件检索主要用区域、设备、录像时间段、录像地点、录像内容为条件进行查询,用 Message 消息发送检索请求和返回查询结果,传送结果的 Message 消息可以发送多条,应支持附录 N 多响应消息传输的要求。文件检索请求和应答命令采用 MANSCDP 协议格式定义。

命令流程:

信令流程描述如下:

  1. 目录检索方向目录拥有方发送目录查询请求 Message 消息,消息体中包含视音频文件检索条件;
  2. 目录拥有方向目录检索方发送 200 OK,无消息体;
  3. 目录拥有方向目录检索方发送查询结果,消息体中含文件目录,当一条 Message 消息无法传送完所有查询结果时,采用多条消息传送;
  4. 目录检索方向目录拥有方发送 200 OK,无消息体。

无查询结果的示例如下:

xml 复制代码
<?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>

没查到录像,那么设备侧回复如下,没有查询到文件的话,元素内容填充"0", 且不携带元素:

xml 复制代码
<?xml version="1.0" encoding="GB2312"?>
<Response>
<CmdType>RecordInfo</CmdType>
<SN>405331641</SN>
<DeviceID>34020000001380000001</DeviceID>
<Name>DaniuSDK</Name>
<SumNum>0</SumNum>
</Response>

有查询结果:

xml 复制代码
<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>

设备侧回复如下:

xml 复制代码
<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设备接入侧为例,设计接口逻辑如下:

arduino 复制代码
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设计如下:

csharp 复制代码
//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设计如下:

typescript 复制代码
//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;
    }
}

调用逻辑如下:

ini 复制代码
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设备接入侧视音频历史文件查询,看似不难,实际上需要处理的逻辑还很多,感兴趣的开发者,可以通过平台,和我私信探讨。

相关推荐
在狂风暴雨中奔跑7 天前
Android+FFmpeg+x264重编码压缩你的视频
音视频开发
音视频牛哥12 天前
[2015~2024]SmartMediaKit音视频直播技术演进之路
音视频开发·视频编码·直播
音视频牛哥14 天前
Windows平台Unity3D下RTMP播放器低延迟设计探讨
音视频开发·视频编码·直播
音视频牛哥14 天前
Windows平台Unity3D下如何低延迟低资源占用播放RTMP或RTSP流?
音视频开发·视频编码·直播
音视频牛哥14 天前
Android平台GB28181设备接入模块动态文字图片水印技术探究
音视频开发·视频编码·直播
陈年15 天前
纯前端视频剪辑
音视频开发
声知视界16 天前
音视频基础能力之 Android 音频篇 (三):高性能音频采集
android·音视频开发
音视频牛哥19 天前
RTSP摄像头8K超高清使用场景探究和播放器要求
音视频开发·视频编码·直播
音视频牛哥19 天前
RTMP如何实现毫秒级延迟体验?
音视频开发·视频编码·直播
CHEtuzki20 天前
现在的电商风口已经很明显了
微信·直播·抖音·电商·录播