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

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

技术背景

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

规范解读

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

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

命令流程:

image.gif视音频文件检索基本流程.png

信令流程描述如下:

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

    image.gif

    没查到录像,那么设备侧回复如下,没有查询到文件的话,<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>

    image.gif

    有查询结果:

    有查询结果-录像检索.png

    image.gif

    <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>

    image.gif

    设备侧回复如下:

    <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>

    image.gif

    需要注意的是,会话外的SIP MESSAGE请求大小不能超过1300个字节。

    技术实现

    以大牛直播SDK的Android平台GB28181设备接入侧为例,设计接口逻辑如下:

    packagecom.gb.ntsignalling;
    publicinterfaceGBSIPAgent {
    voidaddListener(GBSIPAgentListenerlistener);
    voidaddPlayListener(GBSIPAgentPlayListenerplayListener);
    voidremovePlayListener(GBSIPAgentPlayListenerplayListener);
    voidaddDownloadListener(GBSIPAgentDownloadListenerdownloadListener);
    voidremoveDownloadListener(GBSIPAgentDownloadListenerremoveListener);
    voidaddTalkListener(GBSIPAgentTalkListenertalkListener);
    voidremoveTalkListener(GBSIPAgentTalkListenertalkListener);
    voidaddAudioBroadcastListener(GBSIPAgentAudioBroadcastListeneraudioBroadcastListener);
    voidaddDeviceControlListener(GBSIPAgentDeviceControlListenerdeviceControlListener);
    voidaddQueryCommandListener(GBSIPAgentQueryCommandListenerqueryCommandListener);
    voidaddQueryRecordInfoListener(GBSIPAgentQueryRecordInfoListenerqueryRecordInfoListener);
    /*历史视音频文件检索应答*/booleanrespondRecordInfoQueryCommand(StringfromUserName, StringfromUserNameAtDomain, StringtoUserName,StringdeviceName, RecordQueryInfoqueryInfo,
    java.util.List<RecordFileInfo>recordList);
    }

    image.gif

    RecordQueryInfo设计如下:

    //GBSIPAgentQueryRecordInfoListener//Author: daniusdk.compackagecom.gb.ntsignalling;
    publicinterfaceGBSIPAgentQueryRecordInfoListener {
    voidntsOnQueryRecordInfoCommand(StringfromUserName, StringfromUserNameAtDomain,
    StringtoUserName,
    RecordQueryInforecordQueryInfo);
    }
    packagecom.gb.ntsignalling;
    publicinterfaceRecordQueryInfo {
    /**命令序列号(必选)*/StringgetSN();
    /** 目录设备/视频监控联网系统/区域编码(必选)*/StringgetDeviceID();
    /** 录像起始时间(必选)*/StringgetStartTime();
    /** 录像终止时间(必选)*/StringgetEndTime();
    /** 文件路径名 (可选)*/StringgetFilePath();
    /** 录像地址(可选 支持不完全查询)*/StringgetAddress();
    /** 保密属性(可选)缺省为0;0:不涉密,1:涉密*/StringgetSecrecy();
    /** 录像产生类型(可选)time或alarm 或 manual或all*/StringgetType();
    /** 录像触发者ID(可选)*/StringgetRecorderID();
    /**录像模糊查询属性(可选)缺省为0;0:不进行模糊查询,此时根据 SIP 消息中 To头域*URI中的ID值确定查询录像位置,若ID值为本域系统ID 则进行中心历史记录检索,若为前*端设备ID则进行前端设备历史记录检索;1:进行模糊查询,此时设备所在域应同时进行中心*检索和前端检索并将结果统一返回.*/StringgetIndistinctQuery();
    }

    image.gif

    RecordFileInfo设计如下:

    //RecordFileInfo.java//Author: daniusdk.compackagecom.gb.ntsignalling;
    publicclassRecordFileInfo {
    /* 设备/区域编码(必选) */privateStringmDeviceID;
    /* 设备/区域名称(必选) */privateStringmName;
    /*文件路径名 (可选)*/privateStringmFilePath;
    /*录像地址(可选)*/privateStringmAddress;
    /*录像开始时间(可选)*/privateStringmStartTime;
    /*录像结束时间(可选)*/privateStringmEndTime;
    /*保密属性(必选)缺省为0;0:不涉密,1:涉密*/privateStringmSecrecy="0";
    /*录像产生类型(可选)time或alarm 或 manual*/privateStringmType;
    /*录像触发者ID(可选)*/privateStringmRecorderID;
    /*录像文件大小,单位:Byte(可选)*/privateStringmFileSize;
    publicRecordFileInfo() { }
    publicRecordFileInfo(StringdeviceID) {
    this.setDeviceID(deviceID);
        }
    publicRecordFileInfo(StringdeviceID, Stringname) {
    this.setDeviceID(deviceID);
    this.setName(name);
        }
    publicStringgetDeviceID() {
    returnmDeviceID;
        }
    publicvoidsetDeviceID(StringdeviceID) {
    this.mDeviceID=deviceID;
        }
    publicStringgetName() {
    returnmName;
        }
    publicvoidsetName(Stringname) {
    this.mName=name;
        }
    publicStringgetFilePath() {
    returnmFilePath;
        }
    publicvoidsetFilePath(StringfilePath) {
    this.mFilePath=filePath;
        }
    publicStringgetAddress() {
    returnmAddress;
        }
    publicvoidsetAddress(Stringaddress) {
    this.mAddress=address;
        }
    publicStringgetStartTime() {
    returnmStartTime;
        }
    publicvoidsetStartTime(StringstartTime) {
    this.mStartTime=startTime;
        }
    publicStringgetEndTime() {
    returnmEndTime;
        }
    publicvoidsetEndTime(StringendTime) {
    this.mEndTime=endTime;
        }
    publicStringgetSecrecy() {
    returnmSecrecy;
        }
    publicvoidsetSecrecy(Stringsecrecy) {
    this.mSecrecy=secrecy;
        }
    publicStringgetType() {
    returnmType;
        }
    publicvoidsetType(Stringtype) {
    this.mType=type;
        }
    publicStringgetRecorderID() {
    returnmRecorderID;
        }
    publicvoidsetRecorderID(StringrecorderID) {
    this.mRecorderID=recorderID;
        }
    publicStringgetFileSize() {
    returnmFileSize;
        }
    publicvoidsetFileSize(StringfileSize) {
    this.mFileSize=fileSize;
        }
    }

    image.gif

    调用逻辑如下:

    packagecom.mydemo;
    importcom.gb.ntsignalling.GBSIPAgentQueryRecordInfoListener;
    publicclassAndroidG8181DemoImplimplementsGBSIPAgentQueryRecordInfoListener {
    privatestaticclassQueryRecordInfoTaskextendsRecordExecutorService.CancelableTask {
    @Overridepublicvoidrun() {
    RecordBaseQuerybase_query=newRecordBaseQuery(get_canceler(), rec_dir_);
    java.util.Datestart_time_lower=base_query.parser_xml_date_time(record_query_info_.getStartTime());
    java.util.Datestart_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 (RecordFileDescriptioni : 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;
    Handlerhandler=handler_.get();
    GBSIPAgentsip_agent=sip_agent_.get();
    if (null==handler||null==sip_agent)
    return;
    handler.post(newRunnable() {
    @Overridepublicvoidrun() {
    if (null==this.sip_agent_)
    return;
    GBSIPAgentsip_agent=this.sip_agent_.get();
    if (null==sip_agent)
    return;
    if (this.canceler_!=null&&this.canceler_.get())
    return;
    Stringdevice_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_);
                    }
    privateWeakReference<GBSIPAgent>sip_agent_;
    privateAtomicBooleancanceler_;
    privateStringfrom_user_name_;
    privateStringfrom_user_name_at_domain_;
    privateStringto_user_name_;
    privateRecordQueryInforecord_query_info_;
    privateList<RecordFileInfo>record_list_;
    publicRunnableset(GBSIPAgentsip_agent, AtomicBooleancanceler, Stringfrom_user_name, Stringfrom_user_name_at_domain, Stringto_user_name,
    RecordQueryInforecord_query_info, List<RecordFileInfo>record_list) {
    this.sip_agent_=newWeakReference<>(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;
    returnthis;
                    }
                }.set(sip_agent, get_canceler(), this.from_user_name_, this.from_user_name_at_domain_, this.to_user_name_,
    this.record_query_info_, list));
            }
    publicQueryRecordInfoTaskset(Handlerhandler, GBSIPAgentsip_agent, Stringrec_dir,
    Stringfrom_user_name, Stringfrom_user_name_at_domain,
    Stringto_user_name, RecordQueryInfoquery_info) {
    this.handler_=newWeakReference<>(handler);
    this.sip_agent_=newWeakReference<>(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;
    returnthis;
            }
    privateWeakReference<Handler>handler_;
    privateWeakReference<GBSIPAgent>sip_agent_;
    privateStringrec_dir_;
    privateStringfrom_user_name_;
    privateStringfrom_user_name_at_domain_;
    privateStringto_user_name_;
    privateRecordQueryInforecord_query_info_;
        }
    @OverridepublicvoidntsOnQueryRecordInfoCommand(StringfromUserName, StringfromUserNameAtDomain, finalStringtoUserName,
    RecordQueryInforecordQueryInfo) {
    handler_.post(newRunnable() {
    @Overridepublicvoidrun() {
    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());
    QueryRecordInfoTaskquery_task=newQueryRecordInfoTask();
    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");
                }
    privateStringfrom_user_name_;
    privateStringfrom_user_name_at_domain_;
    privateStringto_user_name_;
    privateRecordQueryInforecord_query_info_;
    publicRunnableset(Stringfrom_user_name, Stringfrom_user_name_at_domain, Stringto_user_name, RecordQueryInforecord_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;
    returnthis;
                }
            }.set(fromUserName, fromUserNameAtDomain, toUserName, recordQueryInfo));
        }
    }

    image.gif

    总结

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

    相关文章
    |
    4月前
    |
    Java Android开发 Swift
    安卓与iOS开发对比:平台选择对项目成功的影响
    【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
    144 1
    |
    2月前
    |
    IDE 开发工具 Android开发
    移动应用开发之旅:探索Android和iOS平台
    在这篇文章中,我们将深入探讨移动应用开发的两个主要平台——Android和iOS。我们将了解它们的操作系统、开发环境和工具,并通过代码示例展示如何在这两个平台上创建一个简单的“Hello World”应用。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧,帮助你更好地理解和掌握移动应用开发。
    89 17
    |
    4月前
    |
    Linux API 开发工具
    FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
    ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
    140 0
    FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
    |
    3月前
    |
    开发框架 前端开发 Android开发
    安卓与iOS开发中的跨平台策略
    在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
    |
    11天前
    |
    前端开发 Java Shell
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    87 20
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    |
    8天前
    |
    Dart 前端开发 Android开发
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    28 4
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
    |
    24天前
    |
    缓存 前端开发 Android开发
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
    58 12
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
    |
    28天前
    |
    Dart 前端开发 Android开发
    【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
    【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
    35 1
    【02】写一个注册页面以及配置打包选项打包安卓apk测试—开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草央千澈
    |
    2月前
    |
    搜索推荐 前端开发 API
    探索安卓开发中的自定义视图:打造个性化用户界面
    在安卓应用开发的广阔天地中,自定义视图是一块神奇的画布,让开发者能够突破标准控件的限制,绘制出独一无二的用户界面。本文将带你走进自定义视图的世界,从基础概念到实战技巧,逐步揭示如何在安卓平台上创建和运用自定义视图来提升用户体验。无论你是初学者还是有一定经验的开发者,这篇文章都将为你打开新的视野,让你的应用在众多同质化产品中脱颖而出。
    76 19
    |
    2月前
    |
    JSON Java API
    探索安卓开发:打造你的首个天气应用
    在这篇技术指南中,我们将一起潜入安卓开发的海洋,学习如何从零开始构建一个简单的天气应用。通过这个实践项目,你将掌握安卓开发的核心概念、界面设计、网络编程以及数据解析等技能。无论你是初学者还是有一定基础的开发者,这篇文章都将为你提供一个清晰的路线图和实用的代码示例,帮助你在安卓开发的道路上迈出坚实的一步。让我们一起开始这段旅程,打造属于你自己的第一个安卓应用吧!
    89 14

    相关实验场景

    更多