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

相关推荐
Tiny_React4 小时前
使用 Claude Code Skills 模拟的视频生成流程
人工智能·音视频开发·vibecoding
aqi001 天前
FFmpeg开发笔记(九十八)基于FFmpeg的跨平台图形用户界面LosslessCut
android·ffmpeg·kotlin·音视频·直播·流媒体
aqi002 天前
FFmpeg开发笔记(九十七)国产的开源视频剪辑工具AndroidVideoEditor
android·ffmpeg·音视频·直播·流媒体
aqi003 天前
FFmpeg开发笔记(一百)国产的Android开源视频压缩工具VideoSlimmer
android·ffmpeg·音视频·直播·流媒体
haibindev5 天前
【终极踩坑指南】Windows 10上MsQuic证书加载失败?坑不在证书,而在Schannel!
直播·http3·quic·流媒体
飞鸟真人8 天前
livekit搭建与使用浏览器测试
直播·视频会议·视频聊天·livekit
hk11249 天前
【音视频/边缘计算】2025年度H.265/HEVC高并发解码与画质修复(Super-Resolution)基准测试报告(含沙丘/失控玩家核心样本)
ffmpeg·边缘计算·音视频开发·h.265·测试数据集
aqi0016 天前
FFmpeg开发笔记(九十五)国产的开源视频美颜工具VideoEditorForAndroid
android·ffmpeg·音视频·直播·流媒体
sno_guo17 天前
直播抠图技术100谈之17----相机帧率和直播帧率如何定?
直播·内容运营·抠图·直播运营·直播伴侣
李小轰_Rex19 天前
把手机变成听诊器!摄像头 30 秒隔空测心率 - 开箱即用
android·音视频开发