Android GB28181历史视音频文件检索

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

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

  信令流程:

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

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

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

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

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

 <?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标准就好,下面给出一个有查询结果的例子和一个无查询结果的例子:

<!----查询结果分两次发送, 总共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代码:

/*
* Copyright (C) 1130758427@qq.com. All rights reserved.
*/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 {@Overridepublic 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() {@Overridepublic 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_;}@Overridepublic void ntsOnQueryRecordInfoCommand(String fromUserName, String fromUserNameAtDomain, final String toUserName,RecordQueryInfo recordQueryInfo) {handler_.post(new Runnable() {@Overridepublic 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, 从协议到一个可靠稳定的代码实现,需要花不少精力和时间.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/71258.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pip安装mysqlclient报错 Exception: Can not find valid pkg-config name

今天docker内搭建python3.10环境时报这个错误&#xff0c;安装 mysqlclient 时报错。 WARNING: The directory /home/seluser/.cache/pip or its parent directory is not owned or is not writable by the current user. The cache has been disabled. Check the permissions…

【理解线性代数】(四)线性运算的推广与矩阵基础

1. 数值加法和乘法 数值加法与乘法&#xff0c;是小学数学课程中的基本数学运算。例如&#xff1a; 加法&#xff1a;112 乘法&#xff1a;2*24 在这个知识层次下&#xff0c;运算的基本单位是数字。 2. 从数值到向量 数值加法&#xff0c;可以看作一维空间中的向量加法&…

【SpringBoot】统一功能处理

目录 &#x1f383;1 拦截器 &#x1f380;1.1 拦截器的代码实现 &#x1f3a8;1.2 拦截器的实现原理 &#x1f9f6;2 拦截器应用——登录验证 &#x1f9ba;3 异常统一处理 &#x1f3ad;4 统一数据返回格式 &#x1f9e4;4.1 为什么需要统一数据返回格式 &#x1f9e3;4.2 统…

Spring Security - 基于内存快速demo

基于内存方式 - 只作学习参考1.引入依赖<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>2.login.html、index.html、fail.htmllogin.html:<form method…

手写Spring:第1章-开篇介绍,手写Spring

文章目录 一、手写Spring二、Spring 生命周期 一、手写Spring &#x1f4a1; 目标&#xff1a;我们该对 Spring 学到什么程度&#xff1f;又该怎么学习呢&#xff1f; 手写简化版 Spring 框架&#xff0c;了解 Spring 核心原理&#xff0c;为后续再深入学习 Spring 打下基础。在…

0013Java程序设计-springboot教材图文内容审核系统

摘 要目 录第1章 绪论1.1 研究背景与意义1.2 研究内容1.3 论文组成结构 系统实现用户登录模块的实现后台管理系统登录模块的实现投稿信息的实现 开发环境 摘 要 《教材图文内容审核系统》课程案例库研究系统系统主要功能模块包括投稿信息、打卡记录、新闻资讯等&#xff0c;采…

MySQL加密的几种常见方式

MySQL提供了多种加密方式来保护数据的安全性。下面是几种常见的MySQL加密方式&#xff1a; 密码加密&#xff1a; MySQL5.7及以上版本使用SHA-256算法对密码进行加密。这种加密方式更安全&#xff0c;可以防止密码泄露。 之前的MySQL版本使用SHA-1算法进行密码加密。这种加密方…

Python UI自动化 —— pytest常用运行参数解析、pytest执行顺序解析

pytest常用Console参数&#xff1a; -v 用于显示每个测试函数的执行结果-q 只显示整体测试结果-s 用于显示测试函数中print()函数输出-x 在第一个错误或失败的测试中立即退出-m 只运行带有装饰器配置的测试用例-k 通过表达式运行指定的测试用例-h 帮助 首先来看什么参数都没加…

曾国藩农民出身,弯道超车实现逆袭的大智慧

曾国藩从小就笨笨的&#xff0c;读书多了才开窍&#xff0c;实现人生逆袭。农民出身&#xff0c;弯道超车&#xff0c;贵在坚持。 约翰生说过&#xff1a;“成大事不在于力量的大小&#xff0c;而在于能坚持多久。” 很多家长认为“不让孩子输在起跑线上”&#xff0c;这是错…

Minifilter过滤驱动与R3程序通讯实现文件保护

实现保护文件或目录、R3层通过与过滤驱动通讯通知要保护的文件或目录,可执行创建不可删除或修改。R3层 #include<Windows.h> #include<fltUser.h> #include<iostream> HANDLE g_hPort INVALID_HANDLE_VALUE;//初始化句柄 #define NPMINI_NAME L"Filei…

文件包含漏洞学习小结

目录 一、介绍 二、常见文件包含函数 三、文件包含漏洞代码举例分析 四、文件包含漏洞利用方式 4.1 本地文件包含 1、读取敏感文件 2、文件包含可运行的php代码 ①包含图片码 ②包含日志文件 ③包含环境变量getshell ④临时文件包含 ⑤伪协议 4.2 远程文件包含 4.…

数据结构与算法学习(day1)——简化版桶排序

文章目录 前言本章目标简化版桶排序题目一题目二 前言 &#xff08;1&#xff09;我是一个大三的学生&#xff08;准确来说应该是准大三&#xff0c;因为明天才报名哈哈哈&#xff09;。 &#xff08;2&#xff09;最近就想每天闲着没事也刷些C语言习题来锻炼下编程水平&#x…

9.4 校招 内推 面经

绿泡*泡&#xff1a; neituijunsir 交流裙 &#xff0c;内推/实习/校招汇总表格 1、校招 | 航天科工二院2024校招 校招 | 航天科工二院2024校招 2、校招 | 中国航空无线电电子研究所2024届校招 校招 | 中国航空无线电电子研究所2024届校招 3、校招 | 南京841研究所2024届校…

Elasticsearch安装,Springboot整合Elasticsearch详细教程

Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎&#xff0c;能够实现近乎实时的搜索。 Elasticsearch官网https://www.elastic.co/cn/ 这篇文章主要简单介绍一下Elasticsearch&#xff0c;Elasticsearch的java API博主也在学习中&#xff0c;文章会持续更新~ …

把握市场潮流,溯源一流品质:在抖in新风潮 国货品牌驶过万重山

好原料、好设计、好品质、好服务……这个2023&#xff0c;“国货”二字再度成为服饰行业的发展关键词。以消费热潮为翼&#xff0c;越来越多代表性品类、头部品牌展现出独特价值&#xff0c;迎风而上&#xff0c;在抖音电商掀起一轮轮生意风潮。 一个设问是&#xff1a;在抖音…

无需设计经验,也能制作出精美的房地产电子传单

在数字化时代&#xff0c;传统的纸质传单已经不能满足人们对于互动和个性化的需求。为此&#xff0c;许多房地产公司开始将目光转向H5微传单&#xff0c;这是一种通过互联网和手机浏览器来传达信息的创新方式。今天&#xff0c;我们将教你如何使用乔拓云网制作房地产微传单H5&a…

Web服务器部署上线踩坑流程回顾

5月份时曾部署上线了C的Web服务器&#xff0c;温故而知新&#xff0c;本篇文章梳理总结一下部署流程知识&#xff1b; 最初的解决方案&#xff1a;https://blog.csdn.net/BinBinCome/article/details/129750951?spm1001.2014.3001.5501后来的解决方案&#xff1a;https://blog…

算法通关村第十二关——不简单的字符串转换问题

前言 字符串是我们在日常开发中最常处理的数据&#xff0c;虽然它本身不是一种数据结构&#xff0c;但是由于其可以包含所有信息&#xff0c;所以通常作为数据的一种形式出现&#xff0c;由于不同语言创建和管理字符串的方式也各有差异&#xff0c;因此针对不同语言特征又产生…

阿里云2核2G云服务器租用价格表_一年费用_1个月和1小时收费

阿里云2核2G服务器多少钱一年&#xff1f;108元一年&#xff0c;折合9元一个月&#xff0c;配置为2核CPU、2G内存、3M带宽、50GB高效云盘的轻量应用服务器&#xff0c;如果是云服务器ECS&#xff0c;2核2G配置可以选择ECS通用算力型u1实例、突发性能实例t6和t5实例、密集计算型…

如何使用HTML和CSS创建动画条形图?

概述 动画栏是使用 HTML 和 CSS 创建的图形动画栏。动画栏的布局是使用 HTML 创建的&#xff0c;栏的样式是使用 CSS 制作的。普通的条形图可以正常创建&#xff0c;但我们必须创建带有动画的条形图&#xff0c;因此我们将使用 CSS 过渡动画属性来使其具有动画效果。我们将构建…